5 * Provides discussion forums.
9 * Implements hook_help().
11 function forum_help($path, $arg) {
13 case 'admin/help#forum':
15 $output .= '<h3>' . t('About') . '</h3>';
16 $output .= '<p>' . t('The Forum module lets you create threaded discussion forums with functionality similar to other message board systems. Forums are useful because they allow community members to discuss topics with one another while ensuring those conversations are archived for later reference. In a forum, users post topics and threads in nested hierarchies, allowing discussions to be categorized and grouped. The forum hierarchy consists of:') . '</p>';
18 $output .= '<li>' . t('Optional containers (for example, <em>Support</em>), which can hold:') . '</li>';
19 $output .= '<ul><li>' . t('Forums (for example, <em>Installing Drupal</em>), which can hold:') . '</li>';
20 $output .= '<ul><li>' . t('Forum topics submitted by users (for example, <em>How to start a Drupal 6 Multisite</em>), which start discussions and are starting points for:') . '</li>';
21 $output .= '<ul><li>' . t('Threaded comments submitted by users (for example, <em>You have these options...</em>).') . '</li>';
26 $output .= '<p>' . t('For more information, see the online handbook entry for <a href="@forum">Forum module</a>.', array('@forum' => 'http://drupal.org/documentation/modules/forum')) . '</p>';
27 $output .= '<h3>' . t('Uses') . '</h3>';
29 $output .= '<dt>' . t('Setting up forum structure') . '</dt>';
30 $output .= '<dd>' . t('Visit the <a href="@forums">Forums page</a> to set up containers and forums to hold your discussion topics.', array('@forums' => url('admin/structure/forum'))) . '</dd>';
31 $output .= '<dt>' . t('Starting a discussion') . '</dt>';
32 $output .= '<dd>' . t('The <a href="@create-topic">Forum topic</a> link on the <a href="@content-add">Add new content</a> page creates the first post of a new threaded discussion, or thread.', array('@create-topic' => url('node/add/forum'), '@content-add' => url('node/add'))) . '</dd>';
33 $output .= '<dt>' . t('Navigation') . '</dt>';
34 $output .= '<dd>' . t('Enabling the Forum module provides a default <em>Forums</em> menu item in the navigation menu that links to the <a href="@forums">Forums page</a>.', array('@forums' => url('forum'))) . '</dd>';
35 $output .= '<dt>' . t('Moving forum topics') . '</dt>';
36 $output .= '<dd>' . t('A forum topic (and all of its comments) may be moved between forums by selecting a different forum while editing a forum topic. When moving a forum topic between forums, the <em>Leave shadow copy</em> option creates a link in the original forum pointing to the new location.') . '</dd>';
37 $output .= '<dt>' . t('Locking and disabling comments') . '</dt>';
38 $output .= '<dd>' . t('Selecting <em>Closed</em> under <em>Comment settings</em> while editing a forum topic will lock (prevent new comments on) the thread. Selecting <em>Hidden</em> under <em>Comment settings</em> while editing a forum topic will hide all existing comments on the thread, and prevent new ones.') . '</dd>';
41 case 'admin/structure/forum':
42 $output = '<p>' . t('Forums contain forum topics. Use containers to group related forums.') . '</p>';
43 $output .= theme('more_help_link', array('url' => 'admin/help/forum'));
45 case 'admin/structure/forum/add/container':
46 return '<p>' . t('Use containers to group related forums.') . '</p>';
47 case 'admin/structure/forum/add/forum':
48 return '<p>' . t('A forum holds related forum topics.') . '</p>';
49 case 'admin/structure/forum/settings':
50 return '<p>' . t('Adjust the display of your forum topics. Organize the forums on the <a href="@forum-structure">forum structure page</a>.', array('@forum-structure' => url('admin/structure/forum'))) . '</p>';
55 * Implements hook_theme().
57 function forum_theme() {
60 'template' => 'forums',
61 'variables' => array('forums' => NULL, 'topics' => NULL, 'parents' => NULL, 'tid' => NULL, 'sortby' => NULL, 'forum_per_page' => NULL),
63 'forum_list' => array(
64 'template' => 'forum-list',
65 'variables' => array('forums' => NULL, 'parents' => NULL, 'tid' => NULL),
67 'forum_topic_list' => array(
68 'template' => 'forum-topic-list',
69 'variables' => array('tid' => NULL, 'topics' => NULL, 'sortby' => NULL, 'forum_per_page' => NULL),
71 'forum_icon' => array(
72 'template' => 'forum-icon',
73 'variables' => array('new_posts' => NULL, 'num_posts' => 0, 'comment_mode' => 0, 'sticky' => 0, 'first_new' => FALSE),
75 'forum_submitted' => array(
76 'template' => 'forum-submitted',
77 'variables' => array('topic' => NULL),
79 'forum_form' => array(
80 'render element' => 'form',
81 'file' => 'forum.admin.inc',
87 * Implements hook_menu().
89 function forum_menu() {
90 $items['forum'] = array(
92 'page callback' => 'forum_page',
93 'access arguments' => array('access content'),
94 'file' => 'forum.pages.inc',
96 $items['forum/%forum_forum'] = array(
98 'page callback' => 'forum_page',
99 'page arguments' => array(1),
100 'access arguments' => array('access content'),
101 'file' => 'forum.pages.inc',
103 $items['admin/structure/forum'] = array(
105 'description' => 'Control forum hierarchy settings.',
106 'page callback' => 'drupal_get_form',
107 'page arguments' => array('forum_overview'),
108 'access arguments' => array('administer forums'),
109 'file' => 'forum.admin.inc',
111 $items['admin/structure/forum/list'] = array(
113 'type' => MENU_DEFAULT_LOCAL_TASK,
116 $items['admin/structure/forum/add/container'] = array(
117 'title' => 'Add container',
118 'page callback' => 'forum_form_main',
119 'page arguments' => array('container'),
120 'access arguments' => array('administer forums'),
121 'type' => MENU_LOCAL_ACTION,
122 'parent' => 'admin/structure/forum',
123 'file' => 'forum.admin.inc',
125 $items['admin/structure/forum/add/forum'] = array(
126 'title' => 'Add forum',
127 'page callback' => 'forum_form_main',
128 'page arguments' => array('forum'),
129 'access arguments' => array('administer forums'),
130 'type' => MENU_LOCAL_ACTION,
131 'parent' => 'admin/structure/forum',
132 'file' => 'forum.admin.inc',
134 $items['admin/structure/forum/settings'] = array(
135 'title' => 'Settings',
136 'page callback' => 'drupal_get_form',
137 'page arguments' => array('forum_admin_settings'),
138 'access arguments' => array('administer forums'),
140 'type' => MENU_LOCAL_TASK,
141 'parent' => 'admin/structure/forum',
142 'file' => 'forum.admin.inc',
144 $items['admin/structure/forum/edit/container/%taxonomy_term'] = array(
145 'title' => 'Edit container',
146 'page callback' => 'forum_form_main',
147 'page arguments' => array('container', 5),
148 'access arguments' => array('administer forums'),
149 'file' => 'forum.admin.inc',
151 $items['admin/structure/forum/edit/forum/%taxonomy_term'] = array(
152 'title' => 'Edit forum',
153 'page callback' => 'forum_form_main',
154 'page arguments' => array('forum', 5),
155 'access arguments' => array('administer forums'),
156 'file' => 'forum.admin.inc',
162 * Implements hook_menu_local_tasks_alter().
164 function forum_menu_local_tasks_alter(&$data, $router_item, $root_path) {
167 // Add action link to 'node/add/forum' on 'forum' sub-pages.
168 if ($root_path == 'forum' || $root_path == 'forum/%') {
169 $tid = (isset($router_item['page_arguments'][0]) ? $router_item['page_arguments'][0]->tid : 0);
170 $forum_term = forum_forum_load($tid);
173 // Loop through all bundles for forum taxonomy vocabulary field.
174 $field = field_info_field('taxonomy_forums');
175 foreach ($field['bundles']['node'] as $type) {
176 if (node_access('create', $type)) {
177 $links[$type] = array(
178 '#theme' => 'menu_local_action',
180 'title' => t('Add new @node_type', array('@node_type' => node_type_get_name($type))),
181 'href' => 'node/add/' . str_replace('_', '-', $type) . '/' . $forum_term->tid,
187 // Authenticated user does not have access to create new topics.
189 $links['disallowed'] = array(
190 '#theme' => 'menu_local_action',
192 'title' => t('You are not allowed to post new content in the forum.'),
196 // Anonymous user does not have access to create new topics.
198 $links['login'] = array(
199 '#theme' => 'menu_local_action',
201 'title' => t('<a href="@login">Log in</a> to post new content in the forum.', array(
202 '@login' => url('user/login', array('query' => drupal_get_destination())),
204 'localized_options' => array('html' => TRUE),
209 $data['actions']['output'] = array_merge($data['actions']['output'], $links);
215 * Implements hook_entity_info_alter().
217 function forum_entity_info_alter(&$info) {
218 // Take over URI construction for taxonomy terms that are forums.
219 if ($vid = variable_get('forum_nav_vocabulary', 0)) {
220 // Within hook_entity_info(), we can't invoke entity_load() as that would
221 // cause infinite recursion, so we call taxonomy_vocabulary_get_names()
222 // instead of taxonomy_vocabulary_load(). All we need is the machine name
223 // of $vid, so retrieving and iterating all the vocabulary names is somewhat
224 // inefficient, but entity info is cached across page requests, and an
225 // iteration of all vocabularies once per cache clearing isn't a big deal,
226 // and is done as part of taxonomy_entity_info() anyway.
227 foreach (taxonomy_vocabulary_get_names() as $machine_name => $vocabulary) {
228 if ($vid == $vocabulary->vid) {
229 $info['taxonomy_term']['bundles'][$machine_name]['uri callback'] = 'forum_uri';
236 * Implements callback_entity_info_uri().
238 * Entity URI callback used in forum_entity_info_alter().
240 function forum_uri($forum) {
242 'path' => 'forum/' . $forum->tid,
247 * Checks whether a node can be used in a forum, based on its content type.
253 * Boolean indicating if the node can be assigned to a forum.
255 function _forum_node_check_node_type($node) {
256 // Fetch information about the forum field.
257 $field = field_info_instance('node', 'taxonomy_forums', $node->type);
259 return is_array($field);
263 * Implements hook_node_view().
265 function forum_node_view($node, $view_mode) {
266 if (_forum_node_check_node_type($node)) {
267 if ($view_mode == 'full' && node_is_page($node)) {
268 $vid = variable_get('forum_nav_vocabulary', 0);
269 $vocabulary = taxonomy_vocabulary_load($vid);
270 // Breadcrumb navigation
271 $breadcrumb[] = l(t('Home'), NULL);
272 $breadcrumb[] = l($vocabulary->name, 'forum');
273 if ($parents = taxonomy_get_parents_all($node->forum_tid)) {
274 $parents = array_reverse($parents);
275 foreach ($parents as $parent) {
276 $breadcrumb[] = l($parent->name, 'forum/' . $parent->tid);
279 drupal_set_breadcrumb($breadcrumb);
286 * Implements hook_node_validate().
288 * Checks in particular that the node is assigned only a "leaf" term in the
291 function forum_node_validate($node, $form) {
292 if (_forum_node_check_node_type($node)) {
293 $langcode = $form['taxonomy_forums']['#language'];
294 // vocabulary is selected, not a "container" term.
295 if (!empty($node->taxonomy_forums[$langcode])) {
296 // Extract the node's proper topic ID.
297 $containers = variable_get('forum_containers', array());
298 foreach ($node->taxonomy_forums[$langcode] as $delta => $item) {
299 // If no term was selected (e.g. when no terms exist yet), remove the
301 if (empty($item['tid'])) {
302 unset($node->taxonomy_forums[$langcode][$delta]);
305 $term = taxonomy_term_load($item['tid']);
307 form_set_error('taxonomy_forums', t('Select a forum.'));
310 $used = db_query_range('SELECT 1 FROM {taxonomy_term_data} WHERE tid = :tid AND vid = :vid',0 , 1, array(
311 ':tid' => $term->tid,
312 ':vid' => $term->vid,
314 if ($used && in_array($term->tid, $containers)) {
315 form_set_error('taxonomy_forums', t('The item %forum is a forum container, not a forum. Select one of the forums below instead.', array('%forum' => $term->name)));
323 * Implements hook_node_presave().
325 * Assigns the forum taxonomy when adding a topic from within a forum.
327 function forum_node_presave($node) {
328 if (_forum_node_check_node_type($node)) {
329 // Make sure all fields are set properly:
330 $node->icon = !empty($node->icon) ? $node->icon : '';
331 reset($node->taxonomy_forums);
332 $langcode = key($node->taxonomy_forums);
333 if (!empty($node->taxonomy_forums[$langcode])) {
334 $node->forum_tid = $node->taxonomy_forums[$langcode][0]['tid'];
335 if (isset($node->nid)) {
336 $old_tid = db_query_range("SELECT f.tid FROM {forum} f INNER JOIN {node} n ON f.vid = n.vid WHERE n.nid = :nid ORDER BY f.vid DESC", 0, 1, array(':nid' => $node->nid))->fetchField();
337 if ($old_tid && isset($node->forum_tid) && ($node->forum_tid != $old_tid) && !empty($node->shadow)) {
338 // A shadow copy needs to be created. Retain new term and add old term.
339 $node->taxonomy_forums[$langcode][] = array('tid' => $old_tid);
347 * Implements hook_node_update().
349 function forum_node_update($node) {
350 if (_forum_node_check_node_type($node)) {
351 if (empty($node->revision) && db_query('SELECT tid FROM {forum} WHERE nid=:nid', array(':nid' => $node->nid))->fetchField()) {
352 if (!empty($node->forum_tid)) {
354 ->fields(array('tid' => $node->forum_tid))
355 ->condition('vid', $node->vid)
358 // The node is removed from the forum.
361 ->condition('nid', $node->nid)
366 if (!empty($node->forum_tid)) {
369 'tid' => $node->forum_tid,
376 // If the node has a shadow forum topic, update the record for this
378 if (!empty($node->shadow)) {
380 ->condition('nid', $node->nid)
381 ->condition('vid', $node->vid)
387 'tid' => $node->forum_tid,
395 * Implements hook_node_insert().
397 function forum_node_insert($node) {
398 if (_forum_node_check_node_type($node)) {
399 if (!empty($node->forum_tid)) {
400 $nid = db_insert('forum')
402 'tid' => $node->forum_tid,
412 * Implements hook_node_delete().
414 function forum_node_delete($node) {
415 if (_forum_node_check_node_type($node)) {
417 ->condition('nid', $node->nid)
419 db_delete('forum_index')
420 ->condition('nid', $node->nid)
426 * Implements hook_node_load().
428 function forum_node_load($nodes) {
429 $node_vids = array();
430 foreach ($nodes as $node) {
431 if (_forum_node_check_node_type($node)) {
432 $node_vids[] = $node->vid;
435 if (!empty($node_vids)) {
436 $query = db_select('forum', 'f');
438 ->fields('f', array('nid', 'tid'))
439 ->condition('f.vid', $node_vids);
440 $result = $query->execute();
441 foreach ($result as $record) {
442 $nodes[$record->nid]->forum_tid = $record->tid;
448 * Implements hook_node_info().
450 function forum_node_info() {
453 'name' => t('Forum topic'),
455 'description' => t('A <em>forum topic</em> starts a new discussion thread within a forum.'),
456 'title_label' => t('Subject'),
462 * Implements hook_permission().
464 function forum_permission() {
466 'administer forums' => array(
467 'title' => t('Administer forums'),
474 * Implements hook_taxonomy_term_delete().
476 function forum_taxonomy_term_delete($term) {
477 // For containers, remove the tid from the forum_containers variable.
478 $containers = variable_get('forum_containers', array());
479 $key = array_search($term->tid, $containers);
480 if ($key !== FALSE) {
481 unset($containers[$key]);
483 variable_set('forum_containers', $containers);
487 * Implements hook_comment_publish().
489 * This actually handles the insertion and update of published nodes since
490 * comment_save() calls hook_comment_publish() for all published comments.
492 function forum_comment_publish($comment) {
493 _forum_update_forum_index($comment->nid);
497 * Implements hook_comment_update().
499 * The Comment module doesn't call hook_comment_unpublish() when saving
500 * individual comments, so we need to check for those here.
502 function forum_comment_update($comment) {
503 // comment_save() calls hook_comment_publish() for all published comments,
504 // so we need to handle all other values here.
505 if (!$comment->status) {
506 _forum_update_forum_index($comment->nid);
511 * Implements hook_comment_unpublish().
513 function forum_comment_unpublish($comment) {
514 _forum_update_forum_index($comment->nid);
518 * Implements hook_comment_delete().
520 function forum_comment_delete($comment) {
521 _forum_update_forum_index($comment->nid);
525 * Implements hook_field_storage_pre_insert().
527 function forum_field_storage_pre_insert($entity_type, $entity, &$skip_fields) {
528 if ($entity_type == 'node' && $entity->status && _forum_node_check_node_type($entity)) {
529 $query = db_insert('forum_index')->fields(array('nid', 'title', 'tid', 'sticky', 'created', 'comment_count', 'last_comment_timestamp'));
530 foreach ($entity->taxonomy_forums as $language) {
531 foreach ($language as $item) {
532 $query->values(array(
533 'nid' => $entity->nid,
534 'title' => $entity->title,
535 'tid' => $item['tid'],
536 'sticky' => $entity->sticky,
537 'created' => $entity->created,
538 'comment_count' => 0,
539 'last_comment_timestamp' => $entity->created,
548 * Implements hook_field_storage_pre_update().
550 function forum_field_storage_pre_update($entity_type, $entity, &$skip_fields) {
551 $first_call = &drupal_static(__FUNCTION__, array());
553 if ($entity_type == 'node' && _forum_node_check_node_type($entity)) {
555 // If the node is published, update the forum index.
556 if ($entity->status) {
558 // We don't maintain data for old revisions, so clear all previous values
559 // from the table. Since this hook runs once per field, per object, make
560 // sure we only wipe values once.
561 if (!isset($first_call[$entity->nid])) {
562 $first_call[$entity->nid] = FALSE;
563 db_delete('forum_index')->condition('nid', $entity->nid)->execute();
565 $query = db_insert('forum_index')->fields(array('nid', 'title', 'tid', 'sticky', 'created', 'comment_count', 'last_comment_timestamp'));
566 foreach ($entity->taxonomy_forums as $language) {
567 foreach ($language as $item) {
568 $query->values(array(
569 'nid' => $entity->nid,
570 'title' => $entity->title,
571 'tid' => $item['tid'],
572 'sticky' => $entity->sticky,
573 'created' => $entity->created,
574 'comment_count' => 0,
575 'last_comment_timestamp' => $entity->created,
580 // The logic for determining last_comment_count is fairly complex, so
581 // call _forum_update_forum_index() too.
582 _forum_update_forum_index($entity->nid);
585 // When a forum node is unpublished, remove it from the forum_index table.
587 db_delete('forum_index')->condition('nid', $entity->nid)->execute();
594 * Implements hook_form_FORM_ID_alter() for taxonomy_form_vocabulary().
596 function forum_form_taxonomy_form_vocabulary_alter(&$form, &$form_state, $form_id) {
597 $vid = variable_get('forum_nav_vocabulary', 0);
598 if (isset($form['vid']['#value']) && $form['vid']['#value'] == $vid) {
599 $form['help_forum_vocab'] = array(
600 '#markup' => t('This is the designated forum vocabulary. Some of the normal vocabulary options have been removed.'),
603 // Forum's vocabulary always has single hierarchy. Forums and containers
604 // have only one parent or no parent for root items. By default this value
606 $form['hierarchy']['#value'] = 1;
607 // Do not allow to delete forum's vocabulary.
608 $form['actions']['delete']['#access'] = FALSE;
613 * Implements hook_form_FORM_ID_alter() for taxonomy_form_term().
615 function forum_form_taxonomy_form_term_alter(&$form, &$form_state, $form_id) {
616 $vid = variable_get('forum_nav_vocabulary', 0);
617 if (isset($form['vid']['#value']) && $form['vid']['#value'] == $vid) {
618 // Hide multiple parents select from forum terms.
619 $form['relations']['parent']['#access'] = FALSE;
624 * Implements hook_form_BASE_FORM_ID_alter() for node_form().
626 function forum_form_node_form_alter(&$form, &$form_state, $form_id) {
627 if (isset($form['taxonomy_forums'])) {
628 $langcode = $form['taxonomy_forums']['#language'];
629 // Make the vocabulary required for 'real' forum-nodes.
630 $form['taxonomy_forums'][$langcode]['#required'] = TRUE;
631 $form['taxonomy_forums'][$langcode]['#multiple'] = FALSE;
632 if (empty($form['taxonomy_forums'][$langcode]['#default_value'])) {
633 // If there is no default forum already selected, try to get the forum
634 // ID from the URL (e.g., if we are on a page like node/add/forum/2, we
635 // expect "2" to be the ID of the forum that was requested).
636 $requested_forum_id = arg(3);
637 $form['taxonomy_forums'][$langcode]['#default_value'] = is_numeric($requested_forum_id) ? $requested_forum_id : '';
643 * Implements hook_block_info().
645 function forum_block_info() {
646 $blocks['active'] = array(
647 'info' => t('Active forum topics'),
648 'cache' => DRUPAL_CACHE_CUSTOM,
649 'properties' => array('administrative' => TRUE),
651 $blocks['new'] = array(
652 'info' => t('New forum topics'),
653 'cache' => DRUPAL_CACHE_CUSTOM,
654 'properties' => array('administrative' => TRUE),
660 * Implements hook_block_configure().
662 function forum_block_configure($delta = '') {
663 $form['forum_block_num_' . $delta] = array(
665 '#title' => t('Number of topics'),
666 '#default_value' => variable_get('forum_block_num_' . $delta, '5'),
667 '#options' => drupal_map_assoc(range(2, 20))
673 * Implements hook_block_save().
675 function forum_block_save($delta = '', $edit = array()) {
676 variable_set('forum_block_num_' . $delta, $edit['forum_block_num_' . $delta]);
680 * Implements hook_block_view().
682 * Generates a block containing the currently active forum topics and the most
683 * recently added forum topics.
685 function forum_block_view($delta = '') {
686 $query = db_select('forum_index', 'f')
688 ->addTag('node_access');
691 $title = t('Active forum topics');
693 ->orderBy('f.last_comment_timestamp', 'DESC')
694 ->range(0, variable_get('forum_block_num_active', '5'));
698 $title = t('New forum topics');
700 ->orderBy('f.created', 'DESC')
701 ->range(0, variable_get('forum_block_num_new', '5'));
705 $block['subject'] = $title;
706 // Cache based on the altered query. Enables us to cache with node access enabled.
707 $block['content'] = drupal_render_cache_by_query($query, 'forum_block_view');
708 $block['content']['#access'] = user_access('access content');
713 * Render API callback: Lists nodes based on the element's #query property.
715 * This function can be used as a #pre_render callback.
717 * @see forum_block_view()
719 function forum_block_view_pre_render($elements) {
720 $result = $elements['#query']->execute();
721 if ($node_title_list = node_title_list($result)) {
722 $elements['forum_list'] = $node_title_list;
723 $elements['forum_more'] = array('#theme' => 'more_link', '#url' => 'forum', '#title' => t('Read the latest forum topics.'));
729 * Implements hook_form().
731 function forum_form($node, $form_state) {
732 $type = node_type_get_type($node);
733 $form['title'] = array(
734 '#type' => 'textfield',
735 '#title' => check_plain($type->title_label),
736 '#default_value' => !empty($node->title) ? $node->title : '',
737 '#required' => TRUE, '#weight' => -5
740 if (!empty($node->nid)) {
741 $forum_terms = $node->taxonomy_forums;
742 // If editing, give option to leave shadows.
743 $shadow = (count($forum_terms) > 1);
744 $form['shadow'] = array('#type' => 'checkbox', '#title' => t('Leave shadow copy'), '#default_value' => $shadow, '#description' => t('If you move this topic, you can leave a link in the old forum to the new forum.'));
745 $form['forum_tid'] = array('#type' => 'value', '#value' => $node->forum_tid);
752 * Returns a tree of all forums for a given taxonomy term ID.
755 * (optional) Taxonomy term ID of the forum. If not given all forums will be
759 * A tree of taxonomy objects, with the following additional properties:
760 * - num_topics: Number of topics in the forum.
761 * - num_posts: Total number of posts in all topics.
762 * - last_post: Most recent post for the forum.
763 * - forums: An array of child forums.
765 function forum_forum_load($tid = NULL) {
766 $cache = &drupal_static(__FUNCTION__, array());
768 // Return a cached forum tree if available.
772 if (isset($cache[$tid])) {
776 $vid = variable_get('forum_nav_vocabulary', 0);
778 // Load and validate the parent term.
780 $forum_term = taxonomy_term_load($tid);
781 if (!$forum_term || ($forum_term->vid != $vid)) {
782 return $cache[$tid] = FALSE;
785 // If $tid is 0, create an empty object to hold the child terms.
786 elseif ($tid === 0) {
787 $forum_term = (object) array(
792 // Determine if the requested term is a container.
793 if (!$forum_term->tid || in_array($forum_term->tid, variable_get('forum_containers', array()))) {
794 $forum_term->container = 1;
797 // Load parent terms.
798 $forum_term->parents = taxonomy_get_parents_all($forum_term->tid);
800 // Load the tree below.
802 $_forums = taxonomy_get_tree($vid, $tid);
804 if (count($_forums)) {
805 $query = db_select('node', 'n');
806 $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
807 $query->join('forum', 'f', 'n.vid = f.vid');
808 $query->addExpression('COUNT(n.nid)', 'topic_count');
809 $query->addExpression('SUM(ncs.comment_count)', 'comment_count');
811 ->fields('f', array('tid'))
812 ->condition('n.status', 1)
814 ->addTag('node_access')
816 ->fetchAllAssoc('tid');
819 foreach ($_forums as $forum) {
820 // Determine if the child term is a container.
821 if (in_array($forum->tid, variable_get('forum_containers', array()))) {
822 $forum->container = 1;
825 // Merge in the topic and post counters.
826 if (!empty($counts[$forum->tid])) {
827 $forum->num_topics = $counts[$forum->tid]->topic_count;
828 $forum->num_posts = $counts[$forum->tid]->topic_count + $counts[$forum->tid]->comment_count;
831 $forum->num_topics = 0;
832 $forum->num_posts = 0;
835 // Query "Last Post" information for this forum.
836 $query = db_select('node', 'n');
837 $query->join('users', 'u1', 'n.uid = u1.uid');
838 $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $forum->tid));
839 $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
840 $query->join('users', 'u2', 'ncs.last_comment_uid = u2.uid');
841 $query->addExpression('CASE ncs.last_comment_uid WHEN 0 THEN ncs.last_comment_name ELSE u2.name END', 'last_comment_name');
844 ->fields('ncs', array('last_comment_timestamp', 'last_comment_uid'))
845 ->condition('n.status', 1)
846 ->orderBy('last_comment_timestamp', 'DESC')
848 ->addTag('node_access')
852 // Merge in the "Last Post" information.
853 $last_post = new stdClass();
854 if (!empty($topic->last_comment_timestamp)) {
855 $last_post->created = $topic->last_comment_timestamp;
856 $last_post->name = $topic->last_comment_name;
857 $last_post->uid = $topic->last_comment_uid;
859 $forum->last_post = $last_post;
861 $forums[$forum->tid] = $forum;
864 // Cache the result, and return the tree.
865 $forum_term->forums = $forums;
866 $cache[$tid] = $forum_term;
871 * Calculates the number of new posts in a forum that the user has not yet read.
873 * Nodes are new if they are newer than NODE_NEW_LIMIT.
876 * The term ID of the forum.
881 * The number of new posts in the forum that have not been read by the user.
883 function _forum_topics_unread($term, $uid) {
884 $query = db_select('node', 'n');
885 $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $term));
886 $query->leftJoin('history', 'h', 'n.nid = h.nid AND h.uid = :uid', array(':uid' => $uid));
887 $query->addExpression('COUNT(n.nid)', 'count');
889 ->condition('status', 1)
890 ->condition('n.created', NODE_NEW_LIMIT, '>')
892 ->addTag('node_access')
898 * Gets all the topics in a forum.
901 * The term ID of the forum.
903 * One of the following integers indicating the sort criteria:
904 * - 1: Date - newest first.
905 * - 2: Date - oldest first.
906 * - 3: Posts with the most comments first.
907 * - 4: Posts with the least comments first.
908 * @param $forum_per_page
909 * The maximum number of topics to display per page.
912 * A list of all the topics in a forum.
914 function forum_get_topics($tid, $sortby, $forum_per_page) {
915 global $user, $forum_topic_list_header;
917 $forum_topic_list_header = array(
919 array('data' => t('Topic'), 'field' => 'f.title'),
920 array('data' => t('Replies'), 'field' => 'f.comment_count'),
921 array('data' => t('Last reply'), 'field' => 'f.last_comment_timestamp'),
924 $order = _forum_get_topic_order($sortby);
925 for ($i = 0; $i < count($forum_topic_list_header); $i++) {
926 if ($forum_topic_list_header[$i]['field'] == $order['field']) {
927 $forum_topic_list_header[$i]['sort'] = $order['sort'];
931 $query = db_select('forum_index', 'f')->extend('PagerDefault')->extend('TableSort');
934 ->condition('f.tid', $tid)
935 ->addTag('node_access')
936 ->orderBy('f.sticky', 'DESC')
937 ->orderByHeader($forum_topic_list_header)
938 ->limit($forum_per_page);
940 $count_query = db_select('forum_index', 'f');
941 $count_query->condition('f.tid', $tid);
942 $count_query->addExpression('COUNT(*)');
943 $count_query->addTag('node_access');
945 $query->setCountQuery($count_query);
946 $result = $query->execute();
948 foreach ($result as $record) {
949 $nids[] = $record->nid;
952 $query = db_select('node', 'n')->extend('TableSort');
953 $query->fields('n', array('title', 'nid', 'type', 'sticky', 'created', 'uid'));
954 $query->addField('n', 'comment', 'comment_mode');
956 $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
957 $query->fields('ncs', array('cid', 'last_comment_uid', 'last_comment_timestamp', 'comment_count'));
959 $query->join('forum_index', 'f', 'f.nid = ncs.nid');
960 $query->addField('f', 'tid', 'forum_tid');
962 $query->join('users', 'u', 'n.uid = u.uid');
963 $query->addField('u', 'name');
965 $query->join('users', 'u2', 'ncs.last_comment_uid = u2.uid');
967 $query->addExpression('CASE ncs.last_comment_uid WHEN 0 THEN ncs.last_comment_name ELSE u2.name END', 'last_comment_name');
970 ->orderBy('f.sticky', 'DESC')
971 ->orderByHeader($forum_topic_list_header)
972 ->condition('n.nid', $nids);
974 $result = $query->execute();
981 $first_new_found = FALSE;
982 foreach ($result as $topic) {
984 // A forum is new if the topic is new, or if there are new comments since
985 // the user's last visit.
986 if ($topic->forum_tid != $tid) {
990 $history = _forum_user_last_visit($topic->nid);
991 $topic->new_replies = comment_num_new($topic->nid, $history);
992 $topic->new = $topic->new_replies || ($topic->last_comment_timestamp > $history);
996 // Do not track "new replies" status for topics if the user is anonymous.
997 $topic->new_replies = 0;
1001 // Make sure only one topic is indicated as the first new topic.
1002 $topic->first_new = FALSE;
1003 if ($topic->new != 0 && !$first_new_found) {
1004 $topic->first_new = TRUE;
1005 $first_new_found = TRUE;
1008 if ($topic->comment_count > 0) {
1009 $last_reply = new stdClass();
1010 $last_reply->created = $topic->last_comment_timestamp;
1011 $last_reply->name = $topic->last_comment_name;
1012 $last_reply->uid = $topic->last_comment_uid;
1013 $topic->last_reply = $last_reply;
1022 * Preprocesses variables for forums.tpl.php.
1025 * An array containing the following elements:
1026 * - forums: An array of all forum objects to display for the given taxonomy
1027 * term ID. If tid = 0 then all the top-level forums are displayed.
1028 * - topics: An array of all the topics in the current forum.
1029 * - parents: An array of taxonomy term objects that are ancestors of the
1031 * - tid: Taxonomy term ID of the current forum.
1032 * - sortby: One of the following integers indicating the sort criteria:
1033 * - 1: Date - newest first.
1034 * - 2: Date - oldest first.
1035 * - 3: Posts with the most comments first.
1036 * - 4: Posts with the least comments first.
1037 * - forum_per_page: The maximum number of topics to display per page.
1039 * @see forums.tpl.php
1041 function template_preprocess_forums(&$variables) {
1044 $vid = variable_get('forum_nav_vocabulary', 0);
1045 $vocabulary = taxonomy_vocabulary_load($vid);
1046 $title = !empty($vocabulary->name) ? $vocabulary->name : '';
1048 // Breadcrumb navigation:
1049 $breadcrumb[] = l(t('Home'), NULL);
1050 if ($variables['tid']) {
1051 $breadcrumb[] = l($vocabulary->name, 'forum');
1053 if ($variables['parents']) {
1054 $variables['parents'] = array_reverse($variables['parents']);
1055 foreach ($variables['parents'] as $p) {
1056 if ($p->tid == $variables['tid']) {
1060 $breadcrumb[] = l($p->name, 'forum/' . $p->tid);
1064 drupal_set_breadcrumb($breadcrumb);
1065 drupal_set_title($title);
1067 if ($variables['forums_defined'] = count($variables['forums']) || count($variables['parents'])) {
1068 if (!empty($variables['forums'])) {
1069 $variables['forums'] = theme('forum_list', $variables);
1072 $variables['forums'] = '';
1075 if ($variables['tid'] && !in_array($variables['tid'], variable_get('forum_containers', array()))) {
1076 $variables['topics'] = theme('forum_topic_list', $variables);
1077 drupal_add_feed('taxonomy/term/' . $variables['tid'] . '/feed', 'RSS - ' . $title);
1080 $variables['topics'] = '';
1083 // Provide separate template suggestions based on what's being output. Topic id is also accounted for.
1084 // Check both variables to be safe then the inverse. Forums with topic ID's take precedence.
1085 if ($variables['forums'] && !$variables['topics']) {
1086 $variables['theme_hook_suggestions'][] = 'forums__containers';
1087 $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
1088 $variables['theme_hook_suggestions'][] = 'forums__containers__' . $variables['tid'];
1090 elseif (!$variables['forums'] && $variables['topics']) {
1091 $variables['theme_hook_suggestions'][] = 'forums__topics';
1092 $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
1093 $variables['theme_hook_suggestions'][] = 'forums__topics__' . $variables['tid'];
1096 $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
1101 drupal_set_title(t('No forums defined'));
1102 $variables['forums'] = '';
1103 $variables['topics'] = '';
1108 * Preprocesses variables for forum-list.tpl.php.
1111 * An array containing the following elements:
1112 * - forums: An array of all forum objects to display for the given taxonomy
1113 * term ID. If tid = 0 then all the top-level forums are displayed.
1114 * - parents: An array of taxonomy term objects that are ancestors of the
1116 * - tid: Taxonomy term ID of the current forum.
1118 * @see forum-list.tpl.php
1119 * @see theme_forum_list()
1121 function template_preprocess_forum_list(&$variables) {
1124 // Sanitize each forum so that the template can safely print the data.
1125 foreach ($variables['forums'] as $id => $forum) {
1126 $variables['forums'][$id]->description = !empty($forum->description) ? filter_xss_admin($forum->description) : '';
1127 $variables['forums'][$id]->link = url("forum/$forum->tid");
1128 $variables['forums'][$id]->name = check_plain($forum->name);
1129 $variables['forums'][$id]->is_container = !empty($forum->container);
1130 $variables['forums'][$id]->zebra = $row % 2 == 0 ? 'odd' : 'even';
1133 $variables['forums'][$id]->new_text = '';
1134 $variables['forums'][$id]->new_url = '';
1135 $variables['forums'][$id]->new_topics = 0;
1136 $variables['forums'][$id]->old_topics = $forum->num_topics;
1137 $variables['forums'][$id]->icon_class = 'default';
1138 $variables['forums'][$id]->icon_title = t('No new posts');
1140 $variables['forums'][$id]->new_topics = _forum_topics_unread($forum->tid, $user->uid);
1141 if ($variables['forums'][$id]->new_topics) {
1142 $variables['forums'][$id]->new_text = format_plural($variables['forums'][$id]->new_topics, '1 new', '@count new');
1143 $variables['forums'][$id]->new_url = url("forum/$forum->tid", array('fragment' => 'new'));
1144 $variables['forums'][$id]->icon_class = 'new';
1145 $variables['forums'][$id]->icon_title = t('New posts');
1147 $variables['forums'][$id]->old_topics = $forum->num_topics - $variables['forums'][$id]->new_topics;
1149 $variables['forums'][$id]->last_reply = theme('forum_submitted', array('topic' => $forum->last_post));
1151 // Give meaning to $tid for themers. $tid actually stands for term id.
1152 $variables['forum_id'] = $variables['tid'];
1153 unset($variables['tid']);
1157 * Preprocesses variables for forum-topic-list.tpl.php.
1160 * An array containing the following elements:
1161 * - tid: Taxonomy term ID of the current forum.
1162 * - topics: An array of all the topics in the current forum.
1163 * - forum_per_page: The maximum number of topics to display per page.
1165 * @see forum-topic-list.tpl.php
1166 * @see theme_forum_topic_list()
1168 function template_preprocess_forum_topic_list(&$variables) {
1169 global $forum_topic_list_header;
1171 // Create the tablesorting header.
1172 $ts = tablesort_init($forum_topic_list_header);
1174 foreach ($forum_topic_list_header as $cell) {
1175 $cell = tablesort_header($cell, $forum_topic_list_header, $ts);
1176 $header .= _theme_table_cell($cell, TRUE);
1178 $variables['header'] = $header;
1180 if (!empty($variables['topics'])) {
1182 foreach ($variables['topics'] as $id => $topic) {
1183 $variables['topics'][$id]->icon = theme('forum_icon', array('new_posts' => $topic->new, 'num_posts' => $topic->comment_count, 'comment_mode' => $topic->comment_mode, 'sticky' => $topic->sticky, 'first_new' => $topic->first_new));
1184 $variables['topics'][$id]->zebra = $row % 2 == 0 ? 'odd' : 'even';
1187 // We keep the actual tid in forum table, if it's different from the
1188 // current tid then it means the topic appears in two forums, one of
1189 // them is a shadow copy.
1190 if ($variables['tid'] != $topic->forum_tid) {
1191 $variables['topics'][$id]->moved = TRUE;
1192 $variables['topics'][$id]->title = check_plain($topic->title);
1193 $variables['topics'][$id]->message = l(t('This topic has been moved'), "forum/$topic->forum_tid");
1196 $variables['topics'][$id]->moved = FALSE;
1197 $variables['topics'][$id]->title = l($topic->title, "node/$topic->nid");
1198 $variables['topics'][$id]->message = '';
1200 $variables['topics'][$id]->created = theme('forum_submitted', array('topic' => $topic));
1201 $variables['topics'][$id]->last_reply = theme('forum_submitted', array('topic' => isset($topic->last_reply) ? $topic->last_reply : NULL));
1203 $variables['topics'][$id]->new_text = '';
1204 $variables['topics'][$id]->new_url = '';
1205 if ($topic->new_replies) {
1206 $variables['topics'][$id]->new_text = format_plural($topic->new_replies, '1 new', '@count new');
1207 $variables['topics'][$id]->new_url = url("node/$topic->nid", array('query' => comment_new_page_count($topic->comment_count, $topic->new_replies, $topic), 'fragment' => 'new'));
1213 // Make this safe for the template.
1214 $variables['topics'] = array();
1216 // Give meaning to $tid for themers. $tid actually stands for term id.
1217 $variables['topic_id'] = $variables['tid'];
1218 unset($variables['tid']);
1220 $variables['pager'] = theme('pager');
1224 * Preprocesses variables for forum-icon.tpl.php.
1227 * An array containing the following elements:
1228 * - new_posts: Indicates whether or not the topic contains new posts.
1229 * - num_posts: The total number of posts in all topics.
1230 * - comment_mode: An integer indicating whether comments are open, closed,
1232 * - sticky: Indicates whether the topic is sticky.
1233 * - first_new: Indicates whether this is the first topic with new posts.
1235 * @see forum-icon.tpl.php
1236 * @see theme_forum_icon()
1238 function template_preprocess_forum_icon(&$variables) {
1239 $variables['hot_threshold'] = variable_get('forum_hot_topic', 15);
1240 if ($variables['num_posts'] > $variables['hot_threshold']) {
1241 $variables['icon_class'] = $variables['new_posts'] ? 'hot-new' : 'hot';
1242 $variables['icon_title'] = $variables['new_posts'] ? t('Hot topic, new comments') : t('Hot topic');
1245 $variables['icon_class'] = $variables['new_posts'] ? 'new' : 'default';
1246 $variables['icon_title'] = $variables['new_posts'] ? t('New comments') : t('Normal topic');
1249 if ($variables['comment_mode'] == COMMENT_NODE_CLOSED || $variables['comment_mode'] == COMMENT_NODE_HIDDEN) {
1250 $variables['icon_class'] = 'closed';
1251 $variables['icon_title'] = t('Closed topic');
1254 if ($variables['sticky'] == 1) {
1255 $variables['icon_class'] = 'sticky';
1256 $variables['icon_title'] = t('Sticky topic');
1261 * Preprocesses variables for forum-submitted.tpl.php.
1263 * The submission information will be displayed in the forum list and topic
1267 * An array containing the following elements:
1268 * - topic: The topic object.
1270 * @see forum-submitted.tpl.php
1271 * @see theme_forum_submitted()
1273 function template_preprocess_forum_submitted(&$variables) {
1274 $variables['author'] = isset($variables['topic']->uid) ? theme('username', array('account' => $variables['topic'])) : '';
1275 $variables['time'] = isset($variables['topic']->created) ? format_interval(REQUEST_TIME - $variables['topic']->created) : '';
1279 * Gets the last time the user viewed a node.
1285 * The timestamp when the user last viewed this node, if the user has
1286 * previously viewed the node; otherwise NODE_NEW_LIMIT.
1288 function _forum_user_last_visit($nid) {
1290 $history = &drupal_static(__FUNCTION__, array());
1292 if (empty($history)) {
1293 $result = db_query('SELECT nid, timestamp FROM {history} WHERE uid = :uid', array(':uid' => $user->uid));
1294 foreach ($result as $t) {
1295 $history[$t->nid] = $t->timestamp > NODE_NEW_LIMIT ? $t->timestamp : NODE_NEW_LIMIT;
1298 return isset($history[$nid]) ? $history[$nid] : NODE_NEW_LIMIT;
1302 * Gets topic sorting information based on an integer code.
1305 * One of the following integers indicating the sort criteria:
1306 * - 1: Date - newest first.
1307 * - 2: Date - oldest first.
1308 * - 3: Posts with the most comments first.
1309 * - 4: Posts with the least comments first.
1312 * An array with the following values:
1313 * - field: A field for an SQL query.
1314 * - sort: 'asc' or 'desc'.
1316 function _forum_get_topic_order($sortby) {
1319 return array('field' => 'f.last_comment_timestamp', 'sort' => 'desc');
1322 return array('field' => 'f.last_comment_timestamp', 'sort' => 'asc');
1325 return array('field' => 'f.comment_count', 'sort' => 'desc');
1328 return array('field' => 'f.comment_count', 'sort' => 'asc');
1334 * Updates the taxonomy index for a given node.
1337 * The ID of the node to update.
1339 function _forum_update_forum_index($nid) {
1340 $count = db_query('SELECT COUNT(cid) FROM {comment} c INNER JOIN {forum_index} i ON c.nid = i.nid WHERE c.nid = :nid AND c.status = :status', array(
1342 ':status' => COMMENT_PUBLISHED,
1347 $last_reply = db_query_range('SELECT cid, name, created, uid FROM {comment} WHERE nid = :nid AND status = :status ORDER BY cid DESC', 0, 1, array(
1349 ':status' => COMMENT_PUBLISHED,
1351 db_update('forum_index')
1353 'comment_count' => $count,
1354 'last_comment_timestamp' => $last_reply->created,
1356 ->condition('nid', $nid)
1360 // Comments do not exist.
1361 $node = db_query('SELECT uid, created FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchObject();
1362 db_update('forum_index')
1364 'comment_count' => 0,
1365 'last_comment_timestamp' => $node->created,
1367 ->condition('nid', $nid)
1373 * Implements hook_rdf_mapping().
1375 function forum_rdf_mapping() {
1379 'bundle' => 'forum',
1381 'rdftype' => array('sioc:Post', 'sioct:BoardPost'),
1382 'taxonomy_forums' => array(
1383 'predicates' => array('sioc:has_container'),
1389 'type' => 'taxonomy_term',
1390 'bundle' => 'forums',
1392 'rdftype' => array('sioc:Container', 'sioc:Forum'),