diff --git a/core/modules/forum/config/entity.display.taxonomy_term.forums.default.yml b/core/modules/forum/config/entity.display.taxonomy_term.forums.default.yml new file mode 100644 index 0000000..ef18047 --- /dev/null +++ b/core/modules/forum/config/entity.display.taxonomy_term.forums.default.yml @@ -0,0 +1,9 @@ +id: taxonomy_term.forums.default +uuid: adaef6a9-8dc0-4f2e-9858-88daac440aa9 +targetEntityType: taxonomy_term +bundle: forums +mode: default +content: + description: + weight: '0' + visible: '1' diff --git a/core/modules/forum/config/entity.form_display.taxonomy_term.forums.default.yml b/core/modules/forum/config/entity.form_display.taxonomy_term.forums.default.yml new file mode 100644 index 0000000..203b69a --- /dev/null +++ b/core/modules/forum/config/entity.form_display.taxonomy_term.forums.default.yml @@ -0,0 +1,12 @@ +id: taxonomy_term.forums.default +uuid: c8eab085-8fd3-4545-8600-e13b7d8bb9c4 +targetEntityType: taxonomy_term +bundle: forums +mode: default +content: + name: + weight: '-5' + visible: '1' + description: + weight: '0' + visible: '1' diff --git a/core/modules/forum/config/field.field.forum_container.yml b/core/modules/forum/config/field.field.forum_container.yml new file mode 100644 index 0000000..96eb824 --- /dev/null +++ b/core/modules/forum/config/field.field.forum_container.yml @@ -0,0 +1,18 @@ +id: taxonomy_term.forum_container +uuid: babf2ba1-505f-4c71-8a07-7be19f4fb9f3 +status: '1' +langcode: en +name: forum_container +type: list_boolean +settings: + allowed_values: + - '' + - '' + allowed_values_function: '' +module: options +active: '1' +entity_type: taxonomy_term +locked: '1' +cardinality: '1' +translatable: '0' +indexes: { } diff --git a/core/modules/forum/config/field.instance.taxonomy_term.forums.forum_container.yml b/core/modules/forum/config/field.instance.taxonomy_term.forums.forum_container.yml new file mode 100644 index 0000000..c8c36af --- /dev/null +++ b/core/modules/forum/config/field.instance.taxonomy_term.forums.forum_container.yml @@ -0,0 +1,16 @@ +id: taxonomy_term.forums.forum_container +uuid: 8421d585-f6ef-4209-ad00-cfb30a1ab075 +status: '1' +langcode: en +field_uuid: babf2ba1-505f-4c71-8a07-7be19f4fb9f3 +entity_type: taxonomy_term +bundle: forums +label: Container +description: '' +required: '1' +default_value: + - + value: '0' +default_value_function: '' +settings: { } +field_type: list_boolean diff --git a/core/modules/forum/config/forum.settings.yml b/core/modules/forum/config/forum.settings.yml index 3f0b30f..4f2516e 100644 --- a/core/modules/forum/config/forum.settings.yml +++ b/core/modules/forum/config/forum.settings.yml @@ -3,7 +3,6 @@ block: limit: '5' new: limit: '5' -containers: [] topics: hot_threshold: '15' order: '1' diff --git a/core/modules/forum/config/schema/forum.schema.yml b/core/modules/forum/config/schema/forum.schema.yml index 60bd0f7..9ca1ed8 100644 --- a/core/modules/forum/config/schema/forum.schema.yml +++ b/core/modules/forum/config/schema/forum.schema.yml @@ -22,12 +22,6 @@ forum.settings: limit: type: integer label: 'New forum Count' - containers: - type: sequence - label: 'Containers to group related forums' - sequence: - - type: integer - label: 'Taxonomy Term ID' topics: type: mapping label: 'Forum topics block' diff --git a/core/modules/forum/forum.admin.inc b/core/modules/forum/forum.admin.inc index 332f11c..962e84d 100644 --- a/core/modules/forum/forum.admin.inc +++ b/core/modules/forum/forum.admin.inc @@ -32,7 +32,7 @@ function forum_overview($form, &$form_state) { $term = $form['terms'][$key]['#term']; $form['terms'][$key]['term']['#href'] = 'forum/' . $term->id(); unset($form['terms'][$key]['operations']['#links']['delete']); - if (in_array($form['terms'][$key]['#term']->id(), $config->get('containers'))) { + if (!empty($term->forum_container->value)) { $form['terms'][$key]['operations']['#links']['edit']['title'] = t('edit container'); $form['terms'][$key]['operations']['#links']['edit']['href'] = 'admin/structure/forum/edit/container/' . $term->id(); // We don't want the redirect from the link so we can redirect the diff --git a/core/modules/forum/forum.info.yml b/core/modules/forum/forum.info.yml index 86e5627..c136ae4 100644 --- a/core/modules/forum/forum.info.yml +++ b/core/modules/forum/forum.info.yml @@ -6,6 +6,7 @@ dependencies: - history - taxonomy - comment + - options package: Core version: VERSION core: 8.x diff --git a/core/modules/forum/forum.install b/core/modules/forum/forum.install index adc8ddb..e4b1e26 100644 --- a/core/modules/forum/forum.install +++ b/core/modules/forum/forum.install @@ -5,6 +5,8 @@ * Install, update, and uninstall functions for the Forum module. */ +use Drupal\Core\Language\Language; + /** * Implements hook_install(). */ @@ -73,6 +75,7 @@ function forum_enable() { 'description' => '', 'parent' => array(0), 'vid' => $vocabulary->id(), + 'forum_container' => 0, )); $term->save(); @@ -109,6 +112,18 @@ function forum_enable() { } /** + * Implements hook_modules_preinstall(). + */ +function forum_modules_preinstall($modules) { + $list_boolean = Drupal::service('plugin.manager.entity.field.field_type')->getDefinition('list_boolean'); + if (empty($list_boolean) && in_array('forum', $modules)) { + // Make sure that the list_boolean field type is available before our + // default config is installed. + field_info_cache_clear(); + } +} + +/** * Implements hook_uninstall(). */ function forum_uninstall() { @@ -119,8 +134,15 @@ function forum_uninstall() { $field->delete(); } - // Purge field data now to allow taxonomy module to be uninstalled - // if this is the only field remaining. + if ($field = field_info_field('taxonomy_term', 'forum_container')) { + $field->delete(); + } + + // Purge field data now to allow taxonomy and options module to be uninstalled + // if this is the only field remaining. We need to run it twice because + // field_purge_batch() will not remove the instance and the field in the same + // pass. + field_purge_batch(10); field_purge_batch(10); // Allow to delete a forum's node type. $locked = Drupal::state()->get('node.type.locked'); @@ -261,13 +283,131 @@ function forum_update_last_removed() { * @ingroup config_upgrade */ function forum_update_8000() { + $map = db_query('SELECT vid, machine_name FROM {taxonomy_vocabulary}')->fetchAllKeyed(); + $forum_vid = update_variable_get('forum_nav_vocabulary', FALSE); + if (!empty($map[$forum_vid])) { + // Update the variable to reference the machine name instead of the vid. + update_variable_set('forum_nav_vocabulary', $map[$forum_vid]); + } update_variables_to_config('forum.settings', array( 'forum_hot_topic' => 'topics.hot_threshold', 'forum_per_page' => 'topics.page_limit', 'forum_order' => 'topics.order', 'forum_nav_vocabulary' => 'vocabulary', - 'forum_containers' => 'containers', 'forum_block_num_active' => 'block.active.limit', 'forum_block_num_new' => 'block.new.limit', )); } + +/** + * Implements hook_update_dependencies(). + */ +function forum_update_dependencies() { + // Convert containers to field after the fields and instances are converted to + // ConfigEntities. + $dependencies['forum'][8001] = array( + 'field' => 8003, + 'taxonomy' => 8007, + ); + return $dependencies; +} + +/** + * Adds the forum_container field and copies the values over. + */ +function forum_update_8001() { + $vocabulary = config('forum.settings')->get('vocabulary'); + // Create the field and instance. + $field = array( + 'id' => 'taxonomy_term.forum_container', + 'name' => 'forum_container', + 'entity_type' => 'taxonomy_term', + 'module' => 'options', + 'type' => 'list_boolean', + 'cardinality' => 1, + 'locked' => TRUE, + 'indexes' => array(), + 'settings' => array( + 'allowed_values' => array('', ''), + 'allowed_values_function' => '', + ), + 'schema' => array( + 'columns' => array( + 'value' => array( + 'type' => 'int', + 'not null' => FALSE, + ), + ), + 'indexes' => array(), + 'foreign keys' => array(), + ), + ); + _update_8003_field_create_field($field); + + $instance = array( + 'id' => 'taxonomy_term.' . $vocabulary . '.forum_container', + 'entity_type' => 'taxonomy_term', + 'label' => 'Container', + 'bundle' => $vocabulary, + 'description' => '', + 'required' => TRUE, + 'settings' => array(), + 'default_value' => array('value' => 0), + ); + _update_8003_field_create_instance($field, $instance); +} + +/** + * Migrate forum containers from variable to field values. + */ +function forum_update_8002(&$sandbox) { + // Initialize total values to process. + if (!isset($sandbox['total'])) { + $containers = update_variable_get('forum_containers', array()); + $vocabulary = config('forum.settings')->get('vocabulary'); + $sandbox['containers'] = $containers; + $sandbox['vocabulary'] = $vocabulary; + $sandbox['total'] = count($containers); + $sandbox['processed'] = 0; + } + + if ($sandbox['total']) { + // Retrieve next 20 containers to migrate. + $containers = array_splice($containers, $sandbox['processed'], 20); + foreach ($containers as $tid) { + // Add a row to the field data and revision tables. + db_insert('taxonomy_term__forum_container') + ->fields(array( + 'bundle' => $sandbox['vocabulary'], + 'entity_id' => $tid, + 'revision_id' => $tid, + 'langcode' => Language::LANGCODE_NOT_SPECIFIED, + 'delta' => 0, + 'forum_container_value' => 1, + )) + ->execute(); + db_insert('taxonomy_term_revision__forum_container') + ->fields(array( + 'bundle' => $vocabulary, + 'entity_id' => $tid, + 'revision_id' => $tid, + 'langcode' => Language::LANGCODE_NOT_SPECIFIED, + 'delta' => 0, + 'forum_container_value' => 1, + )) + ->execute(); + } + + // Report status. + $sandbox['processed'] += count($containers); + } + $sandbox['#finished'] = $sandbox['total'] ? $sandbox['processed'] / $sandbox['total'] : 1; + +} + +/** + * Remove the forum_containers variable. + */ +function forum_update_8003() { + update_variable_del('forum_containers'); +} diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module index 1ed6ac3..1b71245 100644 --- a/core/modules/forum/forum.module +++ b/core/modules/forum/forum.module @@ -6,6 +6,7 @@ */ use Drupal\Core\Entity\EntityInterface; +use Drupal\field\Field; use Drupal\node\NodeInterface; use Drupal\taxonomy\Entity\Term; @@ -74,7 +75,7 @@ function forum_theme() { return array( 'forums' => array( 'template' => 'forums', - 'variables' => array('forums' => NULL, 'topics' => NULL, 'parents' => NULL, 'tid' => NULL, 'sortby' => NULL, 'forum_per_page' => NULL), + 'variables' => array('forums' => NULL, 'topics' => NULL, 'parents' => NULL, 'term' => NULL, 'sortby' => NULL, 'forum_per_page' => NULL), ), 'forum_list' => array( 'template' => 'forum-list', @@ -105,18 +106,13 @@ function forum_theme() { function forum_menu() { $items['forum'] = array( 'title' => 'Forums', - 'page callback' => 'forum_page', - 'access arguments' => array('access content'), - 'file' => 'forum.pages.inc', + 'route_name' => 'forum_index', ); - $items['forum/%forum_forum'] = array( + $items['forum/%forum'] = array( 'title' => 'Forums', 'title callback' => 'entity_page_label', 'title arguments' => array(1), - 'page callback' => 'forum_page', - 'page arguments' => array(1), - 'access arguments' => array('access content'), - 'file' => 'forum.pages.inc', + 'route_name' => 'forum_page', ); $items['admin/structure/forum'] = array( 'title' => 'Forums', @@ -168,48 +164,53 @@ function forum_menu_local_tasks(&$data, $router_item, $root_path) { // Add action link to 'node/add/forum' on 'forum' sub-pages. if ($root_path == 'forum' || $root_path == 'forum/%') { - $tid = (isset($router_item['page_arguments'][0]) ? $router_item['page_arguments'][0]->id() : 0); - $forum_term = forum_forum_load($tid); - if ($forum_term) { - $links = array(); - // Loop through all bundles for forum taxonomy vocabulary field. - $field = field_info_field('node', 'taxonomy_forums'); - foreach ($field['bundles'] as $type_name) { - if (($type = entity_load('node_type', $type_name)) && node_access('create', $type_name)) { - $links[$type_name] = array( - '#theme' => 'menu_local_action', - '#link' => array( - 'title' => t('Add new @node_type', array('@node_type' => $type->label())), - 'href' => 'node/add/' . $type_name . '/' . $forum_term->id(), - ), - ); + $request = Drupal::request(); + $forum_term = $request->attributes->get('taxonomy_term'); + $vid = Drupal::config('forum.settings')->get('vocabulary'); + $links = array(); + // Loop through all bundles for forum taxonomy vocabulary field. + $field = Field::fieldInfo()->getField('node', 'taxonomy_forums'); + foreach ($field['bundles'] as $type) { + if (node_access('create', $type)) { + $links[$type] = array( + '#theme' => 'menu_local_action', + '#link' => array( + 'title' => t('Add new @node_type', array('@node_type' => entity_load('node_type', $type)->label())), + 'href' => 'node/add/' . $type, + ), + ); + if ($forum_term && $forum_term->bundle() == $vid) { + // We are viewing a forum term (specific forum), append the tid to the + // url. + $links[$type]['#link']['href'] .= '/' . $forum_term->id(); + } } - if (empty($links)) { - // Authenticated user does not have access to create new topics. - if ($user->isAuthenticated()) { - $links['disallowed'] = array( - '#theme' => 'menu_local_action', - '#link' => array( - 'title' => t('You are not allowed to post new content in the forum.'), - ), - ); - } - // Anonymous user does not have access to create new topics. - else { - $links['login'] = array( - '#theme' => 'menu_local_action', - '#link' => array( - 'title' => t('Log in to post new content in the forum.', array( - '@login' => url('user/login', array('query' => drupal_get_destination())), - )), - 'localized_options' => array('html' => TRUE), - ), - ); - } + } + if (empty($links)) { + // Authenticated user does not have access to create new topics. + if ($user->isAuthenticated()) { + $links['disallowed'] = array( + '#theme' => 'menu_local_action', + '#link' => array( + 'title' => t('You are not allowed to post new content in the forum.'), + ), + ); + } + // Anonymous user does not have access to create new topics. + else { + $links['login'] = array( + '#theme' => 'menu_local_action', + '#link' => array( + 'title' => t('Log in to post new content in the forum.', array( + '@login' => url('user/login', array('query' => drupal_get_destination())), + )), + 'localized_options' => array('html' => TRUE), + ), + ); } - $data['actions'] += $links; } + $data['actions'] += $links; } } @@ -256,33 +257,17 @@ function forum_uri($forum) { } /** - * Checks whether a node can be used in a forum, based on its content type. - * - * @param \Drupal\Core\Entity\EntityInterface $node - * A node entity. - * - * @return - * Boolean indicating if the node can be assigned to a forum. - */ -function _forum_node_check_node_type(EntityInterface $node) { - // Fetch information about the forum field. - $instance = field_info_instance('node', 'taxonomy_forums', $node->getType()); - return !empty($instance); -} - -/** * Implements hook_node_validate(). * * Checks in particular that the node is assigned only a "leaf" term in the * forum taxonomy. */ function forum_node_validate(EntityInterface $node, $form) { - if (_forum_node_check_node_type($node)) { + if (Drupal::service('forum_manager')->checkNodeType($node)) { $langcode = $form['taxonomy_forums']['#language']; // vocabulary is selected, not a "container" term. if (!$node->taxonomy_forums->isEmpty()) { // Extract the node's proper topic ID. - $containers = Drupal::config('forum.settings')->get('containers'); foreach ($node->taxonomy_forums as $delta => $item) { // If no term was selected (e.g. when no terms exist yet), remove the // item. @@ -299,7 +284,7 @@ function forum_node_validate(EntityInterface $node, $form) { ':tid' => $term->id(), ':vid' => $term->bundle(), ))->fetchField(); - if ($used && in_array($term->id(), $containers)) { + if ($used && !empty($term->forum_container->value)) { 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->label()))); } } @@ -313,8 +298,7 @@ function forum_node_validate(EntityInterface $node, $form) { * Assigns the forum taxonomy when adding a topic from within a forum. */ function forum_node_presave(EntityInterface $node) { - - if (_forum_node_check_node_type($node)) { + if (Drupal::service('forum_manager')->checkNodeType($node)) { // Make sure all fields are set properly: $node->icon = !empty($node->icon) ? $node->icon : ''; if (!$node->taxonomy_forums->isEmpty()) { @@ -335,7 +319,7 @@ function forum_node_presave(EntityInterface $node) { * Implements hook_node_update(). */ function forum_node_update(EntityInterface $node) { - if (_forum_node_check_node_type($node)) { + if (Drupal::service('forum_manager')->checkNodeType($node)) { // If this is not a new revision and does exist, update the forum record, // otherwise insert a new one. if ($node->getRevisionId() == $node->original->getRevisionId() && db_query('SELECT tid FROM {forum} WHERE nid=:nid', array(':nid' => $node->id()))->fetchField()) { @@ -413,7 +397,7 @@ function forum_node_update(EntityInterface $node) { * Implements hook_node_insert(). */ function forum_node_insert(EntityInterface $node) { - if (_forum_node_check_node_type($node)) { + if (Drupal::service('forum_manager')->checkNodeType($node)) { if (!empty($node->forum_tid)) { $nid = db_insert('forum') ->fields(array( @@ -448,7 +432,7 @@ function forum_node_insert(EntityInterface $node) { * Implements hook_node_predelete(). */ function forum_node_predelete(EntityInterface $node) { - if (_forum_node_check_node_type($node)) { + if (Drupal::service('forum_manager')->checkNodeType($node)) { db_delete('forum') ->condition('nid', $node->id()) ->execute(); @@ -464,7 +448,7 @@ function forum_node_predelete(EntityInterface $node) { function forum_node_load($nodes) { $node_vids = array(); foreach ($nodes as $node) { - if (_forum_node_check_node_type($node)) { + if (Drupal::service('forum_manager')->checkNodeType($node)) { $node_vids[] = $node->getRevisionId(); } } @@ -493,27 +477,13 @@ function forum_permission() { } /** - * Implements hook_taxonomy_term_delete(). - */ -function forum_taxonomy_term_delete(Term $term) { - // For containers, remove the tid from the forum_containers variable. - $config = Drupal::config('forum.settings'); - $containers = $config->get('containers'); - $key = array_search($term->id(), $containers); - if ($key !== FALSE) { - unset($containers[$key]); - } - $config->set('containers', $containers)->save(); -} - -/** * Implements hook_comment_publish(). * * This actually handles the insertion and update of published nodes since * $comment->save() calls hook_comment_publish() for all published comments. */ function forum_comment_publish($comment) { - _forum_update_forum_index($comment->nid->target_id); + Drupal::service('forum_manager')->updateIndex($comment->nid->target_id); } /** @@ -526,7 +496,7 @@ function forum_comment_update($comment) { // $comment->save() calls hook_comment_publish() for all published comments, // so we need to handle all other values here. if (!$comment->status->value) { - _forum_update_forum_index($comment->nid->target_id); + Drupal::service('forum_manager')->updateIndex($comment->nid->target_id); } } @@ -534,14 +504,14 @@ function forum_comment_update($comment) { * Implements hook_comment_unpublish(). */ function forum_comment_unpublish($comment) { - _forum_update_forum_index($comment->nid->target_id); + Drupal::service('forum_manager')->updateIndex($comment->nid->target_id); } /** * Implements hook_comment_delete(). */ function forum_comment_delete($comment) { - _forum_update_forum_index($comment->nid->target_id); + Drupal::service('forum_manager')->updateIndex($comment->nid->target_id); } /** @@ -628,303 +598,6 @@ function forum_block_view_pre_render($elements) { } /** - * Returns a tree of all forums for a given taxonomy term ID. - * - * @param $tid - * (optional) Taxonomy term ID of the forum. If not given all forums will be - * returned. - * - * @return - * A tree of taxonomy objects, with the following additional properties: - * - num_topics: Number of topics in the forum. - * - num_posts: Total number of posts in all topics. - * - last_post: Most recent post for the forum. - * - forums: An array of child forums. - */ -function forum_forum_load($tid = NULL) { - $cache = &drupal_static(__FUNCTION__, array()); - - // Return a cached forum tree if available. - if (!isset($tid)) { - $tid = 0; - } - if (isset($cache[$tid])) { - return $cache[$tid]; - } - - $config = Drupal::config('forum.settings'); - $vid = $config->get('vocabulary'); - - // Load and validate the parent term. - if ($tid) { - $forum_term = entity_load('taxonomy_term', $tid); - if (!$forum_term || ($forum_term->bundle() != $vid)) { - return $cache[$tid] = FALSE; - } - } - // If $tid is 0, create an empty entity to hold the child terms. - elseif ($tid === 0) { - $forum_term = entity_create('taxonomy_term', array( - 'tid' => 0, - 'vid' => $vid, - )); - } - - // Determine if the requested term is a container. - if (!$forum_term->id() || in_array($forum_term->id(), $config->get('containers'))) { - $forum_term->container = 1; - } - - // Load parent terms. - $forum_term->parents = taxonomy_term_load_parents_all($forum_term->id()); - - // Load the tree below. - $forums = array(); - $_forums = taxonomy_get_tree($vid, $tid, NULL, TRUE); - - if (count($_forums)) { - $query = db_select('node_field_data', 'n'); - $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid'); - $query->join('forum', 'f', 'n.vid = f.vid'); - $query->addExpression('COUNT(n.nid)', 'topic_count'); - $query->addExpression('SUM(ncs.comment_count)', 'comment_count'); - $counts = $query - ->fields('f', array('tid')) - ->condition('n.status', 1) - // @todo This should be actually filtering on the desired node status - // field language and just fall back to the default language. - ->condition('n.default_langcode', 1) - ->groupBy('tid') - ->addTag('node_access') - ->execute() - ->fetchAllAssoc('tid'); - } - - foreach ($_forums as $forum) { - // Determine if the child term is a container. - if (in_array($forum->id(), $config->get('containers'))) { - $forum->container = 1; - } - - // Merge in the topic and post counters. - if (!empty($counts[$forum->id()])) { - $forum->num_topics = $counts[$forum->id()]->topic_count; - $forum->num_posts = $counts[$forum->id()]->topic_count + $counts[$forum->id()]->comment_count; - } - else { - $forum->num_topics = 0; - $forum->num_posts = 0; - } - - // Query "Last Post" information for this forum. - $query = db_select('node_field_data', 'n'); - $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $forum->id())); - $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid'); - $query->join('users', 'u', 'ncs.last_comment_uid = u.uid'); - $query->addExpression('CASE ncs.last_comment_uid WHEN 0 THEN ncs.last_comment_name ELSE u.name END', 'last_comment_name'); - - $topic = $query - ->fields('ncs', array('last_comment_timestamp', 'last_comment_uid')) - ->condition('n.status', 1) - // @todo This should be actually filtering on the desired node status - // field language and just fall back to the default language. - ->condition('n.default_langcode', 1) - ->orderBy('last_comment_timestamp', 'DESC') - ->range(0, 1) - ->addTag('node_access') - ->execute() - ->fetchObject(); - - // Merge in the "Last Post" information. - $last_post = new stdClass(); - if (!empty($topic->last_comment_timestamp)) { - $last_post->created = $topic->last_comment_timestamp; - $last_post->name = $topic->last_comment_name; - $last_post->uid = $topic->last_comment_uid; - } - $forum->last_post = $last_post; - - $forums[$forum->id()] = $forum; - } - - // Cache the result, and return the tree. - $forum_term->forums = $forums; - $cache[$tid] = $forum_term; - return $forum_term; -} - -/** - * Calculates the number of new posts in a forum that the user has not yet read. - * - * Nodes are new if they are newer than HISTORY_READ_LIMIT. - * - * @param $term - * The term ID of the forum. - * @param $uid - * The user ID. - * - * @return - * The number of new posts in the forum that have not been read by the user. - */ -function _forum_topics_unread($term, $uid) { - $query = db_select('node_field_data', 'n'); - $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $term)); - $query->leftJoin('history', 'h', 'n.nid = h.nid AND h.uid = :uid', array(':uid' => $uid)); - $query->addExpression('COUNT(n.nid)', 'count'); - return $query - ->condition('status', 1) - // @todo This should be actually filtering on the desired node status field - // language and just fall back to the default language. - ->condition('n.default_langcode', 1) - ->condition('n.created', HISTORY_READ_LIMIT, '>') - ->isNull('h.nid') - ->addTag('node_access') - ->execute() - ->fetchField(); -} - -/** - * Gets all the topics in a forum. - * - * @param $tid - * The term ID of the forum. - * @param $sortby - * One of the following integers indicating the sort criteria: - * - 1: Date - newest first. - * - 2: Date - oldest first. - * - 3: Posts with the most comments first. - * - 4: Posts with the least comments first. - * @param $forum_per_page - * The maximum number of topics to display per page. - * - * @return - * A list of all the topics in a forum. - */ -function forum_get_topics($tid, $sortby, $forum_per_page) { - global $user, $forum_topic_list_header; - - $forum_topic_list_header = array( - array('data' => t('Topic'), 'field' => 'f.title'), - array('data' => t('Replies'), 'field' => 'f.comment_count'), - array('data' => t('Last reply'), 'field' => 'f.last_comment_timestamp'), - ); - - $order = _forum_get_topic_order($sortby); - for ($i = 0; $i < count($forum_topic_list_header); $i++) { - if ($forum_topic_list_header[$i]['field'] == $order['field']) { - $forum_topic_list_header[$i]['sort'] = $order['sort']; - } - } - - $query = db_select('forum_index', 'f') - ->extend('Drupal\Core\Database\Query\PagerSelectExtender') - ->extend('Drupal\Core\Database\Query\TableSortExtender'); - $query->fields('f'); - $query - ->condition('f.tid', $tid) - ->addTag('node_access') - ->addMetaData('base_table', 'forum_index') - ->orderBy('f.sticky', 'DESC') - ->orderByHeader($forum_topic_list_header) - ->limit($forum_per_page); - - $count_query = db_select('forum_index', 'f'); - $count_query->condition('f.tid', $tid); - $count_query->addExpression('COUNT(*)'); - $count_query->addTag('node_access'); - $count_query->addMetaData('base_table', 'forum_index'); - - $query->setCountQuery($count_query); - $result = $query->execute(); - $nids = array(); - foreach ($result as $record) { - $nids[] = $record->nid; - } - if ($nids) { - $nodes = node_load_multiple($nids); - - $query = db_select('node_field_data', 'n') - ->extend('Drupal\Core\Database\Query\TableSortExtender'); - $query->fields('n', array('nid')); - - $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid'); - $query->fields('ncs', array('cid', 'last_comment_uid', 'last_comment_timestamp', 'comment_count')); - - $query->join('forum_index', 'f', 'f.nid = ncs.nid'); - $query->addField('f', 'tid', 'forum_tid'); - - $query->join('users', 'u', 'n.uid = u.uid'); - $query->addField('u', 'name'); - - $query->join('users', 'u2', 'ncs.last_comment_uid = u2.uid'); - - $query->addExpression('CASE ncs.last_comment_uid WHEN 0 THEN ncs.last_comment_name ELSE u2.name END', 'last_comment_name'); - - $query - ->orderBy('f.sticky', 'DESC') - ->orderByHeader($forum_topic_list_header) - ->condition('n.nid', $nids) - // @todo This should be actually filtering on the desired node language - // and just fall back to the default language. - ->condition('n.default_langcode', 1); - - $result = array(); - foreach ($query->execute() as $row) { - $topic = $nodes[$row->nid]; - $topic->comment_mode = $topic->comment; - - foreach ($row as $key => $value) { - $topic->{$key} = $value; - } - $result[] = $topic; - } - } - else { - $result = array(); - } - - $topics = array(); - $first_new_found = FALSE; - foreach ($result as $topic) { - if ($user->isAuthenticated()) { - // A forum is new if the topic is new, or if there are new comments since - // the user's last visit. - if ($topic->forum_tid != $tid) { - $topic->new = 0; - } - else { - $history = _forum_user_last_visit($topic->id()); - $topic->new_replies = comment_num_new($topic->id(), $history); - $topic->new = $topic->new_replies || ($topic->last_comment_timestamp > $history); - } - } - else { - // Do not track "new replies" status for topics if the user is anonymous. - $topic->new_replies = 0; - $topic->new = 0; - } - - // Make sure only one topic is indicated as the first new topic. - $topic->first_new = FALSE; - if ($topic->new != 0 && !$first_new_found) { - $topic->first_new = TRUE; - $first_new_found = TRUE; - } - - if ($topic->comment_count > 0) { - $last_reply = new stdClass(); - $last_reply->created = $topic->last_comment_timestamp; - $last_reply->name = $topic->last_comment_name; - $last_reply->uid = $topic->last_comment_uid; - $topic->last_reply = $last_reply; - } - $topics[$topic->id()] = $topic; - } - - return $topics; -} - -/** * Implements hook_preprocess_HOOK() for block.html.twig. */ function forum_preprocess_block(&$variables) { @@ -945,7 +618,7 @@ function forum_preprocess_block(&$variables) { * - topics: An array of all the topics in the current forum. * - parents: An array of taxonomy term objects that are ancestors of the * current term ID. - * - tid: Taxonomy term ID of the current forum. + * - term: Taxonomy term of the current forum. * - sortby: One of the following integers indicating the sort criteria: * - 1: Date - newest first. * - 2: Date - oldest first. @@ -954,6 +627,7 @@ function forum_preprocess_block(&$variables) { * - forum_per_page: The maximum number of topics to display per page. */ function template_preprocess_forums(&$variables) { + $variables['tid'] = $variables['term']->id(); if ($variables['forums_defined'] = count($variables['forums']) || count($variables['parents'])) { if (!empty($variables['forums'])) { $variables['forums'] = array( @@ -967,7 +641,7 @@ function template_preprocess_forums(&$variables) { $variables['forums'] = array(); } - if ($variables['tid'] && array_search($variables['tid'], Drupal::config('forum.settings')->get('containers')) === FALSE) { + if ($variables['term'] && empty($variables['term']->forum_container->value)) { $variables['topics'] = array( '#theme' => 'forum_topic_list', '#tid' => $variables['tid'], @@ -1024,7 +698,7 @@ function template_preprocess_forum_list(&$variables) { $variables['forums'][$id]->description = filter_xss_admin($forum->description->value); $variables['forums'][$id]->link = url("forum/" . $forum->id()); $variables['forums'][$id]->name = check_plain($forum->label()); - $variables['forums'][$id]->is_container = !empty($forum->container); + $variables['forums'][$id]->is_container = !empty($forum->forum_container->value); $variables['forums'][$id]->zebra = $row % 2 == 0 ? 'odd' : 'even'; $row++; @@ -1035,7 +709,7 @@ function template_preprocess_forum_list(&$variables) { $variables['forums'][$id]->icon_class = 'default'; $variables['forums'][$id]->icon_title = t('No new posts'); if ($user->isAuthenticated()) { - $variables['forums'][$id]->new_topics = _forum_topics_unread($forum->id(), $user->id()); + $variables['forums'][$id]->new_topics = Drupal::service('forum_manager')->unreadTopics($forum->id(), $user->id()); if ($variables['forums'][$id]->new_topics) { $variables['forums'][$id]->new_text = format_plural($variables['forums'][$id]->new_topics, '1 new post in forum %title', '@count new posts in forum %title', array('%title' => $variables['forums'][$id]->label())); $variables['forums'][$id]->new_url = url('forum/' . $forum->id(), array('fragment' => 'new')); @@ -1071,12 +745,14 @@ function template_preprocess_forum_list(&$variables) { function template_preprocess_forum_topic_list(&$variables) { global $forum_topic_list_header; - // Create the tablesorting header. - $ts = tablesort_init($forum_topic_list_header); $header = ''; - foreach ($forum_topic_list_header as $cell) { - $cell = tablesort_header($cell, $forum_topic_list_header, $ts); - $header .= _theme_table_cell($cell, TRUE); + if (!empty($forum_topic_list_header)) { + // Create the tablesorting header. + $ts = tablesort_init($forum_topic_list_header); + foreach ($forum_topic_list_header as $cell) { + $cell = tablesort_header($cell, $forum_topic_list_header, $ts); + $header .= _theme_table_cell($cell, TRUE); + } } $variables['header'] = $header; @@ -1204,102 +880,6 @@ function template_preprocess_forum_submitted(&$variables) { } /** - * Gets the last time the user viewed a node. - * - * @param $nid - * The node ID. - * - * @return - * The timestamp when the user last viewed this node, if the user has - * previously viewed the node; otherwise HISTORY_READ_LIMIT. - */ -function _forum_user_last_visit($nid) { - global $user; - $history = &drupal_static(__FUNCTION__, array()); - - if (empty($history)) { - $result = db_query('SELECT nid, timestamp FROM {history} WHERE uid = :uid', array(':uid' => $user->id())); - foreach ($result as $t) { - $history[$t->nid] = $t->timestamp > HISTORY_READ_LIMIT ? $t->timestamp : HISTORY_READ_LIMIT; - } - } - return isset($history[$nid]) ? $history[$nid] : HISTORY_READ_LIMIT; -} - -/** - * Gets topic sorting information based on an integer code. - * - * @param $sortby - * One of the following integers indicating the sort criteria: - * - 1: Date - newest first. - * - 2: Date - oldest first. - * - 3: Posts with the most comments first. - * - 4: Posts with the least comments first. - * - * @return - * An array with the following values: - * - field: A field for an SQL query. - * - sort: 'asc' or 'desc'. - */ -function _forum_get_topic_order($sortby) { - switch ($sortby) { - case 1: - return array('field' => 'f.last_comment_timestamp', 'sort' => 'desc'); - break; - case 2: - return array('field' => 'f.last_comment_timestamp', 'sort' => 'asc'); - break; - case 3: - return array('field' => 'f.comment_count', 'sort' => 'desc'); - break; - case 4: - return array('field' => 'f.comment_count', 'sort' => 'asc'); - break; - } -} - -/** - * Updates the taxonomy index for a given node. - * - * @param $nid - * The ID of the node to update. - */ -function _forum_update_forum_index($nid) { - $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( - ':nid' => $nid, - ':status' => COMMENT_PUBLISHED, - ))->fetchField(); - - if ($count > 0) { - // Comments exist. - $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( - ':nid' => $nid, - ':status' => COMMENT_PUBLISHED, - ))->fetchObject(); - db_update('forum_index') - ->fields( array( - 'comment_count' => $count, - 'last_comment_timestamp' => $last_reply->created, - )) - ->condition('nid', $nid) - ->execute(); - } - else { - // Comments do not exist. - // @todo This should be actually filtering on the desired node language and - // just fall back to the default language. - $node = db_query('SELECT uid, created FROM {node_field_data} WHERE nid = :nid AND default_langcode = 1', array(':nid' => $nid))->fetchObject(); - db_update('forum_index') - ->fields( array( - 'comment_count' => 0, - 'last_comment_timestamp' => $node->created, - )) - ->condition('nid', $nid) - ->execute(); - } -} - -/** * Returns HTML for a forum form. * * By default this does not alter the appearance of a form at all, but is diff --git a/core/modules/forum/forum.pages.inc b/core/modules/forum/forum.pages.inc deleted file mode 100644 index 3908f31..0000000 --- a/core/modules/forum/forum.pages.inc +++ /dev/null @@ -1,62 +0,0 @@ -get('vocabulary')); - - if (!isset($forum_term)) { - // On the main page, display all the top-level forums. - $forum_term = forum_forum_load(0); - // Set the page title to forum's vocabulary name. - drupal_set_title($vocabulary->label()); - } - - if ($forum_term->id() && array_search($forum_term->id(), $config->get('containers')) === FALSE) { - // Add RSS feed for forums. - drupal_add_feed('taxonomy/term/' . $forum_term->id() . '/feed', 'RSS - ' . $forum_term->label()); - } - - if (empty($forum_term->forums) && empty($forum_term->parents)) { - // Root of empty forum. - drupal_set_title(t('No forums defined')); - } - - $forum_per_page = $config->get('topics.page_limit'); - $sort_by = $config->get('topics.order'); - - if (empty($forum_term->container)) { - $topics = forum_get_topics($forum_term->id(), $sort_by, $forum_per_page); - } - else { - $topics = ''; - } - - $build = array( - '#theme' => 'forums', - '#forums' => $forum_term->forums, - '#topics' => $topics, - '#parents' => $forum_term->parents, - '#tid' => $forum_term->id(), - '#sortby' => $sort_by, - '#forums_per_page' => $forum_per_page, - ); - $build['#attached']['css'][] = drupal_get_path('module', 'forum') . '/css/forum.module.css'; - return $build; -} diff --git a/core/modules/forum/forum.routing.yml b/core/modules/forum/forum.routing.yml index fe816f0..954e04a 100644 --- a/core/modules/forum/forum.routing.yml +++ b/core/modules/forum/forum.routing.yml @@ -4,12 +4,28 @@ forum_delete: _form: 'Drupal\forum\Form\DeleteForm' requirements: _permission: 'administer forums' + forum_settings: pattern: '/admin/structure/forum/settings' defaults: _form: '\Drupal\forum\ForumSettingsForm' requirements: _permission: 'administer forums' + +forum_index: + pattern: '/forum' + defaults: + _content: 'Drupal\forum\Controller\ForumController::forumIndex' + requirements: + _permission: 'access content' + +forum_page: + pattern: '/forum/{taxonomy_term}' + defaults: + _content: 'Drupal\forum\Controller\ForumController::forumPage' + requirements: + _permission: 'access content' + forum_add_container: pattern: 'admin/structure/forum/add/container' defaults: diff --git a/core/modules/forum/forum.services.yml b/core/modules/forum/forum.services.yml index e246711..8ea4dac 100644 --- a/core/modules/forum/forum.services.yml +++ b/core/modules/forum/forum.services.yml @@ -1,6 +1,9 @@ services: + forum_manager: + class: Drupal\forum\ForumManager + arguments: ['@config.factory', '@plugin.manager.entity', '@database', '@field.info', '@string_translation'] forum.breadcrumb: class: Drupal\forum\ForumBreadcrumbBuilder - arguments: ['@entity.manager', '@config.factory'] + arguments: ['@entity.manager', '@config.factory', '@forum_manager'] tags: - { name: breadcrumb_builder, priority: 1001 } diff --git a/core/modules/forum/lib/Drupal/forum/Controller/ForumController.php b/core/modules/forum/lib/Drupal/forum/Controller/ForumController.php index e9fec4b..a0db5d2 100644 --- a/core/modules/forum/lib/Drupal/forum/Controller/ForumController.php +++ b/core/modules/forum/lib/Drupal/forum/Controller/ForumController.php @@ -7,10 +7,15 @@ namespace Drupal\forum\Controller; -use Drupal\Core\Config\ConfigFactory; +use Drupal\Core\Config\Config; +use Drupal\Core\Controller\ControllerInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityManager; +use Drupal\Core\StringTranslation\TranslationManager; +use Drupal\forum\ForumManagerInterface; +use Drupal\taxonomy\TermInterface; use Drupal\taxonomy\TermStorageControllerInterface; +use Drupal\taxonomy\VocabularyStorageControllerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -19,6 +24,13 @@ class ForumController implements ContainerInjectionInterface { /** + * Forum manager service. + * + * @var \Drupal\forum\ForumManagerInterface + */ + protected $forumManager; + + /** * Entity Manager Service. * * @var \Drupal\Core\Entity\EntityManager @@ -33,37 +45,141 @@ class ForumController implements ContainerInjectionInterface { protected $config; /** + * Vocabulary storage controller. + * + * @var \Drupal\taxonomy\VocabularyStorageControllerInterface + */ + protected $vocabularyStorageController; + + /** * Term storage controller. * * @var \Drupal\taxonomy\TermStorageControllerInterface */ - protected $storageController; + protected $termStorageController; + + /** + * Translation manager service. + * + * @var \Drupal\Core\StringTranslation\TranslationManager + */ + protected $translationManager; + + /** + * Constructs a ForumController object. + * + * @param \Drupal\Core\Config\Config $config + * Config object for forum.settings. + * @param \Drupal\forum\ForumManagerInterface $forum_manager + * The forum manager service. + * @param \Drupal\taxonomy\VocabularyStorageControllerInterface $vocabulary_storage_controller + * Vocabulary storage controller. + * @param \Drupal\taxonomy\TermStorageControllerInterface $term_storage_controller + * Term storage controller. + * @param \Drupal\Core\Entity\EntityManager $entity_manager + * The entity manager service. + * @param \Drupal\Core\StringTranslation\TranslationManager $translation_manager + * The translation manager service. + */ + public function __construct(Config $config, ForumManagerInterface $forum_manager, VocabularyStorageControllerInterface $vocabulary_storage_controller, TermStorageControllerInterface $term_storage_controller, EntityManager $entity_manager, TranslationManager $translation_manager) { + $this->config = $config; + $this->forumManager = $forum_manager; + $this->vocabularyStorageController = $vocabulary_storage_controller; + $this->termStorageController = $term_storage_controller; + $this->entityManager = $entity_manager; + $this->translationManager = $translation_manager; + } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( + $container->get('config.factory')->get('forum.settings'), + $container->get('forum_manager'), + $container->get('entity.manager')->getStorageController('taxonomy_vocabulary'), + $container->get('entity.manager')->getStorageController('taxonomy_term'), $container->get('entity.manager'), - $container->get('config.factory'), - $container->get('entity.manager')->getStorageController('taxonomy_term') + $container->get('string_translation') ); } /** - * Constructs a ForumController object. + * Returns forum page for a given forum. * - * @param \Drupal\Core\Entity\EntityManager $entity_manager - * The entity manager service. - * @param \Drupal\Core\Config\ConfigFactory $config_factory - * The factory for configuration objects. - * @param \Drupal\taxonomy\TermStorageControllerInterface $storage_controller - * The term storage controller. + * @param \Drupal\taxonomy\TermInterface $taxonomy_term + * The forum to render the page for. + * + * @return array + * A render array. */ - public function __construct(EntityManager $entity_manager, ConfigFactory $config_factory, TermStorageControllerInterface $storage_controller) { - $this->entityManager = $entity_manager; - $this->config = $config_factory->get('forum.settings'); - $this->storageController = $storage_controller; + public function forumPage(TermInterface $taxonomy_term) { + // Get forum details. + $taxonomy_term->forums = $this->forumManager->getChildren($this->config->get('vocabulary'), $taxonomy_term->id()); + $taxonomy_term->parents = $this->forumManager->getParents($taxonomy_term->id()); + if (empty($taxonomy_term->forum_container->value)) { + // Add RSS feed for forums. + drupal_add_feed('taxonomy/term/' . $taxonomy_term->id() . '/feed', 'RSS - ' . $taxonomy_term->label()); + } + + if (empty($taxonomy_term->forum_container->value)) { + $topics = $this->forumManager->getTopics($taxonomy_term->id()); + } + else { + $topics = ''; + } + return $this->build($taxonomy_term->forums, $taxonomy_term, $topics, $taxonomy_term->parents); + } + + /** + * Returns forum index page. + * + * @return array + * A render array. + */ + public function forumIndex() { + $vocabulary = $this->vocabularyStorageController->load($this->config->get('vocabulary')); + $index = $this->forumManager->getIndex(); + $build = $this->build($index->forums, $index); + if (empty($index->forums)) { + // Root of empty forum. + $build['#title'] = $this->translationManager->translate('No forums defined'); + } + else { + // Set the page title to forum's vocabulary name. + $build['#title'] = $vocabulary->label(); + } + return $build; + } + + /** + * Returns a renderable forum index page array. + * + * @param array $forums + * A list of forums. + * @param \Drupal\taxonomy\TermInterface $term + * The taxonomy term of the forum. + * @param array $topics + * The topics of this forum. + * @param array $parents + * The parent forums in relation this forum. + * + * @return array + * A render array. + */ + protected function build($forums, TermInterface $term, $topics = array(), $parents = array()) { + $build = array( + '#theme' => 'forums', + '#forums' => $forums, + '#topics' => $topics, + '#parents' => $parents, + '#term' => $term, + '#sortby' => $this->config->get('topics.order'), + '#forums_per_page' => $this->config->get('topics.page_limit'), + ); + // @todo Make this a library - see https://drupal.org/node/2028113. + $build['#attached']['css'][] = drupal_get_path('module', 'forum') . '/css/forum.module.css'; + return $build; } /** @@ -74,8 +190,9 @@ public function __construct(EntityManager $entity_manager, ConfigFactory $config */ public function addForum() { $vid = $this->config->get('vocabulary'); - $taxonomy_term = $this->storageController->create(array( + $taxonomy_term = $this->termStorageController->create(array( 'vid' => $vid, + 'forum_controller' => 0, )); return $this->entityManager->getForm($taxonomy_term, 'forum'); } @@ -88,8 +205,9 @@ public function addForum() { */ public function addContainer() { $vid = $this->config->get('vocabulary'); - $taxonomy_term = $this->storageController->create(array( + $taxonomy_term = $this->termStorageController->create(array( 'vid' => $vid, + 'forum_container' => 1, )); return $this->entityManager->getForm($taxonomy_term, 'container'); } diff --git a/core/modules/forum/lib/Drupal/forum/Form/ContainerFormController.php b/core/modules/forum/lib/Drupal/forum/Form/ContainerFormController.php index 208d39c..40f8199 100644 --- a/core/modules/forum/lib/Drupal/forum/Form/ContainerFormController.php +++ b/core/modules/forum/lib/Drupal/forum/Form/ContainerFormController.php @@ -42,14 +42,8 @@ public function form(array $form, array &$form_state) { */ public function save(array $form, array &$form_state) { $is_new = $this->entity->isNew(); + $this->entity->forum_container = TRUE; $term = parent::save($form, $form_state); - if ($is_new) { - // Update config item to track the container terms. - $config = $this->configFactory->get('forum.settings'); - $containers = $config->get('containers'); - $containers[] = $term->id(); - $config->set('containers', $containers)->save(); - } } } diff --git a/core/modules/forum/lib/Drupal/forum/ForumBreadcrumbBuilder.php b/core/modules/forum/lib/Drupal/forum/ForumBreadcrumbBuilder.php index c8d523c..724baaf 100644 --- a/core/modules/forum/lib/Drupal/forum/ForumBreadcrumbBuilder.php +++ b/core/modules/forum/lib/Drupal/forum/ForumBreadcrumbBuilder.php @@ -10,6 +10,8 @@ use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface; use Drupal\Core\Config\ConfigFactory; use Drupal\Core\Entity\EntityManager; +use Drupal\forum\ForumManagerInterface; +use Symfony\Cmf\Component\Routing\RouteObjectInterface; /** * Class to define the forum breadcrumb builder. @@ -31,47 +33,47 @@ class ForumBreadcrumbBuilder implements BreadcrumbBuilderInterface { protected $entityManager; /** + * The forum manager service. + * + * @var \Drupal\forum\ForumManagerInterface + */ + protected $forumManager; + + /** * Constructs a new ForumBreadcrumbBuilder. * * @param \Drupal\Core\Entity\EntityManager * The entity manager. * @param \Drupal\Core\Config\ConfigFactory $configFactory * The configuration factory. + * @param \Drupal\forum\ForumManagerInterface $forum_manager + * The forum manager service. */ - public function __construct(EntityManager $entity_manager, ConfigFactory $configFactory) { + public function __construct(EntityManager $entity_manager, ConfigFactory $configFactory, ForumManagerInterface $forum_manager) { $this->entityManager = $entity_manager; $this->config = $configFactory->get('forum.settings'); + $this->forumManager = $forum_manager; } /** * {@inheritdoc} */ public function build(array $attributes) { - // @todo This only works for legacy routes. Once node/% and forum/% are // converted to the new router this code will need to be updated. - if (isset($attributes['_drupal_menu_item'])) { - $item = $attributes['_drupal_menu_item']; - switch ($item['path']) { - - case 'node/%': - $node = $item['map'][1]; - // Load the object in case of missing wildcard loaders. - $node = is_object($node) ? $node : node_load($node); - if (_forum_node_check_node_type($node)) { - $breadcrumb = $this->forumPostBreadcrumb($node); - } - break; - - case 'forum/%': - $term = $item['map'][1]; - // Load the object in case of missing wildcard loaders. - $term = is_object($term) ? $term : forum_forum_load($term); - $breadcrumb = $this->forumTermBreadcrumb($term); - break; + if (isset($attributes['_drupal_menu_item']) && ($item = $attributes['_drupal_menu_item']) && $item['path'] == 'node/%') { + $node = $item['map'][1]; + // Load the object in case of missing wildcard loaders. + $node = is_object($node) ? $node : node_load($node); + if ($this->forumManager->checkNodeType($node)) { + $breadcrumb = $this->forumPostBreadcrumb($node); } } + if (!empty($attributes[RouteObjectInterface::ROUTE_NAME]) && $attributes[RouteObjectInterface::ROUTE_NAME] == 'forum_page' && isset($attributes['taxonomy_term'])) { + $breadcrumb = $this->forumTermBreadcrumb($attributes['taxonomy_term']); + } + if (!empty($breadcrumb)) { return $breadcrumb; } diff --git a/core/modules/forum/lib/Drupal/forum/ForumManager.php b/core/modules/forum/lib/Drupal/forum/ForumManager.php new file mode 100644 index 0000000..0f03e63 --- /dev/null +++ b/core/modules/forum/lib/Drupal/forum/ForumManager.php @@ -0,0 +1,551 @@ +configFactory = $config_factory; + $this->entityManager = $entity_manager; + $this->connection = $connection; + $this->fieldInfo = $field_info; + $this->translationManager = $translation_manager; + } + + /** + * {@inheritdoc} + */ + public function getTopics($tid) { + $config = $this->configFactory->get('forum.settings'); + $forum_per_page = $config->get('topics.page_limit'); + $sortby = $config->get('topics.order'); + + global $user, $forum_topic_list_header; + + $forum_topic_list_header = array( + array('data' => $this->translationManager->translate('Topic'), 'field' => 'f.title'), + array('data' => $this->translationManager->translate('Replies'), 'field' => 'f.comment_count'), + array('data' => $this->translationManager->translate('Last reply'), 'field' => 'f.last_comment_timestamp'), + ); + + $order = $this->getTopicOrder($sortby); + for ($i = 0; $i < count($forum_topic_list_header); $i++) { + if ($forum_topic_list_header[$i]['field'] == $order['field']) { + $forum_topic_list_header[$i]['sort'] = $order['sort']; + } + } + + $query = $this->connection->select('forum_index', 'f') + ->extend('Drupal\Core\Database\Query\PagerSelectExtender') + ->extend('Drupal\Core\Database\Query\TableSortExtender'); + $query->fields('f'); + $query + ->condition('f.tid', $tid) + ->addTag('node_access') + ->addMetaData('base_table', 'forum_index') + ->orderBy('f.sticky', 'DESC') + ->orderByHeader($forum_topic_list_header) + ->limit($forum_per_page); + + $count_query = $this->connection->select('forum_index', 'f'); + $count_query->condition('f.tid', $tid); + $count_query->addExpression('COUNT(*)'); + $count_query->addTag('node_access'); + $count_query->addMetaData('base_table', 'forum_index'); + + $query->setCountQuery($count_query); + $result = $query->execute(); + $nids = array(); + foreach ($result as $record) { + $nids[] = $record->nid; + } + if ($nids) { + $nodes = $this->entityManager->getStorageController('node')->loadMultiple($nids); + + $query = $this->connection->select('node_field_data', 'n') + ->extend('Drupal\Core\Database\Query\TableSortExtender'); + $query->fields('n', array('nid')); + + $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid'); + $query->fields('ncs', array( + 'cid', + 'last_comment_uid', + 'last_comment_timestamp', + 'comment_count' + )); + + $query->join('forum_index', 'f', 'f.nid = ncs.nid'); + $query->addField('f', 'tid', 'forum_tid'); + + $query->join('users', 'u', 'n.uid = u.uid'); + $query->addField('u', 'name'); + + $query->join('users', 'u2', 'ncs.last_comment_uid = u2.uid'); + + $query->addExpression('CASE ncs.last_comment_uid WHEN 0 THEN ncs.last_comment_name ELSE u2.name END', 'last_comment_name'); + + $query + ->orderBy('f.sticky', 'DESC') + ->orderByHeader($forum_topic_list_header) + ->condition('n.nid', $nids) + // @todo This should be actually filtering on the desired node language + // and just fall back to the default language. + ->condition('n.default_langcode', 1); + + $result = array(); + foreach ($query->execute() as $row) { + $topic = $nodes[$row->nid]; + $topic->comment_mode = $topic->comment; + + foreach ($row as $key => $value) { + $topic->{$key} = $value; + } + $result[] = $topic; + } + } + else { + $result = array(); + } + + $topics = array(); + $first_new_found = FALSE; + foreach ($result as $topic) { + if ($user->isAuthenticated()) { + // A forum is new if the topic is new, or if there are new comments since + // the user's last visit. + if ($topic->forum_tid != $tid) { + $topic->new = 0; + } + else { + $history = $this->lastVisit($topic->id()); + $topic->new_replies = $this->numberNew($topic->id(), $history); + $topic->new = $topic->new_replies || ($topic->last_comment_timestamp > $history); + } + } + else { + // Do not track "new replies" status for topics if the user is anonymous. + $topic->new_replies = 0; + $topic->new = 0; + } + + // Make sure only one topic is indicated as the first new topic. + $topic->first_new = FALSE; + if ($topic->new != 0 && !$first_new_found) { + $topic->first_new = TRUE; + $first_new_found = TRUE; + } + + if ($topic->comment_count > 0) { + $last_reply = new \stdClass(); + $last_reply->created = $topic->last_comment_timestamp; + $last_reply->name = $topic->last_comment_name; + $last_reply->uid = $topic->last_comment_uid; + $topic->last_reply = $last_reply; + } + $topics[$topic->id()] = $topic; + } + + return $topics; + + } + + /** + * Gets topic sorting information based on an integer code. + * + * @param int $sortby + * One of the following integers indicating the sort criteria: + * - ForumManager::NEWEST_FIRST: Date - newest first. + * - ForumManager::OLDEST_FIRST: Date - oldest first. + * - ForumManager::MOST_POPULAR_FIRST: Posts with the most comments first. + * - ForumManager::LEAST_POPULAR_FIRST: Posts with the least comments first. + * + * @return array + * An array with the following values: + * - field: A field for an SQL query. + * - sort: 'asc' or 'desc'. + */ + protected function getTopicOrder($sortby) { + switch ($sortby) { + case static::NEWEST_FIRST: + return array('field' => 'f.last_comment_timestamp', 'sort' => 'desc'); + + case static::OLDEST_FIRST: + return array('field' => 'f.last_comment_timestamp', 'sort' => 'asc'); + + case static::MOST_POPULAR_FIRST: + return array('field' => 'f.comment_count', 'sort' => 'desc'); + + case static::LEAST_POPULAR_FIRST: + return array('field' => 'f.comment_count', 'sort' => 'asc'); + + } + } + + /** + * Wraps comment_num_new() in a method. + * + * @param int $nid + * Node ID. + * @param int $timestamp + * Timestamp of last read. + * + * @return int + * Number of new comments. + */ + protected function numberNew($nid, $timestamp) { + return comment_num_new($nid, $timestamp); + } + + /** + * Gets the last time the user viewed a node. + * + * @param int $nid + * The node ID. + * + * @return int + * The timestamp when the user last viewed this node, if the user has + * previously viewed the node; otherwise HISTORY_READ_LIMIT. + */ + protected function lastVisit($nid) { + global $user; + + if (empty($this->history[$nid])) { + $result = $this->connection->select('history', 'h') + ->fields('h', array('nid', 'timestamp')) + ->condition('uid', $user->id()) + ->execute(); + foreach ($result as $t) { + $this->history[$t->nid] = $t->timestamp > HISTORY_READ_LIMIT ? $t->timestamp : HISTORY_READ_LIMIT; + } + } + return isset($this->history[$nid]) ? $this->history[$nid] : HISTORY_READ_LIMIT; + } + + /** + * Provides the last post information for the given forum tid. + * + * @param int $tid + * The forum tid. + * + * @return \stdClass + * The last post for the given forum. + */ + protected function getLastPost($tid) { + if (!empty($this->lastPostData[$tid])) { + return $this->lastPostData[$tid]; + } + // Query "Last Post" information for this forum. + $query = $this->connection->select('node_field_data', 'n'); + $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $tid)); + $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid'); + $query->join('users', 'u', 'ncs.last_comment_uid = u.uid'); + $query->addExpression('CASE ncs.last_comment_uid WHEN 0 THEN ncs.last_comment_name ELSE u.name END', 'last_comment_name'); + + $topic = $query + ->fields('ncs', array('last_comment_timestamp', 'last_comment_uid')) + ->condition('n.status', 1) + ->orderBy('last_comment_timestamp', 'DESC') + ->range(0, 1) + ->addTag('node_access') + ->execute() + ->fetchObject(); + + // Build the last post information. + $last_post = new \stdClass(); + if (!empty($topic->last_comment_timestamp)) { + $last_post->created = $topic->last_comment_timestamp; + $last_post->name = $topic->last_comment_name; + $last_post->uid = $topic->last_comment_uid; + } + + $this->lastPostData[$tid] = $last_post; + return $last_post; + } + + /** + * Provides statistics for a forum. + * + * @param int $tid + * The forum tid. + * + * @return \stdClass|null + * Statistics for the given forum if statistics exist, else NULL. + */ + protected function getForumStatistics($tid) { + if (empty($this->forumStatistics)) { + // Prime the statistics. + $query = $this->connection->select('node_field_data', 'n'); + $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid'); + $query->join('forum', 'f', 'n.vid = f.vid'); + $query->addExpression('COUNT(n.nid)', 'topic_count'); + $query->addExpression('SUM(ncs.comment_count)', 'comment_count'); + $this->forumStatistics = $query + ->fields('f', array('tid')) + ->condition('n.status', 1) + ->condition('n.default_langcode', 1) + ->groupBy('tid') + ->addTag('node_access') + ->execute() + ->fetchAllAssoc('tid'); + } + + if (!empty($this->forumStatistics[$tid])) { + return $this->forumStatistics[$tid]; + } + } + + /** + * {@inheritdoc} + */ + public function getChildren($vid, $tid) { + if (!empty($this->forumChildren[$tid])) { + return $this->forumChildren[$tid]; + } + $forums = array(); + $_forums = taxonomy_get_tree($vid, $tid, NULL, TRUE); + foreach ($_forums as $forum) { + // Merge in the topic and post counters. + if (($count = $this->getForumStatistics($forum->id()))) { + $forum->num_topics = $count->topic_count; + $forum->num_posts = $count->topic_count + $count->comment_count; + } + else { + $forum->num_topics = 0; + $forum->num_posts = 0; + } + + // Merge in last post details. + $forum->last_post = $this->getLastPost($forum->id()); + $forums[$forum->id()] = $forum; + } + + $this->forumChildren[$tid] = $forums; + return $forums; + } + + /** + * {@inheritdoc} + */ + public function getIndex() { + if ($this->index) { + return $this->index; + } + + $vid = $this->configFactory->get('forum.settings')->get('vocabulary'); + $index = $this->entityManager->getStorageController('taxonomy_term')->create(array( + 'tid' => 0, + 'container' => 1, + 'parents' => array(), + 'isIndex' => TRUE, + 'vid' => $vid + )); + + // Load the tree below. + $index->forums = $this->getChildren($vid, 0); + $this->index = $index; + return $index; + } + + /** + * {@inheritdoc} + */ + public function resetCache() { + // Reset the index. + $this->index = NULL; + // Reset history. + $this->history = NULL; + } + + /** + * {@inheritdoc} + */ + public function getParents($tid) { + return taxonomy_term_load_parents_all($tid); + } + + /** + * {@inheritdoc} + */ + public function checkNodeType(NodeInterface $node) { + // Fetch information about the forum field. + $instances = $this->fieldInfo->getBundleInstances('node', $node->bundle()); + return !empty($instances['taxonomy_forums']); + } + + /** + * {@inheritdoc} + */ + public function unreadTopics($term, $uid) { + $query = $this->connection->select('node_field_data', 'n'); + $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $term)); + $query->leftJoin('history', 'h', 'n.nid = h.nid AND h.uid = :uid', array(':uid' => $uid)); + $query->addExpression('COUNT(n.nid)', 'count'); + return $query + ->condition('status', 1) + // @todo This should be actually filtering on the desired node status + // field language and just fall back to the default language. + ->condition('n.default_langcode', 1) + ->condition('n.created', HISTORY_READ_LIMIT, '>') + ->isNull('h.nid') + ->addTag('node_access') + ->execute() + ->fetchField(); + } + + /** + * {@inheritdoc} + */ + public function updateIndex($nid) { + $count = $this->connection->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( + ':nid' => $nid, + ':status' => COMMENT_PUBLISHED, + ))->fetchField(); + + if ($count > 0) { + // Comments exist. + $last_reply = $this->connection->queryRange('SELECT cid, name, created, uid FROM {comment} WHERE nid = :nid AND status = :status ORDER BY cid DESC', 0, 1, array( + ':nid' => $nid, + ':status' => COMMENT_PUBLISHED, + ))->fetchObject(); + $this->connection->update('forum_index') + ->fields( array( + 'comment_count' => $count, + 'last_comment_timestamp' => $last_reply->created, + )) + ->condition('nid', $nid) + ->execute(); + } + else { + // Comments do not exist. + // @todo This should be actually filtering on the desired node language and + // just fall back to the default language. + $node = $this->connection->query('SELECT uid, created FROM {node_field_data} WHERE nid = :nid AND default_langcode = 1', array(':nid' => $nid))->fetchObject(); + $this->connection->update('forum_index') + ->fields( array( + 'comment_count' => 0, + 'last_comment_timestamp' => $node->created, + )) + ->condition('nid', $nid) + ->execute(); + } + } +} diff --git a/core/modules/forum/lib/Drupal/forum/ForumManagerInterface.php b/core/modules/forum/lib/Drupal/forum/ForumManagerInterface.php new file mode 100644 index 0000000..6072d87 --- /dev/null +++ b/core/modules/forum/lib/Drupal/forum/ForumManagerInterface.php @@ -0,0 +1,104 @@ +assertEqual($topics, '6', 'Number of topics found.'); // Verify the number of unread topics. - $unread_topics = _forum_topics_unread($this->forum['tid'], $this->edit_any_topics_user->id()); + $unread_topics = $this->container->get('forum_manager')->unreadTopics($this->forum['tid'], $this->edit_any_topics_user->id()); $unread_topics = format_plural($unread_topics, '1 new post', '@count new posts'); $xpath = $this->buildXPathQuery('//tr[@id=:forum]//td[@class="topics"]//a', $forum_arg); $this->assertFieldByXPath($xpath, $unread_topics, 'Number of unread topics found.'); @@ -417,6 +417,8 @@ function createForum($type, $parent = 0) { $parent_tid = db_query("SELECT t.parent FROM {taxonomy_term_hierarchy} t WHERE t.tid = :tid", array(':tid' => $tid))->fetchField(); $this->assertTrue($parent == $parent_tid, 'The ' . $type . ' is linked to its container'); + $forum = $this->container->get('plugin.manager.entity')->getStorageController('taxonomy_term')->load($tid); + $this->assertEqual(($type == 'forum container'), (bool) $forum->forum_container->value); return $term; } @@ -437,11 +439,6 @@ function deleteForum($tid) { // Assert that the forum no longer exists. $this->drupalGet('forum/' . $tid); $this->assertResponse(404, 'The forum was not found'); - - // Assert that the associated term has been removed from the - // forum_containers variable. - $containers = \Drupal::config('forum.settings')->get('containers'); - $this->assertFalse(in_array($tid, $containers), 'The forum_containers variable has been updated.'); } /** diff --git a/core/modules/forum/tests/Drupal/forum/Tests/ForumManagerTest.php b/core/modules/forum/tests/Drupal/forum/Tests/ForumManagerTest.php new file mode 100644 index 0000000..96675fb --- /dev/null +++ b/core/modules/forum/tests/Drupal/forum/Tests/ForumManagerTest.php @@ -0,0 +1,101 @@ + 'Forum Manager', + 'description' => 'Tests the forum manager functionality.', + 'group' => 'Forum', + ); + } + + /** + * Tests ForumManager::getIndex(). + */ + public function testGetIndex() { + $entity_manager = $this->getMockBuilder('\Drupal\Core\Entity\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + + $storage_controller = $this->getMockBuilder('\Drupal\taxonomy\VocabularyStorageController') + ->disableOriginalConstructor() + ->getMock(); + + $config_factory = $this->getMockBuilder('\Drupal\Core\Config\ConfigFactory') + ->disableOriginalConstructor() + ->getMock(); + + $config = $this->getMockBuilder('\Drupal\Core\Config\Config') + ->disableOriginalConstructor() + ->getMock(); + + $config_factory->expects($this->once()) + ->method('get') + ->will($this->returnValue($config)); + + $config->expects($this->once()) + ->method('get') + ->will($this->returnValue('forums')); + + $entity_manager->expects($this->once()) + ->method('getStorageController') + ->will($this->returnValue($storage_controller)); + + // This is sufficient for testing purposes. + $term = new \stdClass(); + + $storage_controller->expects($this->once()) + ->method('create') + ->will($this->returnValue($term)); + + $connection = $this->getMockBuilder('\Drupal\Core\Database\Connection') + ->disableOriginalConstructor() + ->getMock(); + + $translation_manager = $this->getMockBuilder('\Drupal\Core\StringTranslation\TranslationManager') + ->disableOriginalConstructor() + ->getMock(); + + $field_info = $this->getMockBuilder('\Drupal\field\FieldInfo') + ->disableOriginalConstructor() + ->getMock(); + + $manager = $this->getMock('\Drupal\forum\ForumManager', array('getChildren'), array( + $config_factory, + $entity_manager, + $connection, + $field_info, + $translation_manager, + )); + + $manager->expects($this->once()) + ->method('getChildren') + ->will($this->returnValue(array())); + + // Get the index once. + $index1 = $manager->getIndex(); + + // Get it again. This should not return the previously generated index. If + // it does not, then the test will fail as the mocked methods will be called + // more than once. + $index2 = $manager->getIndex(); + + $this->assertEquals($index1, $index2); + } + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Upgrade/ForumUpgradePathTest.php b/core/modules/system/lib/Drupal/system/Tests/Upgrade/ForumUpgradePathTest.php new file mode 100644 index 0000000..e1a50e4 --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Tests/Upgrade/ForumUpgradePathTest.php @@ -0,0 +1,58 @@ + 'Forum upgrade test', + 'description' => 'Upgrade tests with forum data.', + 'group' => 'Upgrade path', + ); + } + + public function setUp() { + $path = drupal_get_path('module', 'system') . '/tests/upgrade'; + $this->databaseDumpFiles = array( + $path . '/drupal-7.bare.standard_all.database.php.gz', + $path . '/drupal-7.forum.database.php', + ); + parent::setUp(); + } + + /** + * Tests expected forum and container conversions after a successful upgrade. + */ + public function testForumUpgrade() { + $this->assertTrue($this->performUpgrade(), 'The upgrade was completed successfully.'); + + // Make sure the field is created. + $vocabulary = $this->container->get('config.factory')->get('forum.settings')->get('vocabulary'); + $field = field_info_instance('taxonomy_term', 'forum_container', $vocabulary); + $this->assertTrue((bool) $field, 'Field was found'); + + // Check that the values of forum_container are correct. + $containers = entity_load_multiple_by_properties('taxonomy_term', array('name' => 'Container')); + $container = reset($containers); + $this->assertTrue((bool) $container->forum_container->value); + + $forums = entity_load_multiple_by_properties('taxonomy_term', array('name' => 'Forum')); + $forum = reset($forums); + $this->assertFalse((bool) $forum->forum_container->value); + } + +} diff --git a/core/modules/system/tests/upgrade/drupal-7.forum.database.php b/core/modules/system/tests/upgrade/drupal-7.forum.database.php new file mode 100644 index 0000000..b6e6cf8 --- /dev/null +++ b/core/modules/system/tests/upgrade/drupal-7.forum.database.php @@ -0,0 +1,48 @@ +fields('tv', array('vid')) + ->condition('name', 'forums') + ->execute() + ->fetchField(); + +$container = db_insert('taxonomy_term_data') + ->fields(array( + 'vid' => $vocabulary, + 'name' => 'Container', + 'description' => 'Container', + 'format' => 'full_html', + 'weight' => 0, + )) + ->execute(); + +$forum = db_insert('taxonomy_term_data') + ->fields(array( + 'vid' => $vocabulary, + 'name' => 'Forum', + 'description' => 'Forum', + 'format' => 'full_html', + 'weight' => 0, + )) + ->execute(); + +db_delete('variable') + ->condition('name', 'forum_containers') + ->execute(); + +db_insert('variable')->fields(array( + 'name' => 'forum_containers', + 'value' => serialize(array($container)), +)) +->execute();