2 // $Id: forum.module 144 2007-03-28 07:52:20Z thierry $
6 * Enable threaded discussions about general topics.
10 * Implementation of hook_help().
12 function forum_help($section) {
14 case 'admin/help#forum':
15 $output = '<p>'. t('The forum module lets you create threaded discussion forums for a particular topic on your site. This is similar to a message board system such as phpBB. Forums are very useful because they allow community members to discuss topics with one another, and they are archived for future reference.') .'</p>';
16 $output .= '<p>'. t('Forums can be organized under what are called <em>containers</em>. Containers hold forums and, in turn, forums hold threaded discussions. Both containers and forums can be placed inside other containers and forums. By planning the structure of your containers and forums well, you make it easier for users to find a topic area of interest to them. Forum topics can be moved by selecting a different forum and can be left in the existing forum by selecting <em>leave a shadow copy</em>. Forum topics can also have their own URL.') .'</p>';
17 $output .= '<p>'. t('Forums module <strong>requires Taxonomy and Comments module</strong> be enabled.') .'</p>';
18 $output .= t('<p>You can</p>
20 <li>administer forums at <a href="%admin-forum">administer >> forums</a>.</li>
21 <li>enable the required comment and taxonomy modules at <a href="%admin-modules">administer >> modules</a>.</li>
22 <li>read about the comment module at <a href="%admin-help-comment">administer >> help >> comment</a>.</li>
23 <li>read about the taxonomy module at <a href="%admin-help-taxonomy">administer >> help >> taxonomy</a>.</li>
25 ', array('%admin-forum' => url('admin/forum'), '%admin-modules' => url('admin/modules'), '%admin-help-comment' => url('admin/help/comment'), '%admin-help-taxonomy' => url('admin/help/taxonomy')));
26 $output .= '<p>'. t('For more information please read the configuration and customization handbook <a href="%forum">Forum page</a>.', array('%forum' => 'http://drupal.org/handbook/modules/forum/')) .'</p>';
28 case 'admin/modules#description':
29 return t('Enables threaded discussions about general topics.');
31 return t('<p>This is a list of existing containers and forums that you can edit. Containers hold forums and, in turn, forums hold threaded discussions. Both containers and forums can be placed inside other containers and forums. By planning the structure of your containers and forums well, you make it easier for users to find a topic area of interest to them.</p>');
32 case 'admin/forum/add/container':
33 return t('<p>Containers help you organize your forums. The job of a container is to hold, or contain, other forums that are related. For example, a container named "Food" might hold two forums named "Fruit" and "Vegetables".</p>');
34 case 'admin/forum/add/forum':
35 return t('<p>A forum holds discussion topics that are related. For example, a forum named "Fruit" might contain topics titled "Apples" and "Bananas".</p>');
36 case 'admin/forum/configure':
37 return t('This is where you can configure system-wide options for how your forums act and display.');
38 case 'node/add#forum':
39 return t('Create a new topic for discussion in the forums.');
44 * Implementation of hook_menu().
46 function forum_menu($may_cache) {
50 $items[] = array('path' => 'node/add/forum',
51 'title' => t('forum topic'),
52 'access' => user_access('create forum topics'));
53 $items[] = array('path' => 'forum',
54 'title' => t('forums'),
55 'callback' => 'forum_page',
56 'access' => user_access('access content'),
57 'type' => MENU_SUGGESTED_ITEM);
58 $items[] = array('path' => 'admin/forum',
59 'title' => t('forums'),
60 'callback' => 'forum_overview',
61 'access' => user_access('administer forums'),
62 'type' => MENU_NORMAL_ITEM);
63 $items[] = array('path' => 'admin/forum/list',
65 'access' => user_access('administer forums'),
66 'type' => MENU_DEFAULT_LOCAL_TASK,
68 $items[] = array('path' => 'admin/forum/add/container',
69 'title' => t('add container'),
70 'callback' => 'forum_form_container',
71 'access' => user_access('administer forums'),
72 'type' => MENU_LOCAL_TASK);
73 $items[] = array('path' => 'admin/forum/add/forum',
74 'title' => t('add forum'),
75 'callback' => 'forum_form_forum',
76 'access' => user_access('administer forums'),
77 'type' => MENU_LOCAL_TASK);
78 $items[] = array('path' => 'admin/forum/configure',
79 'title' => t('configure'),
80 'callback' => 'forum_admin_configure',
81 'access' => user_access('administer forums'),
82 'type' => MENU_LOCAL_TASK);
84 elseif (is_numeric(arg(4))) {
85 $term = taxonomy_get_term(arg(4));
86 // Check if this is a valid term.
88 $items[] = array('path' => 'admin/forum/edit/container',
89 'title' => t('edit container'),
90 'callback' => 'forum_form_container',
91 'callback arguments' => array((array)$term),
92 'access' => user_access('administer forums'),
93 'type' => MENU_CALLBACK);
94 $items[] = array('path' => 'admin/forum/edit/forum',
95 'title' => t('edit forum'),
96 'callback' => 'forum_form_forum',
97 'callback arguments' => array((array)$term),
98 'access' => user_access('administer forums'),
99 'type' => MENU_CALLBACK);
107 * Implementation of hook_node_info().
109 function forum_node_info() {
110 return array('forum' => array('name' => t('forum topic'), 'base' => 'forum'));
114 * Implementation of hook_access().
116 function forum_access($op, $node) {
119 if ($op == 'create') {
120 return user_access('create forum topics');
123 if ($op == 'update' || $op == 'delete') {
124 if (user_access('edit own forum topics') && ($user->uid == $node->uid)) {
131 * Implementation of hook_perm().
133 function forum_perm() {
134 return array('create forum topics', 'edit own forum topics', 'administer forums');
138 * Implementation of hook_nodeapi().
140 function forum_nodeapi(&$node, $op, $teaser, $page) {
142 case 'delete revision':
143 db_query('DELETE FROM {forum} WHERE vid = %d', $node->vid);
149 * Implementation of hook_taxonomy().
151 function forum_taxonomy($op, $type, $term = NULL) {
152 if ($op == 'delete' && $term['vid'] == _forum_get_vid()) {
155 $results = db_query('SELECT f.nid FROM {forum} f WHERE f.tid = %d', $term['tid']);
156 while ($node = db_fetch_object($results)) {
157 // node_delete will also remove any association with non-forum vocabularies.
158 node_delete($node->nid);
161 // For containers, remove the tid from the forum_containers variable.
162 $containers = variable_get('forum_containers', array());
163 $key = array_search($term['tid'], $containers);
164 if ($key !== FALSE) {
165 unset($containers[$key]);
167 variable_set('forum_containers', $containers);
170 variable_del('forum_nav_vocabulary');
176 * Implementation of hook_settings
178 function forum_admin_configure() {
181 $number = drupal_map_assoc(array(5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 80, 100, 150, 200, 250, 300, 350, 400, 500));
182 $form['forum_hot_topic'] = array('#type' => 'select',
183 '#title' => t('Hot topic threshold'),
184 '#default_value' => variable_get('forum_hot_topic', 15),
185 '#options' => $number,
186 '#description' => t('The number of posts a topic must have to be considered hot.'),
188 $number = drupal_map_assoc(array(10, 25, 50, 75, 100));
189 $form['forum_per_page'] = array('#type' => 'select',
190 '#title' => t('Topics per page'),
191 '#default_value' => variable_get('forum_per_page', 25),
192 '#options' => $number,
193 '#description' => t('The default number of topics displayed per page; links to browse older messages are automatically being displayed.'),
195 $forder = array(1 => t('Date - newest first'), 2 => t('Date - oldest first'), 3 => t('Posts - most active first'), 4=> t('Posts - least active first'));
196 $form['forum_order'] = array('#type' => 'radios',
197 '#title' => t('Default order'),
198 '#default_value' => variable_get('forum_order', '1'),
199 '#options' => $forder,
200 '#description' => t('The default display order for topics.'),
203 return system_settings_form('forum_admin_configure', $form);
207 * Implementation of hook_form_alter().
209 function forum_form_alter($form_id, &$form) {
210 // hide critical options from forum vocabulary
211 if ($form_id == 'taxonomy_form_vocabulary') {
212 if ($form['vid']['#value'] == _forum_get_vid()) {
213 $form['help_forum_vocab'] = array(
214 '#value' => t('This is the designated forum vocabulary. Some of the normal vocabulary options have been removed.'),
217 $form['nodes']['forum'] = array('#type' => 'checkbox', '#value' => 1, '#title' => t('forum topic'), '#attributes' => array('disabled' => '' ), '#description' => t('forum topic is affixed to the forum vocabulary.'));
218 $form['hierarchy'] = array('#type' => 'value', '#value' => 1);
219 unset($form['relations']);
220 unset($form['tags']);
221 unset($form['multiple']);
222 $form['required'] = array('#type' => 'value', '#value' => 1);
225 unset($form['nodes']['forum']);
231 * Implementation of hook_load().
233 function forum_load($node) {
234 $forum = db_fetch_object(db_query('SELECT * FROM {forum} WHERE vid = %d', $node->vid));
240 * Implementation of hook_block().
242 * Generates a block containing the currently active forum topics and the
243 * most recently added forum topics.
245 function forum_block($op = 'list', $delta = 0, $edit = array()) {
248 $blocks[0]['info'] = t('Active forum topics');
249 $blocks[1]['info'] = t('New forum topics');
253 $form['forum_block_num_'. $delta] = array('#type' => 'select', '#title' => t('Number of topics'), '#default_value' => variable_get('forum_block_num_'. $delta, '5'), '#options' => drupal_map_assoc(array(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)));
257 variable_set('forum_block_num_'. $delta, $edit['forum_block_num_'. $delta]);
261 if (user_access('access content')) {
264 $title = t('Active forum topics');
265 $sql = db_rewrite_sql("SELECT n.nid, n.title, l.comment_count FROM {node} n INNER JOIN {node_comment_statistics} l ON n.nid = l.nid WHERE n.status = 1 AND n.type = 'forum' ORDER BY l.last_comment_timestamp DESC");
266 $result = db_query_range($sql, 0, variable_get('forum_block_num_0', '5'));
267 if (db_num_rows($result)) {
268 $content = node_title_list($result);
273 $title = t('New forum topics');
274 $sql = db_rewrite_sql("SELECT n.nid, n.title, l.comment_count FROM {node} n INNER JOIN {node_comment_statistics} l ON n.nid = l.nid WHERE n.type = 'forum' AND n.status = 1 ORDER BY n.nid DESC");
275 $result = db_query_range($sql, 0, variable_get('forum_block_num_1', '5'));
276 if (db_num_rows($result)) {
277 $content = node_title_list($result);
283 $content .= '<div class="more-link">'. l(t('more'), 'forum', array('title' => t('Read the latest forum topics.'))) .'</div>';
286 $block['subject'] = $title;
287 $block['content'] = $content;
295 * Implementation of hook_view().
297 function forum_view(&$node, $teaser = FALSE, $page = FALSE) {
299 $vocabulary = taxonomy_get_vocabulary(variable_get('forum_nav_vocabulary', ''));
300 // Breadcrumb navigation
301 $breadcrumb = array();
302 $breadcrumb[] = array('path' => 'forum', 'title' => $vocabulary->name);
303 if ($parents = taxonomy_get_parents_all($node->tid)) {
304 $parents = array_reverse($parents);
305 foreach ($parents as $p) {
306 $breadcrumb[] = array('path' => 'forum/'. $p->tid, 'title' => $p->name);
309 $breadcrumb[] = array('path' => 'node/'. $node->nid);
310 menu_set_location($breadcrumb);
313 $node = node_prepare($node, $teaser);
315 $node->body .= theme('forum_topic_navigation', $node);
319 * Implementation of hook_submit().
321 * Check in particular that only a "leaf" term in the associated taxonomy
322 * vocabulary is selected, not a "container" term.
324 function forum_submit(&$node) {
325 // Make sure all fields are set properly:
326 $node->icon = $node->icon ? $node->icon : '';
328 if ($node->taxonomy) {
329 // Extract the node's proper topic ID.
330 $vocabulary = variable_get('forum_nav_vocabulary', '');
331 foreach ($node->taxonomy as $term) {
332 if (db_result(db_query('SELECT COUNT(*) FROM {term_data} WHERE tid = %d AND vid = %d', $term, $vocabulary))) {
336 if ($node->tid && $node->shadow) {
337 // A shadow copy needs to be created. Retain existing term and add new term.
338 $terms = array_keys(taxonomy_node_get_terms($node->nid));
339 if (!in_array($node->tid, $terms)) {
340 $terms[] = $node->tid;
342 $node->taxonomy = $terms;
348 * Implementation of hook_validate().
350 * Check in particular that only a "leaf" term in the associated taxonomy
351 * vocabulary is selected, not a "container" term.
353 function forum_validate($node) {
354 if ($node->taxonomy) {
355 // Extract the node's proper topic ID.
356 $vocabulary = variable_get('forum_nav_vocabulary', '');
357 $containers = variable_get('forum_containers', array());
358 foreach ($node->taxonomy as $term) {
359 if (db_result(db_query('SELECT COUNT(*) FROM {term_data} WHERE tid = %d AND vid = %d', $term, $vocabulary))) {
360 if (in_array($term, $containers)) {
361 $term = taxonomy_get_term($term);
362 form_set_error('taxonomy', t('The item %forum is only a container for forums. Please select one of the forums below it.', array('%forum' => theme('placeholder', $term->name))));
370 * Implementation of hook_update().
372 function forum_update($node) {
373 if ($node->revision) {
374 db_query("INSERT INTO {forum} (nid, vid, tid) VALUES (%d, %d, %d)", $node->nid, $node->vid, $node->tid);
377 db_query('UPDATE {forum} SET tid = %d WHERE vid = %d', $node->tid, $node->vid);
382 * Implementation of hook_form().
384 function forum_form(&$node) {
385 $form['title'] = array('#type' => 'textfield', '#title' => t('Subject'), '#default_value' => $node->title, '#required' => TRUE, '#weight' => -5);
388 $forum_terms = taxonomy_node_get_terms_by_vocabulary(_forum_get_vid(), $node->nid);
389 // if editing, give option to leave shadows
390 $shadow = (count($forum_terms) > 1);
391 $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.'));
394 $form['body_filter']['body'] = array('#type' => 'textarea', '#title' => t('Body'), '#default_value' => $node->body, '#rows' => 20, '#required' => TRUE);
395 $form['body_filter']['format'] = filter_form($node->format);
401 * Implementation of hook_prepare; assign forum taxonomy when adding a topic from within a forum.
403 function forum_prepare(&$node) {
406 $node->taxonomy[arg(3)]->vid = _forum_get_vid();
407 $node->taxonomy[arg(3)]->tid = arg(3);
412 * Implementation of hook_insert().
414 function forum_insert($node) {
415 db_query('INSERT INTO {forum} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $node->tid);
419 * Implementation of hook_delete().
421 function forum_delete(&$node) {
422 db_query('DELETE FROM {forum} WHERE nid = %d', $node->nid);
426 * Returns a form for adding a container to the forum vocabulary
428 * @param $edit Associative array containing a container term to be added or edited.
430 function forum_form_container($edit = array()) {
431 // Handle a delete operation.
432 if ($_POST['op'] == t('Delete') || $_POST['edit']['confirm']) {
433 return _forum_confirm_delete($edit['tid']);
436 $form['name'] = array(
437 '#title' => t('Container name'),
438 '#type' => 'textfield',
439 '#default_value' => $edit['name'],
441 '#description' => t('The container name is used to identify related forums.'),
445 $form['description'] = array(
446 '#type' => 'textarea',
447 '#title' => t('Description'),
448 '#default_value' => $edit['description'],
449 '#description' => t('The container description can give users more information about the forums it contains.')
451 $form['parent']['#tree'] = TRUE;
452 $form['parent'][0] = _forum_parent_select($edit['tid'], t('Parent'), 'container');
453 $form['weight'] = array('#type' => 'weight',
454 '#title' => t('Weight'),
455 '#default_value' => $edit['weight'],
456 '#description' => t('When listing containers, those with with light (small) weights get listed before containers with heavier (larger) weights. Containers with equal weights are sorted alphabetically.')
459 $form['vid'] = array('#type' => 'hidden',
460 '#value' => _forum_get_vid());
461 $form['submit'] = array(
463 '#value' => t('Submit')
466 $form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
467 $form['tid'] = array('#type' => 'value', '#value' => $edit['tid']);
470 return drupal_get_form('forum_form_container', $form, 'forum_form');
474 * Returns a form for adding a forum to the forum vocabulary
476 * @param $edit Associative array containing a forum term to be added or edited.
478 function forum_form_forum($edit = array()) {
479 // Handle a delete operation.
480 if ($_POST['op'] == t('Delete') || $_POST['edit']['confirm']) {
481 return _forum_confirm_delete($edit['tid']);
484 $form['name'] = array('#type' => 'textfield',
485 '#title' => t('Forum name'),
486 '#default_value' => $edit['name'],
488 '#description' => t('The forum name is used to identify related discussions.'),
491 $form['description'] = array('#type' => 'textarea',
492 '#title' => t('Description'),
493 '#default_value' => $edit['description'],
494 '#description' => t('The forum description can give users more information about the discussion topics it contains.'),
496 $form['parent']['#tree'] = TRUE;
497 $form['parent'][0] = _forum_parent_select($edit['tid'], t('Parent'), 'forum');
498 $form['weight'] = array('#type' => 'weight',
499 '#title' => t('Weight'),
500 '#default_value' => $edit['weight'],
501 '#description' => t('When listing forums, those with lighter (smaller) weights get listed before containers with heavier (larger) weights. Forums with equal weights are sorted alphabetically.'),
504 $form['vid'] = array('#type' => 'hidden', '#value' => _forum_get_vid());
505 $form['submit' ] = array('#type' => 'submit', '#value' => t('Submit'));
507 $form['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
508 $form['tid'] = array('#type' => 'hidden', '#value' => $edit['tid']);
511 return drupal_get_form('forum_form_forum', $form, 'forum_form');
515 * Process forum form and container form submissions.
517 function forum_form_submit($form_id, $form_values) {
518 if ($form_id == 'forum_form_container') {
520 $type = t('forum container');
527 $status = taxonomy_save_term($form_values);
531 $containers = variable_get('forum_containers', array());
532 $containers[] = $form_values['tid'];
533 variable_set('forum_containers', $containers);
535 drupal_set_message(t('Created new %type %term.', array('%term' => theme('placeholder', $form_values['name']), '%type' => $type)));
538 drupal_set_message(t('The %type %term has been updated.', array('%term' => theme('placeholder', $form_values['name']), '%type' => $type)));
541 return 'admin/forum';
545 * Returns a confirmation page for deleting a forum taxonomy term.
547 * @param $tid ID of the term to be deleted
549 function _forum_confirm_delete($tid) {
550 $term = taxonomy_get_term($tid);
552 $form['tid'] = array('#type' => 'value', '#value' => $tid);
553 $form['name'] = array('#type' => 'value', '#value' => $term->name);
555 return confirm_form('forum_confirm_delete', $form, t('Are you sure you want to delete the forum %name?', array('%name' => theme('placeholder', $term->name))), 'admin/forums', t('Deleting a forum or container will delete all sub-forums and associated posts as well. This action cannot be undone.'), t('Delete'), t('Cancel'));
559 * Implementation of forms api _submit call. Deletes a forum after confirmation.
561 function forum_confirm_delete_submit($form_id, $form_values) {
562 taxonomy_del_term($form_values['tid']);
563 drupal_set_message(t('The forum %term and all sub-forums and associated posts have been deleted.', array('%term' => theme('placeholder', $form_values['name']))));
564 watchdog('content', t('forum: deleted %term and all its sub-forums and associated posts.', array('%term' => theme('placeholder', $form_values['name']))));
566 return 'admin/forum';
570 * Returns an overview list of existing forums and containers
572 function forum_overview() {
573 $header = array(t('Name'), t('Operations'));
575 $tree = taxonomy_get_tree(_forum_get_vid());
577 foreach ($tree as $term) {
578 if (in_array($term->tid, variable_get('forum_containers', array()))) {
579 $rows[] = array(_taxonomy_depth($term->depth) .' '. check_plain($term->name), l(t('edit container'), "admin/forum/edit/container/$term->tid"));
582 $rows[] = array(_taxonomy_depth($term->depth) .' '. check_plain($term->name), l(t('edit forum'), "admin/forum/edit/forum/$term->tid"));
588 $rows[] = array(array('data' => '<em>' . t('There are no existing containers or forums. You may add some on the <a href="%container">add container</a> or <a href="%forum">add forum</a> pages.', array('%container' => url('admin/forum/add/container'), '%forum' => url('admin/forum/add/forum'))) . '</em>', 'colspan' => 2));
590 return theme('table', $header, $rows);
594 * Returns a select box for available parent terms
596 * @param $tid ID of the term which is being added or edited
597 * @param $title Title to display the select box with
598 * @param $child_type Whether the child is forum or container
600 function _forum_parent_select($tid, $title, $child_type) {
602 $parents = taxonomy_get_parents($tid);
604 $parent = array_shift($parents);
605 $parent = $parent->tid;
611 $children = taxonomy_get_tree(_forum_get_vid(), $tid);
613 // A term can't be the child of itself, nor of its children.
614 foreach ($children as $child) {
615 $exclude[] = $child->tid;
619 $tree = taxonomy_get_tree(_forum_get_vid());
620 $options[0] = '<'. t('root') .'>';
622 foreach ($tree as $term) {
623 if (!in_array($term->tid, $exclude)) {
624 $options[$term->tid] = _taxonomy_depth($term->depth) . $term->name;
628 if ($child_type == 'container') {
629 $description = t('Containers are usually placed at the top (root) level of your forum but you can also place a container inside a parent container or forum.');
631 else if ($child_type == 'forum') {
632 $description = t('You may place your forum inside a parent container or forum, or at the top (root) level of your forum.');
635 return array('#type' => 'select', '#title' => $title, '#default_value' => $parent, '#options' => $options, '#description' => $description, '#required' => TRUE);
638 function forum_term_path($term) {
639 return 'forum/'. $term->tid;
643 * Returns the vocabulary id for forum navigation.
645 function _forum_get_vid() {
646 $vid = variable_get('forum_nav_vocabulary', '');
648 // Check to see if a forum vocabulary exists
649 $vid = db_result(db_query("SELECT vid FROM {vocabulary} WHERE module = '%s'", 'forum'));
651 $edit = array('name' => 'Forums', 'multiple' => 0, 'required' => 1, 'hierarchy' => 1, 'relations' => 0, 'module' => 'forum', 'nodes' => array('forum' => 1));
652 taxonomy_save_vocabulary($edit);
655 variable_set('forum_nav_vocabulary', $vid);
662 * Formats a topic for display
664 * @TODO Give a better description. Not sure where this function is used yet.
666 function _forum_format($topic) {
667 if ($topic && $topic->timestamp) {
668 return t('%time ago<br />by %author', array('%time' => format_interval(time() - $topic->timestamp), '%author' => theme('username', $topic)));
676 * Returns a list of all forums for a given taxonomy id
678 * Forum objects contain the following fields
679 * -num_topics Number of topics in the forum
680 * -num_posts Total number of posts in all topics
681 * -last_post Most recent post for the forum
684 * Taxonomy ID of the vocabulary that holds the forum list.
686 * Array of object containing the forum information.
688 function forum_get_forums($tid = 0) {
691 $_forums = taxonomy_get_tree(variable_get('forum_nav_vocabulary', ''), $tid);
693 if (count($_forums)) {
697 $sql = "SELECT r.tid, COUNT(n.nid) AS topic_count, SUM(l.comment_count) AS comment_count FROM {node} n INNER JOIN {node_comment_statistics} l ON n.nid = l.nid INNER JOIN {term_node} r ON n.nid = r.nid WHERE n.status = 1 AND n.type = 'forum' GROUP BY r.tid";
698 $sql = db_rewrite_sql($sql);
699 $_counts = db_query($sql, $forum->tid);
700 while ($count = db_fetch_object($_counts)) {
701 $counts[$count->tid] = $count;
705 foreach ($_forums as $forum) {
706 if (in_array($forum->tid, variable_get('forum_containers', array()))) {
707 $forum->container = 1;
710 if ($counts[$forum->tid]) {
711 $forum->num_topics = $counts[$forum->tid]->topic_count;
712 $forum->num_posts = $counts[$forum->tid]->topic_count + $counts[$forum->tid]->comment_count;
715 $forum->num_topics = 0;
716 $forum->num_posts = 0;
719 // This query does not use full ANSI syntax since MySQL 3.x does not support
720 // table1 INNER JOIN table2 INNER JOIN table3 ON table2_criteria ON table3_criteria
721 // used to join node_comment_statistics to users.
722 $sql = "SELECT ncs.last_comment_timestamp, IF (ncs.last_comment_uid != 0, u2.name, ncs.last_comment_name) AS last_comment_name, ncs.last_comment_uid FROM {node} n INNER JOIN {users} u1 ON n.uid = u1.uid INNER JOIN {term_node} tn ON n.nid = tn.nid INNER JOIN {node_comment_statistics} ncs ON n.nid = ncs.nid INNER JOIN {users} u2 ON ncs.last_comment_uid=u2.uid WHERE n.status = 1 AND tn.tid = %d ORDER BY ncs.last_comment_timestamp DESC";
723 $sql = db_rewrite_sql($sql);
724 $topic = db_fetch_object(db_query_range($sql, $forum->tid, 0, 1));
726 $last_post = new StdClass();
727 $last_post->timestamp = $topic->last_comment_timestamp;
728 $last_post->name = $topic->last_comment_name;
729 $last_post->uid = $topic->last_comment_uid;
730 $forum->last_post = $last_post;
732 $forums[$forum->tid] = $forum;
739 * Calculate the number of nodes the user has not yet read and are newer
740 * than NODE_NEW_LIMIT.
742 function _forum_topics_unread($term, $uid) {
743 $sql = "SELECT COUNT(n.nid) FROM {node} n INNER JOIN {term_node} tn ON n.nid = tn.nid AND tn.tid = %d LEFT JOIN {history} h ON n.nid = h.nid AND h.uid = %d WHERE n.status = 1 AND n.type = 'forum' AND n.created > %d AND h.nid IS NULL";
744 $sql = db_rewrite_sql($sql);
745 return db_result(db_query($sql, $term, $uid, NODE_NEW_LIMIT));
748 function forum_get_topics($tid, $sortby, $forum_per_page) {
749 global $user, $forum_topic_list_header;
751 $forum_topic_list_header = array(
752 array('data' => ' '),
753 array('data' => t('Topic'), 'field' => 'n.title'),
754 array('data' => t('Replies'), 'field' => 'l.comment_count'),
755 array('data' => t('Created'), 'field' => 'n.created'),
756 array('data' => t('Last reply'), 'field' => 'l.last_comment_timestamp'),
759 $order = _forum_get_topic_order($sortby);
760 for ($i = 0; $i < count($forum_topic_list_header); $i++) {
761 if ($forum_topic_list_header[$i]['field'] == $order['field']) {
762 $forum_topic_list_header[$i]['sort'] = $order['sort'];
766 $term = taxonomy_get_term($tid);
768 $sql = db_rewrite_sql("SELECT n.nid, f.tid, n.title, n.sticky, u.name, u.uid, n.created AS timestamp, n.comment AS comment_mode, l.last_comment_timestamp, IF(l.last_comment_uid != 0, cu.name, l.last_comment_name) AS last_comment_name, l.last_comment_uid, l.comment_count AS num_comments FROM {node_comment_statistics} l, {users} cu, {term_node} r, {users} u, {forum} f, {node} n WHERE n.status = 1 AND l.last_comment_uid = cu.uid AND n.nid = l.nid AND n.nid = r.nid AND r.tid = %d AND n.uid = u.uid AND n.vid = f.vid");
769 $sql .= tablesort_sql($forum_topic_list_header, 'n.sticky DESC,');
770 $sql .= ', n.created DESC'; // Always add a secondary sort order so that the news forum topics are on top.
772 $sql_count = db_rewrite_sql("SELECT COUNT(n.nid) FROM {node} n INNER JOIN {term_node} r ON n.nid = r.nid AND r.tid = %d WHERE n.status = 1 AND n.type = 'forum'");
774 $result = pager_query($sql, $forum_per_page, 0, $sql_count, $tid);
776 while ($topic = db_fetch_object($result)) {
778 // folder is new if topic is new or there are new comments since last visit
779 if ($topic->tid != $tid) {
783 $history = _forum_user_last_visit($topic->nid);
784 $topic->new_replies = comment_num_new($topic->nid, $history);
785 $topic->new = $topic->new_replies || ($topic->timestamp > $history);
789 // Do not track "new replies" status for topics if the user is anonymous.
790 $topic->new_replies = 0;
794 if ($topic->num_comments > 0) {
795 $last_reply = new StdClass();
796 $last_reply->timestamp = $topic->last_comment_timestamp;
797 $last_reply->name = $topic->last_comment_name;
798 $last_reply->uid = $topic->last_comment_uid;
799 $topic->last_reply = $last_reply;
808 * Finds the first unread node for a given forum.
810 function _forum_new($tid) {
813 $sql = "SELECT n.nid FROM {node} n LEFT JOIN {history} h ON n.nid = h.nid AND h.uid = %d INNER JOIN {term_node} r ON n.nid = r.nid AND r.tid = %d WHERE n.status = 1 AND n.type = 'forum' AND h.nid IS NULL AND n.created > %d ORDER BY created";
814 $sql = db_rewrite_sql($sql);
815 $nid = db_result(db_query_range($sql, $user->uid, $tid, NODE_NEW_LIMIT, 0, 1));
817 return $nid ? $nid : 0;
821 * Menu callback; prints a forum listing.
823 function forum_page($tid = 0) {
824 if (module_exist('taxonomy') && module_exist('comment')) {
825 $forum_per_page = variable_get('forum_per_page', 25);
826 $sortby = variable_get('forum_order', 1);
828 $forums = forum_get_forums($tid);
829 $parents = taxonomy_get_parents_all($tid);
830 if ($tid && !in_array($tid, variable_get('forum_containers', array()))) {
831 $topics = forum_get_topics($tid, $sortby, $forum_per_page);
834 return theme('forum_display', $forums, $topics, $parents, $tid, $sortby, $forum_per_page);
837 drupal_set_message(t('The forum module requires both the taxonomy module and the comment module to be enabled and configured.'), 'error');
843 * Format the forum body.
847 function theme_forum_display($forums, $topics, $parents, $tid, $sortby, $forum_per_page) {
849 // forum list, topics list, topic browser and 'add new topic' link
851 $vocabulary = taxonomy_get_vocabulary(variable_get('forum_nav_vocabulary', ''));
852 $title = $vocabulary->name;
854 // Breadcrumb navigation:
855 $breadcrumb = array();
857 $breadcrumb[] = array('path' => 'forum', 'title' => $title);
861 $parents = array_reverse($parents);
862 foreach ($parents as $p) {
863 if ($p->tid == $tid) {
867 $breadcrumb[] = array('path' => 'forum/'. $p->tid, 'title' => $p->name);
872 drupal_set_title($title);
874 $breadcrumb[] = array('path' => $_GET['q']);
875 menu_set_location($breadcrumb);
877 if (count($forums) || count($parents)) {
878 $output = '<div id="forum">';
881 if (module_exist('tracker')) {
883 $output .= ' <li>'. l(t('My discussions.'), "tracker/$user->uid") .'</li>';
886 $output .= ' <li>'. l(t('Active discussions.'), 'tracker') .'</li>';
889 if (user_access('create forum topics')) {
890 $output .= '<li>'. l(t('Post new forum topic.'), "node/add/forum/$tid") .'</li>';
892 else if ($user->uid) {
893 $output .= '<li>'. t('You are not allowed to post a new forum topic.') .'</li>';
896 $output .= '<li>'. t('<a href="%login">Login</a> to post a new forum topic.', array('%login' => url('user/login'))) .'</li>';
900 $output .= theme('forum_list', $forums, $parents, $tid);
902 if ($tid && !in_array($tid, variable_get('forum_containers', array()))) {
903 drupal_add_link(array('rel' => 'alternate',
904 'type' => 'application/rss+xml',
905 'title' => 'RSS - '. $title,
906 'href' => url('taxonomy/term/'. $tid .'/0/feed')));
908 $output .= theme('forum_topic_list', $tid, $topics, $sortby, $forum_per_page);
909 $output .= theme('feed_icon', url("taxonomy/term/$tid/0/feed"));
914 drupal_set_title(t('No forums defined'));
922 * Format the forum listing.
926 function theme_forum_list($forums, $parents, $tid) {
931 $header = array(t('Forum'), t('Topics'), t('Posts'), t('Last post'));
933 foreach ($forums as $forum) {
934 if ($forum->container) {
935 $description = '<div style="margin-left: '. ($forum->depth * 30) ."px;\">\n";
936 $description .= ' <div class="name">'. l($forum->name, "forum/$forum->tid") ."</div>\n";
938 if ($forum->description) {
939 $description .= ' <div class="description">'. filter_xss_admin($forum->description) ."</div>\n";
941 $description .= "</div>\n";
943 $rows[] = array(array('data' => $description, 'class' => 'container', 'colspan' => '4'));
946 $new_topics = _forum_topics_unread($forum->tid, $user->uid);
947 $forum->old_topics = $forum->num_topics - $new_topics;
952 $description = '<div style="margin-left: '. ($forum->depth * 30) ."px;\">\n";
953 $description .= ' <div class="name">'. l($forum->name, "forum/$forum->tid") ."</div>\n";
955 if ($forum->description) {
956 $description .= ' <div class="description">'. filter_xss_admin($forum->description) ."</div>\n";
958 $description .= "</div>\n";
961 array('data' => $description, 'class' => 'forum'),
962 array('data' => $forum->num_topics . ($new_topics ? '<br />'. l(format_plural($new_topics, '1 new', '%count new'), "forum/$forum->tid", NULL, NULL, 'new') : ''), 'class' => 'topics'),
963 array('data' => $forum->num_posts, 'class' => 'posts'),
964 array('data' => _forum_format($forum->last_post), 'class' => 'last-reply'));
968 return theme('table', $header, $rows);
975 * Format the topic listing.
979 function theme_forum_topic_list($tid, $topics, $sortby, $forum_per_page) {
980 global $forum_topic_list_header;
984 foreach ($topics as $topic) {
985 // folder is new if topic is new or there are new comments since last visit
986 if ($topic->tid != $tid) {
988 array('data' => theme('forum_icon', $topic->new, $topic->num_comments, $topic->comment_mode, $topic->sticky), 'class' => 'icon'),
989 array('data' => check_plain($topic->title), 'class' => 'title'),
990 array('data' => l(t('This topic has been moved'), "forum/$topic->tid"), 'colspan' => '3')
995 array('data' => theme('forum_icon', $topic->new, $topic->num_comments, $topic->comment_mode, $topic->sticky), 'class' => 'icon'),
996 array('data' => l($topic->title, "node/$topic->nid"), 'class' => 'topic'),
997 array('data' => $topic->num_comments . ($topic->new_replies ? '<br />'. l(format_plural($topic->new_replies, '1 new', '%count new'), "node/$topic->nid", NULL, NULL, 'new') : ''), 'class' => 'replies'),
998 array('data' => _forum_format($topic), 'class' => 'created'),
999 array('data' => _forum_format($topic->last_reply), 'class' => 'last-reply')
1005 $output .= theme('table', $forum_topic_list_header, $rows);
1006 $output .= theme('pager', NULL, $forum_per_page, 0);
1012 * Format the icon for each individual topic.
1014 * @ingroup themeable
1016 function theme_forum_icon($new_posts, $num_posts = 0, $comment_mode = 0, $sticky = 0) {
1018 if ($num_posts > variable_get('forum_hot_topic', 15)) {
1019 $icon = $new_posts ? 'hot-new' : 'hot';
1022 $icon = $new_posts ? 'new' : 'default';
1025 if ($comment_mode == COMMENT_NODE_READ_ONLY || $comment_mode == COMMENT_NODE_DISABLED) {
1033 $output = theme('image', "misc/forum-$icon.png");
1036 $output = "<a name=\"new\">$output</a>";
1043 * Format the next/previous forum topic navigation links.
1045 * @ingroup themeable
1047 function theme_forum_topic_navigation($node) {
1050 // get previous and next topic
1051 $sql = "SELECT n.nid, n.title, n.sticky, l.comment_count, l.last_comment_timestamp FROM {node} n INNER JOIN {node_comment_statistics} l ON n.nid = l.nid INNER JOIN {term_node} r ON n.nid = r.nid AND r.tid = %d WHERE n.status = 1 AND n.type = 'forum' ORDER BY n.sticky DESC, ". _forum_get_topic_order_sql(variable_get('forum_order', 1));
1052 $result = db_query(db_rewrite_sql($sql), $node->tid);
1054 while ($topic = db_fetch_object($result)) {
1056 $next = new StdClass();
1057 $next->nid = $topic->nid;
1058 $next->title = $topic->title;
1061 if ($topic->nid == $node->nid) {
1065 $prev = new StdClass();
1066 $prev->nid = $topic->nid;
1067 $prev->title = $topic->title;
1071 if ($prev || $next) {
1072 $output .= '<div class="forum-topic-navigation">';
1075 $output .= l(t('‹ ') . $prev->title, 'node/'. $prev->nid, array('class' => 'topic-previous', 'title' => t('Go to previous forum topic')));
1077 if ($prev && $next) {
1078 // Word break (a is an inline element)
1082 $output .= l($next->title . t(' ›'), 'node/'. $next->nid, array('class' => 'topic-next', 'title' => t('Go to next forum topic')));
1085 $output .= '</div>';
1091 function _forum_user_last_visit($nid) {
1093 static $history = array();
1095 if (empty($history)) {
1096 $result = db_query('SELECT nid, timestamp FROM {history} WHERE uid = %d', $user->uid);
1097 while ($t = db_fetch_object($result)) {
1098 $history[$t->nid] = $t->timestamp > NODE_NEW_LIMIT ? $t->timestamp : NODE_NEW_LIMIT;
1101 return $history[$nid] ? $history[$nid] : NODE_NEW_LIMIT;
1104 function _forum_get_topic_order($sortby) {
1107 return array('field' => 'l.last_comment_timestamp', 'sort' => 'desc');
1110 return array('field' => 'l.last_comment_timestamp', 'sort' => 'asc');
1113 return array('field' => 'l.comment_count', 'sort' => 'desc');
1116 return array('field' => 'l.comment_count', 'sort' => 'asc');
1121 function _forum_get_topic_order_sql($sortby) {
1122 $order = _forum_get_topic_order($sortby);
1123 return $order['field'] .' '. $order['sort'];