first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
name: Forum
type: module
description: 'Provides discussion forums.'
dependencies:
- drupal:node
- drupal:history
- drupal:taxonomy
- drupal:comment
- drupal:options
package: Core
# version: VERSION
configure: forum.overview
lifecycle: deprecated
lifecycle_link: "https://www.drupal.org/node/3223395#s-forum"
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

244
core/modules/forum/forum.install Executable file
View File

@@ -0,0 +1,244 @@
<?php
/**
* @file
* Install, update, and uninstall functions for the Forum module.
*/
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\taxonomy\Entity\Term;
/**
* Implements hook_install().
*/
function forum_install($is_syncing) {
// Set the weight of the forum.module to 1 so it is loaded after the taxonomy.module.
module_set_weight('forum', 1);
// Do not allow to delete the forum's node type machine name.
$locked = \Drupal::state()->get('node.type.locked');
$locked['forum'] = 'forum';
\Drupal::state()->set('node.type.locked', $locked);
if (!$is_syncing) {
// Create a default forum so forum posts can be created.
$term = Term::create([
'name' => t('General discussion'),
'description' => '',
'parent' => [0],
'vid' => 'forums',
'forum_container' => 0,
]);
$term->save();
}
}
/**
* Implements hook_uninstall().
*/
function forum_uninstall() {
if ($field_storage = FieldStorageConfig::loadByName('node', 'taxonomy_forums')) {
$field_storage->delete();
}
if ($field_storage = FieldStorageConfig::loadByName('node', 'comment_forum')) {
$field_storage->delete();
}
if ($field_storage = FieldStorageConfig::loadByName('taxonomy_term', 'forum_container')) {
$field_storage->delete();
}
// Purge field data now to allow taxonomy and options module to be uninstalled
// if this is the only field remaining.
field_purge_batch(10);
// Allow to delete a forum's node type.
$locked = \Drupal::state()->get('node.type.locked');
unset($locked['forum']);
\Drupal::state()->set('node.type.locked', $locked);
}
/**
* Implements hook_schema().
*/
function forum_schema() {
$schema['forum'] = [
'description' => 'Stores the relationship of nodes to forum terms.',
'fields' => [
'nid' => [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'The {node}.nid of the node.',
],
'vid' => [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'Primary Key: The {node}.vid of the node.',
],
'tid' => [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'The {taxonomy_term_data}.tid of the forum term assigned to the node.',
],
],
'indexes' => [
'forum_topic' => ['nid', 'tid'],
'tid' => ['tid'],
],
'primary key' => ['vid'],
'foreign keys' => [
'forum_node' => [
'table' => 'node',
'columns' => [
'nid' => 'nid',
'vid' => 'vid',
],
],
],
];
$schema['forum_index'] = [
'description' => 'Maintains denormalized information about node/term relationships.',
'fields' => [
'nid' => [
'description' => 'The {node}.nid this record tracks.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'title' => [
'description' => 'The node title.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
],
'tid' => [
'description' => 'The term ID.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
],
'sticky' => [
'description' => 'Boolean indicating whether the node is sticky.',
'type' => 'int',
'not null' => FALSE,
'default' => 0,
'size' => 'tiny',
],
'created' => [
'description' => 'The Unix timestamp when the node was created.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'size' => 'big',
],
'last_comment_timestamp' => [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'The Unix timestamp of the last comment that was posted within this node, from {comment}.timestamp.',
'size' => 'big',
],
'comment_count' => [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'description' => 'The total number of comments on this node.',
],
],
'indexes' => [
'forum_topics' => ['nid', 'tid', 'sticky', 'last_comment_timestamp'],
'created' => ['created'],
'last_comment_timestamp' => ['last_comment_timestamp'],
],
'primary key' => ['nid', 'tid'],
'foreign keys' => [
'tracked_node' => [
'table' => 'node',
'columns' => ['nid' => 'nid'],
],
'term' => [
'table' => 'taxonomy_term_data',
'columns' => [
'tid' => 'tid',
],
],
],
];
return $schema;
}
/**
* Remove the year 2038 date limitation.
*/
function forum_update_10100(&$sandbox = NULL) {
$connection = \Drupal::database();
if ($connection->schema()->tableExists('forum_index') && $connection->databaseType() != 'sqlite') {
$new = [
'description' => 'The Unix timestamp when the node was created.',
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
'size' => 'big',
];
$connection->schema()->changeField('forum_index', 'created', 'created', $new);
$new = [
'type' => 'int',
'not null' => TRUE,
'default' => 0,
'description' => 'The Unix timestamp of the last comment that was posted within this node, from {comment}.timestamp.',
'size' => 'big',
];
$connection->schema()->changeField('forum_index', 'last_comment_timestamp', 'last_comment_timestamp', $new);
}
}
/**
* Repopulate the forum index table.
*/
function forum_update_10101(&$sandbox = NULL): PluralTranslatableMarkup {
$query = \Drupal::database()->select('forum_index', 'fi')
->fields('fi', ['nid', 'tid'])
->groupBy('nid')
->groupBy('tid');
$query->addExpression('count(*)', 'count');
$query->having('count(*) > 1');
$results = $query->execute();
$nids_to_rebuild = [];
foreach ($results as $row) {
\Drupal::database()->delete('forum_index')->condition('tid', $row->tid)->condition('nid', $row->nid)->execute();
$nids_to_rebuild[] = $row->nid;
}
\Drupal::state()->set('forum_update_10101_nids', $nids_to_rebuild);
return new PluralTranslatableMarkup(count($nids_to_rebuild), 'Removed 1 duplicate entry from forum_index', 'Removed @count duplicate entries from forum_index');
}
/**
* Add a primary key to forum_index.
*/
function forum_update_10102(&$sandbox = NULL) {
$connection = \Drupal::database();
if ($connection->schema()->tableExists('forum_index')) {
// Data in this table could have duplicates. The data can be re-constructed
// from other data in the site. To avoid duplicate key errors we delete any
// rows that are duplicates and then recreate them in a post-update hook.
// @see \forum_post_update_recreate_forum_index_rows().
$connection->schema()->addPrimaryKey('forum_index', ['nid', 'tid']);
return \t('Added primary key to the forum_index table.');
}
return \t('Index already exists');
}

View File

@@ -0,0 +1,11 @@
forum_add_forum_local_action:
route_name: forum.add_forum
title: 'Add forum'
appears_on:
- forum.overview
forum_add_container_local_action:
route_name: forum.add_container
title: 'Add container'
appears_on:
- forum.overview

View File

@@ -0,0 +1,9 @@
forum.index:
title: Forums
route_name: forum.index
menu_name: tools
forum.overview:
title: Forums
parent: system.admin_structure
description: 'Control forum hierarchy settings.'
route_name: forum.overview

View File

@@ -0,0 +1,9 @@
forum.overview:
route_name: forum.overview
base_route: forum.overview
title: List
forum.settings:
route_name: forum.settings
base_route: forum.overview
title: Settings
weight: 100

760
core/modules/forum/forum.module Executable file
View File

@@ -0,0 +1,760 @@
<?php
/**
* @file
* Provides discussion forums.
*/
use Drupal\comment\CommentInterface;
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\language\Plugin\migrate\source\d7\LanguageContentSettingsTaxonomyVocabulary as D7LanguageContentSettingsTaxonomyVocabulary;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\taxonomy\Plugin\migrate\source\d6\Term as D6Term;
use Drupal\taxonomy\Plugin\migrate\source\d6\Vocabulary as D6Vocabulary;
use Drupal\taxonomy\Plugin\migrate\source\d6\VocabularyPerType as D6VocabularyPerType;
use Drupal\taxonomy\Plugin\migrate\source\d7\Term as D7Term;
use Drupal\taxonomy\Plugin\migrate\source\d7\TermEntityTranslation;
use Drupal\taxonomy\Plugin\migrate\source\d7\Vocabulary as D7Vocabulary;
use Drupal\taxonomy\Plugin\migrate\source\d7\VocabularyTranslation as D7VocabularyTranslation;
use Drupal\taxonomy\VocabularyInterface;
use Drupal\user\Entity\User;
/**
* Implements hook_help().
*/
function forum_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.forum':
$output = '';
$output .= '<h2>' . t('About') . '</h2>';
$output .= '<p>' . t('The Forum module lets you create threaded discussion forums with functionality similar to other message board systems. In a forum, users post topics and threads in nested hierarchies, allowing discussions to be categorized and grouped.') . '</p>';
$output .= '<p>' . t('The Forum module adds and uses a content type called <em>Forum topic</em>. For background information on content types, see the <a href=":node_help">Node module help page</a>.', [':node_help' => Url::fromRoute('help.page', ['name' => 'node'])->toString()]) . '</p>';
$output .= '<p>' . t('A forum is represented by a hierarchical structure, consisting of:');
$output .= '<ul>';
$output .= '<li>' . t('<em>Forums</em> (for example, <em>Recipes for cooking vegetables</em>)') . '</li>';
$output .= '<li>' . t('<em>Forum topics</em> submitted by users (for example, <em>How to cook potatoes</em>), which start discussions.') . '</li>';
$output .= '<li>' . t('Threaded <em>comments</em> submitted by users (for example, <em>You wash the potatoes first and then...</em>).') . '</li>';
$output .= '<li>' . t('Optional <em>containers</em>, used to group similar forums. Forums can be placed inside containers, and vice versa.') . '</li>';
$output .= '</ul>';
$output .= '</p>';
$output .= '<p>' . t('For more information, see the <a href=":forum">online documentation for the Forum module</a>.', [':forum' => 'https://www.drupal.org/documentation/modules/forum']) . '</p>';
$output .= '<h2>' . t('Uses') . '</h2>';
$output .= '<dl>';
$output .= '<dt>' . t('Setting up the forum structure') . '</dt>';
$output .= '<dd>' . t('Visit the <a href=":forums">Forums page</a> to set up containers and forums to hold your discussion topics.', [':forums' => Url::fromRoute('forum.overview')->toString()]) . '</dd>';
$output .= '<dt>' . t('Starting a discussion') . '</dt>';
$output .= '<dd>' . t('The <a href=":create-topic">Forum topic</a> link on the <a href=":content-add">Add content</a> page creates the first post of a new threaded discussion, or thread.', [':create-topic' => Url::fromRoute('node.add', ['node_type' => 'forum'])->toString(), ':content-add' => Url::fromRoute('node.add_page')->toString()]) . '</dd>';
$output .= '<dt>' . t('Navigating in the forum') . '</dt>';
$output .= '<dd>' . t('Installing the Forum module provides a default <em>Forums</em> menu link in the Tools menu that links to the <a href=":forums">Forums page</a>.', [':forums' => Url::fromRoute('forum.index')->toString()]) . '</dd>';
$output .= '<dt>' . t('Moving forum topics') . '</dt>';
$output .= '<dd>' . t('A forum topic (and all of its comments) may be moved between forums by selecting a different forum while editing a forum topic. When moving a forum topic between forums, the <em>Leave shadow copy</em> option creates a link in the original forum pointing to the new location.') . '</dd>';
$output .= '<dt>' . t('Locking and disabling comments') . '</dt>';
$output .= '<dd>' . t('Selecting <em>Closed</em> under <em>Comment settings</em> while editing a forum topic will lock (prevent new comments on) the thread. Selecting <em>Hidden</em> under <em>Comment settings</em> while editing a forum topic will hide all existing comments on the thread, and prevent new ones.') . '</dd>';
$output .= '</dl>';
return $output;
case 'forum.overview':
$output = '<p>' . t('Forums contain forum topics. Use containers to group related forums.') . '</p>';
$more_help_link = [
'#type' => 'link',
'#url' => Url::fromRoute('help.page', ['name' => 'forum']),
'#title' => t('More help'),
'#attributes' => [
'class' => ['icon-help'],
],
];
$container = [
'#theme' => 'container',
'#children' => $more_help_link,
'#attributes' => [
'class' => ['more-link'],
],
];
$output .= \Drupal::service('renderer')->renderInIsolation($container);
return $output;
case 'forum.add_container':
return '<p>' . t('Use containers to group related forums.') . '</p>';
case 'forum.add_forum':
return '<p>' . t('A forum holds related forum topics.') . '</p>';
case 'forum.settings':
return '<p>' . t('Adjust the display of your forum topics. Organize the forums on the <a href=":forum-structure">forum structure page</a>.', [':forum-structure' => Url::fromRoute('forum.overview')->toString()]) . '</p>';
}
}
/**
* Implements hook_theme().
*/
function forum_theme() {
return [
'forums' => [
'variables' => ['forums' => [], 'topics' => [], 'topics_pager' => [], 'parents' => NULL, 'term' => NULL, 'sortby' => NULL, 'forum_per_page' => NULL, 'header' => []],
],
'forum_list' => [
'variables' => ['forums' => NULL, 'parents' => NULL, 'tid' => NULL],
],
'forum_icon' => [
'variables' => ['new_posts' => NULL, 'num_posts' => 0, 'comment_mode' => 0, 'sticky' => 0, 'first_new' => FALSE],
],
'forum_submitted' => [
'variables' => ['topic' => NULL],
],
'forum_topic' => [
'variables' => ['title_link' => NULL, 'submitted' => NULL],
],
];
}
/**
* Implements hook_entity_type_build().
*/
function forum_entity_type_build(array &$entity_types) {
/** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
// Register forum specific forms.
$entity_types['taxonomy_term']
->setFormClass('forum', 'Drupal\forum\Form\ForumForm')
->setFormClass('container', 'Drupal\forum\Form\ContainerForm')
->setLinkTemplate('forum-edit-container-form', '/admin/structure/forum/edit/container/{taxonomy_term}')
->setLinkTemplate('forum-delete-form', '/admin/structure/forum/delete/forum/{taxonomy_term}')
->setLinkTemplate('forum-edit-form', '/admin/structure/forum/edit/forum/{taxonomy_term}');
}
/**
* Implements hook_entity_bundle_info_alter().
*/
function forum_entity_bundle_info_alter(&$bundles) {
// Take over URI construction for taxonomy terms that are forums.
if ($vid = \Drupal::config('forum.settings')->get('vocabulary')) {
if (isset($bundles['taxonomy_term'][$vid])) {
$bundles['taxonomy_term'][$vid]['uri_callback'] = 'forum_uri';
}
}
}
/**
* Entity URI callback used in forum_entity_bundle_info_alter().
*/
function forum_uri($forum) {
return Url::fromRoute('forum.page', ['taxonomy_term' => $forum->id()]);
}
/**
* Implements hook_entity_bundle_field_info_alter().
*/
function forum_entity_bundle_field_info_alter(&$fields, EntityTypeInterface $entity_type, $bundle) {
if ($entity_type->id() == 'node' && !empty($fields['taxonomy_forums'])) {
$fields['taxonomy_forums']->addConstraint('ForumLeaf', []);
}
}
/**
* Implements hook_ENTITY_TYPE_presave() for node entities.
*
* Assigns the forum taxonomy when adding a topic from within a forum.
*/
function forum_node_presave(EntityInterface $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()) {
$node->forum_tid = $node->taxonomy_forums->target_id;
// Only do a shadow copy check if this is not a new node.
if (!$node->isNew()) {
$old_tid = \Drupal::service('forum.index_storage')->getOriginalTermId($node);
if ($old_tid && isset($node->forum_tid) && ($node->forum_tid != $old_tid) && !empty($node->shadow)) {
// A shadow copy needs to be created. Retain new term and add old term.
$node->taxonomy_forums[count($node->taxonomy_forums)] = ['target_id' => $old_tid];
}
}
}
}
}
/**
* Implements hook_ENTITY_TYPE_update() for node entities.
*/
function forum_node_update(EntityInterface $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.
/** @var \Drupal\forum\ForumIndexStorageInterface $forum_index_storage */
$forum_index_storage = \Drupal::service('forum.index_storage');
if ($node->getRevisionId() == $node->original->getRevisionId() && $forum_index_storage->getOriginalTermId($node)) {
if (!empty($node->forum_tid)) {
$forum_index_storage->update($node);
}
// The node is removed from the forum.
else {
$forum_index_storage->delete($node);
}
}
else {
if (!empty($node->forum_tid)) {
$forum_index_storage->create($node);
}
}
// If the node has a shadow forum topic, update the record for this
// revision.
if (!empty($node->shadow)) {
$forum_index_storage->deleteRevision($node);
$forum_index_storage->create($node);
}
// If the node is published, update the forum index.
if ($node->isPublished()) {
$forum_index_storage->deleteIndex($node);
$forum_index_storage->createIndex($node);
}
// When a forum node is unpublished, remove it from the forum_index table.
else {
$forum_index_storage->deleteIndex($node);
}
}
}
/**
* Implements hook_ENTITY_TYPE_insert() for node entities.
*/
function forum_node_insert(EntityInterface $node) {
if (\Drupal::service('forum_manager')->checkNodeType($node)) {
/** @var \Drupal\forum\ForumIndexStorageInterface $forum_index_storage */
$forum_index_storage = \Drupal::service('forum.index_storage');
if (!empty($node->forum_tid)) {
$forum_index_storage->create($node);
}
// If the node is published, update the forum index.
if ($node->isPublished()) {
$forum_index_storage->createIndex($node);
}
}
}
/**
* Implements hook_ENTITY_TYPE_predelete() for node entities.
*/
function forum_node_predelete(EntityInterface $node) {
if (\Drupal::service('forum_manager')->checkNodeType($node)) {
/** @var \Drupal\forum\ForumIndexStorageInterface $forum_index_storage */
$forum_index_storage = \Drupal::service('forum.index_storage');
$forum_index_storage->delete($node);
$forum_index_storage->deleteIndex($node);
}
}
/**
* Implements hook_ENTITY_TYPE_storage_load() for node entities.
*/
function forum_node_storage_load($nodes) {
$node_vids = [];
foreach ($nodes as $node) {
if (\Drupal::service('forum_manager')->checkNodeType($node)) {
$node_vids[] = $node->getRevisionId();
}
}
if (!empty($node_vids)) {
$result = \Drupal::service('forum.index_storage')->read($node_vids);
foreach ($result as $record) {
$nodes[$record->nid]->forum_tid = $record->tid;
}
}
}
/**
* Implements hook_ENTITY_TYPE_update() for comment entities.
*/
function forum_comment_update(CommentInterface $comment) {
if ($comment->getCommentedEntityTypeId() == 'node') {
\Drupal::service('forum.index_storage')->updateIndex($comment->getCommentedEntity());
}
}
/**
* Implements hook_ENTITY_TYPE_insert() for comment entities.
*/
function forum_comment_insert(CommentInterface $comment) {
if ($comment->getCommentedEntityTypeId() == 'node') {
\Drupal::service('forum.index_storage')->updateIndex($comment->getCommentedEntity());
}
}
/**
* Implements hook_ENTITY_TYPE_delete() for comment entities.
*/
function forum_comment_delete(CommentInterface $comment) {
if ($comment->getCommentedEntityTypeId() == 'node') {
\Drupal::service('forum.index_storage')->updateIndex($comment->getCommentedEntity());
}
}
/**
* Implements hook_form_BASE_FORM_ID_alter() for \Drupal\taxonomy\VocabularyForm.
*/
function forum_form_taxonomy_vocabulary_form_alter(&$form, FormStateInterface $form_state, $form_id) {
$vid = \Drupal::config('forum.settings')->get('vocabulary');
$vocabulary = $form_state->getFormObject()->getEntity();
if ($vid == $vocabulary->id()) {
$form['help_forum_vocab'] = [
'#markup' => t('This is the designated forum vocabulary. Some of the normal vocabulary options have been removed.'),
'#weight' => -1,
];
// Forum's vocabulary always has single hierarchy. Forums and containers
// have only one parent or no parent for root items. By default this value
// is 0.
$form['hierarchy']['#value'] = VocabularyInterface::HIERARCHY_SINGLE;
// Do not allow to delete forum's vocabulary.
$form['actions']['delete']['#access'] = FALSE;
// Do not allow to change a vid of forum's vocabulary.
$form['vid']['#disabled'] = TRUE;
}
}
/**
* Implements hook_form_FORM_ID_alter() for \Drupal\taxonomy\TermForm.
*/
function forum_form_taxonomy_term_form_alter(&$form, FormStateInterface $form_state, $form_id) {
$vid = \Drupal::config('forum.settings')->get('vocabulary');
if (isset($form['vid']['#value']) && $form['vid']['#value'] == $vid) {
// Hide multiple parents select from forum terms.
$form['relations']['parent']['#access'] = FALSE;
}
}
/**
* Implements hook_form_BASE_FORM_ID_alter() for \Drupal\node\NodeForm.
*/
function forum_form_node_form_alter(&$form, FormStateInterface $form_state, $form_id) {
$node = $form_state->getFormObject()->getEntity();
if (isset($node->taxonomy_forums) && !$node->isNew()) {
$forum_terms = $node->taxonomy_forums;
// If editing, give option to leave shadows.
$shadow = (count($forum_terms) > 1);
$form['shadow'] = [
'#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.'),
];
$form['forum_tid'] = ['#type' => 'value', '#value' => $node->forum_tid];
}
if (isset($form['taxonomy_forums'])) {
$widget =& $form['taxonomy_forums']['widget'];
$widget['#multiple'] = FALSE;
if (empty($widget['#default_value'])) {
// If there is no default forum already selected, try to get the forum
// ID from the URL (e.g., if we are on a page like node/add/forum/2, we
// expect "2" to be the ID of the forum that was requested).
$requested_forum_id = \Drupal::request()->query->get('forum_id');
$widget['#default_value'] = is_numeric($requested_forum_id) ? $requested_forum_id : '';
}
}
}
/**
* Implements hook_preprocess_HOOK() for block templates.
*/
function forum_preprocess_block(&$variables) {
if ($variables['configuration']['provider'] == 'forum') {
$variables['attributes']['role'] = 'navigation';
}
}
/**
* Implements hook_theme_suggestions_HOOK().
*/
function forum_theme_suggestions_forums(array $variables) {
$suggestions = [];
$tid = $variables['term']->id();
// Provide separate template suggestions based on what's being output. Topic
// ID is also accounted for. Check both variables to be safe then the inverse.
// Forums with topic IDs take precedence.
if ($variables['forums'] && !$variables['topics']) {
$suggestions[] = 'forums__containers';
$suggestions[] = 'forums__' . $tid;
$suggestions[] = 'forums__containers__' . $tid;
}
elseif (!$variables['forums'] && $variables['topics']) {
$suggestions[] = 'forums__topics';
$suggestions[] = 'forums__' . $tid;
$suggestions[] = 'forums__topics__' . $tid;
}
else {
$suggestions[] = 'forums__' . $tid;
}
return $suggestions;
}
/**
* Prepares variables for forums templates.
*
* Default template: forums.html.twig.
*
* @param array $variables
* An array containing the following elements:
* - forums: An array of all forum objects to display for the given taxonomy
* term ID. If tid = 0 then all the top-level forums are displayed.
* - 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.
* - 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.
* - 3: Posts with the most comments first.
* - 4: Posts with the least comments first.
* - 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'] = [
'#theme' => 'forum_list',
'#forums' => $variables['forums'],
'#parents' => $variables['parents'],
'#tid' => $variables['tid'],
];
}
if ($variables['term'] && empty($variables['term']->forum_container->value) && !empty($variables['topics'])) {
$forum_topic_list_header = $variables['header'];
$table = [
'#theme' => 'table__forum_topic_list',
'#responsive' => FALSE,
'#attributes' => ['id' => 'forum-topic-' . $variables['tid']],
'#header' => [],
'#rows' => [],
];
if (!empty($forum_topic_list_header)) {
$table['#header'] = $forum_topic_list_header;
}
/** @var \Drupal\node\NodeInterface $topic */
foreach ($variables['topics'] as $id => $topic) {
$variables['topics'][$id]->icon = [
'#theme' => 'forum_icon',
'#new_posts' => $topic->new,
'#num_posts' => $topic->comment_count,
'#comment_mode' => $topic->comment_mode,
'#sticky' => $topic->isSticky(),
'#first_new' => $topic->first_new,
];
// We keep the actual tid in forum table, if it's different from the
// current tid then it means the topic appears in two forums, one of
// them is a shadow copy.
if ($variables['tid'] != $topic->forum_tid) {
$variables['topics'][$id]->moved = TRUE;
$variables['topics'][$id]->title = $topic->getTitle();
$variables['topics'][$id]->message = Link::fromTextAndUrl(t('This topic has been moved'), Url::fromRoute('forum.page', ['taxonomy_term' => $topic->forum_tid]))->toString();
}
else {
$variables['topics'][$id]->moved = FALSE;
$variables['topics'][$id]->title_link = Link::fromTextAndUrl($topic->getTitle(), $topic->toUrl())->toString();
$variables['topics'][$id]->message = '';
}
$forum_submitted = [
'#theme' => 'forum_submitted',
'#topic' => (object) [
'uid' => $topic->getOwnerId(),
'name' => $topic->getOwner()->getDisplayName(),
'created' => $topic->getCreatedTime(),
],
];
$variables['topics'][$id]->submitted = \Drupal::service('renderer')->render($forum_submitted);
$forum_submitted = [
'#theme' => 'forum_submitted',
'#topic' => $topic->last_reply ?? NULL,
];
$variables['topics'][$id]->last_reply = \Drupal::service('renderer')->render($forum_submitted);
$variables['topics'][$id]->new_text = '';
$variables['topics'][$id]->new_url = '';
if ($topic->new_replies) {
$page_number = \Drupal::entityTypeManager()->getStorage('comment')
->getNewCommentPageNumber($topic->comment_count, $topic->new_replies, $topic, 'comment_forum');
$query = $page_number ? ['page' => $page_number] : NULL;
$variables['topics'][$id]->new_text = \Drupal::translation()->formatPlural($topic->new_replies, '1 new post<span class="visually-hidden"> in topic %title</span>', '@count new posts<span class="visually-hidden"> in topic %title</span>', ['%title' => $variables['topics'][$id]->label()]);
$variables['topics'][$id]->new_url = Url::fromRoute('entity.node.canonical', ['node' => $topic->id()], ['query' => $query, 'fragment' => 'new'])->toString();
}
// Build table rows from topics.
$row = [];
$row[] = [
'data' => [
$topic->icon,
[
'#theme' => 'forum_topic',
'#title_link' => $topic->title_link,
'#submitted' => $topic->submitted,
],
],
'class' => ['forum__topic'],
];
if ($topic->moved) {
$row[] = [
'data' => $topic->message,
'colspan' => '2',
];
}
else {
$new_replies = '';
if ($topic->new_replies) {
$new_replies = '<br /><a href="' . $topic->new_url . '">' . $topic->new_text . '</a>';
}
$row[] = [
'data' => [
[
'#prefix' => $topic->comment_count,
'#markup' => $new_replies,
],
],
'class' => ['forum__replies'],
];
$row[] = [
'data' => $topic->last_reply,
'class' => ['forum__last-reply'],
];
}
$table['#rows'][] = $row;
}
$variables['topics_original'] = $variables['topics'];
$variables['topics'] = $table;
$variables['topics_pager'] = [
'#type' => 'pager',
];
}
}
}
/**
* Prepares variables for forum list templates.
*
* Default template: forum-list.html.twig.
*
* @param array $variables
* An array containing the following elements:
* - forums: An array of all forum objects to display for the given taxonomy
* term ID. If tid = 0 then all the top-level forums are displayed.
* - parents: An array of taxonomy term objects that are ancestors of the
* current term ID.
* - tid: Taxonomy term ID of the current forum.
*/
function template_preprocess_forum_list(&$variables) {
$user = \Drupal::currentUser();
$row = 0;
// Sanitize each forum so that the template can safely print the data.
foreach ($variables['forums'] as $id => $forum) {
$variables['forums'][$id]->description = ['#markup' => $forum->description->value];
$variables['forums'][$id]->link = forum_uri($forum);
$variables['forums'][$id]->name = $forum->label();
$variables['forums'][$id]->is_container = !empty($forum->forum_container->value);
$variables['forums'][$id]->zebra = $row % 2 == 0 ? 'odd' : 'even';
$row++;
$variables['forums'][$id]->new_text = '';
$variables['forums'][$id]->new_url = '';
$variables['forums'][$id]->new_topics = 0;
$variables['forums'][$id]->old_topics = $forum->num_topics;
$variables['forums'][$id]->icon_class = 'default';
$variables['forums'][$id]->icon_title = t('No new posts');
if ($user->isAuthenticated()) {
$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 = \Drupal::translation()->formatPlural($variables['forums'][$id]->new_topics, '1 new post<span class="visually-hidden"> in forum %title</span>', '@count new posts<span class="visually-hidden"> in forum %title</span>', ['%title' => $variables['forums'][$id]->label()]);
$variables['forums'][$id]->new_url = Url::fromRoute('forum.page', ['taxonomy_term' => $forum->id()], ['fragment' => 'new'])->toString();
$variables['forums'][$id]->icon_class = 'new';
$variables['forums'][$id]->icon_title = t('New posts');
}
$variables['forums'][$id]->old_topics = $forum->num_topics - $variables['forums'][$id]->new_topics;
}
$forum_submitted = ['#theme' => 'forum_submitted', '#topic' => $forum->last_post];
$variables['forums'][$id]->last_reply = \Drupal::service('renderer')->render($forum_submitted);
}
$variables['pager'] = [
'#type' => 'pager',
];
// Give meaning to $tid for themers. $tid actually stands for term ID.
$variables['forum_id'] = $variables['tid'];
unset($variables['tid']);
}
/**
* Prepares variables for forum icon templates.
*
* Default template: forum-icon.html.twig.
*
* @param array $variables
* An array containing the following elements:
* - new_posts: Indicates whether or not the topic contains new posts.
* - num_posts: The total number of posts in all topics.
* - comment_mode: An integer indicating whether comments are open, closed,
* or hidden.
* - sticky: Indicates whether the topic is sticky.
* - first_new: Indicates whether this is the first topic with new posts.
*/
function template_preprocess_forum_icon(&$variables) {
$variables['hot_threshold'] = \Drupal::config('forum.settings')->get('topics.hot_threshold');
if ($variables['num_posts'] > $variables['hot_threshold']) {
$variables['icon_status'] = $variables['new_posts'] ? 'hot-new' : 'hot';
$variables['icon_title'] = $variables['new_posts'] ? t('Hot topic, new comments') : t('Hot topic');
}
else {
$variables['icon_status'] = $variables['new_posts'] ? 'new' : 'default';
$variables['icon_title'] = $variables['new_posts'] ? t('New comments') : t('Normal topic');
}
if ($variables['comment_mode'] == CommentItemInterface::CLOSED || $variables['comment_mode'] == CommentItemInterface::HIDDEN) {
$variables['icon_status'] = 'closed';
$variables['icon_title'] = t('Closed topic');
}
if ($variables['sticky'] == 1) {
$variables['icon_status'] = 'sticky';
$variables['icon_title'] = t('Sticky topic');
}
$variables['attributes']['title'] = $variables['icon_title'];
}
/**
* Prepares variables for forum submission information templates.
*
* The submission information will be displayed in the forum list and topic
* list.
*
* Default template: forum-submitted.html.twig.
*
* @param array $variables
* An array containing the following elements:
* - topic: The topic object.
*/
function template_preprocess_forum_submitted(&$variables) {
$variables['author'] = '';
if (isset($variables['topic']->uid)) {
$username = ['#theme' => 'username', '#account' => User::load($variables['topic']->uid)];
$variables['author'] = \Drupal::service('renderer')->render($username);
}
$variables['time'] = isset($variables['topic']->created) ? \Drupal::service('date.formatter')->formatTimeDiffSince($variables['topic']->created) : '';
}
/**
* Implements hook_migrate_prepare_row().
*/
function forum_migrate_prepare_row(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) {
$source_plugin = $migration->getSourcePlugin();
if (is_a($source_plugin, D6Term::class) || is_a($source_plugin, D7Term::class) || is_a($source_plugin, TermEntityTranslation::class)) {
$connection = $source_plugin->getDatabase();
if ($connection) {
if ($connection->schema()->tableExists('variable')) {
$query = $connection->select('variable', 'v')
->fields('v', ['value'])
->condition('name', 'forum_containers');
$result = $query->execute()->fetchCol();
if ($result) {
$forum_container_tids = unserialize($result[0], ['allowed_classes' => FALSE]);
$current_tid = $row->getSourceProperty('tid');
$row->setSourceProperty('is_container', in_array($current_tid, $forum_container_tids));
}
}
}
}
}
/**
* Implements hook_migrate_MIGRATION_ID_prepare_row().
*/
function forum_migrate_d7_taxonomy_vocabulary_prepare_row(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) {
// If the vocabulary being migrated is the one defined in the
// 'forum_nav_vocabulary' variable, set the 'forum_vocabulary' source
// property to true so we know this is the vocabulary used by Forum.
$connection = $migration->getSourcePlugin()->getDatabase();
if ($connection) {
if ($connection->schema()->tableExists('variable')) {
$query = $connection->select('variable', 'v')
->fields('v', ['value'])
->condition('name', 'forum_nav_vocabulary');
$result = $query->execute()->fetchCol();
if ($result) {
$forum_nav_vocabulary = unserialize($result[0], ['allowed_classes' => FALSE]);
if ($forum_nav_vocabulary == $row->getSourceProperty('vid')) {
$row->setSourceProperty('forum_vocabulary', TRUE);
}
}
}
}
}
/**
* Implements hook_migration_plugins_alter().
*/
function forum_migration_plugins_alter(array &$migrations) {
// Function to append the forum_vocabulary process plugin.
$merge_forum_vocabulary = function ($process) {
$process[] = [
'plugin' => 'forum_vocabulary',
'machine_name' => 'forums',
];
return $process;
};
$merge_forum_field_name = function ($process) {
$process[] = [
'plugin' => 'forum_vocabulary',
'machine_name' => 'taxonomy_forums',
];
return $process;
};
foreach ($migrations as $migration_id => $migration) {
// Add process for forum_nav_vocabulary.
/** @var \Drupal\migrate\Plugin\migrate\source\SqlBase $source_plugin */
$source_plugin = \Drupal::service('plugin.manager.migration')
->createStubMigration($migration)
->getSourcePlugin();
if (is_a($source_plugin, D6Vocabulary::class)
|| is_a($source_plugin, D6VocabularyPerType::class)) {
if (isset($migration['process']['vid'])) {
$migrations[$migration_id]['process']['vid'] = $merge_forum_vocabulary($migration['process']['vid']);
}
if (isset($migration['process']['field_name'])) {
$migrations[$migration_id]['process']['field_name'] = $merge_forum_field_name($migration['process']['field_name']);
}
}
if (is_a($source_plugin, D7Vocabulary::class)
&& !is_a($source_plugin, D7VocabularyTranslation::class)
&& !is_a($source_plugin, D7LanguageContentSettingsTaxonomyVocabulary::class)) {
if (isset($migration['process']['vid'])) {
$process[] = $migrations[$migration_id]['process']['vid'];
$migrations[$migration_id]['process']['vid'] = $merge_forum_vocabulary($process);
}
}
// Add process for forum_container.
if (is_a($source_plugin, D6Term::class)
|| is_a($source_plugin, D7Term::class)
|| is_a($source_plugin, TermEntityTranslation::class)) {
$migrations[$migration_id]['process']['forum_container'] = 'is_container';
}
}
}

View File

@@ -0,0 +1,2 @@
administer forums:
title: 'Administer forums'

View File

@@ -0,0 +1,61 @@
<?php
/**
* @file
* Contains post update functions.
*/
use Drupal\Core\Site\Settings;
use Drupal\Core\StringTranslation\PluralTranslatableMarkup;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\node\NodeInterface;
/**
* Repopulate the forum index table.
*/
function forum_post_update_recreate_forum_index_rows(&$sandbox = NULL): TranslatableMarkup {
$entityStorage = \Drupal::entityTypeManager()->getStorage('node');
if (!isset($sandbox['ids'])) {
// This must be the first run. Initialize the sandbox.
$sandbox['ids'] = \Drupal::state()->get('forum_update_10101_nids', []);
$sandbox['max'] = count($sandbox['ids']);
}
$ids = array_splice($sandbox['ids'], 0, (int) Settings::get('entity_update_batch_size', 50));
$insert = \Drupal::database()->insert('forum_index')->fields([
'nid',
'title',
'tid',
'sticky',
'created',
'last_comment_timestamp',
'comment_count',
]);
$do_insert = FALSE;
foreach ($entityStorage->loadMultiple($ids) as $entity) {
$do_insert = TRUE;
assert($entity instanceof NodeInterface);
$insert->values([
$entity->id(),
$entity->label(),
$entity->taxonomy_forums->target_id,
(int) $entity->isSticky(),
$entity->getCreatedTime(),
$entity->comment_forum->last_comment_timestamp,
$entity->comment_forum->comment_count,
]);
}
if ($do_insert) {
$insert->execute();
}
$sandbox['#finished'] = empty($sandbox['max']) || empty($sandbox['ids']) ? 1 : ($sandbox['max'] - count($sandbox['ids'])) / $sandbox['max'];
if ($sandbox['#finished'] === 1) {
\Drupal::state()->delete('forum_update_10101_nids');
return new TranslatableMarkup('Finished updating forum index rows.');
}
return new PluralTranslatableMarkup($sandbox['max'] - count($sandbox['ids']),
'Processed @count entry of @total.',
'Processed @count entries of @total.',
['@total' => $sandbox['max']],
);
}

View File

@@ -0,0 +1,72 @@
entity.taxonomy_term.forum_delete_form:
path: '/admin/structure/forum/delete/forum/{taxonomy_term}'
defaults:
_form: '\Drupal\forum\Form\DeleteForm'
_title: 'Delete forum'
requirements:
_permission: 'administer forums'
forum.settings:
path: '/admin/structure/forum/settings'
defaults:
_form: '\Drupal\forum\ForumSettingsForm'
_title: 'Forums'
requirements:
_permission: 'administer forums'
forum.index:
path: '/forum'
defaults:
_controller: '\Drupal\forum\Controller\ForumController::forumIndex'
_title: 'Forums'
requirements:
_permission: 'access content'
forum.page:
path: '/forum/{taxonomy_term}'
defaults:
_controller: '\Drupal\forum\Controller\ForumController::forumPage'
_title_callback: '\Drupal\taxonomy\Controller\TaxonomyController::termTitle'
requirements:
_permission: 'access content'
_entity_access: 'taxonomy_term.view'
forum.add_container:
path: '/admin/structure/forum/add/container'
defaults:
_controller: '\Drupal\forum\Controller\ForumController::addContainer'
_title: 'Add container'
requirements:
_permission: 'administer forums'
forum.add_forum:
path: '/admin/structure/forum/add/forum'
defaults:
_controller: '\Drupal\forum\Controller\ForumController::addForum'
_title: 'Add forum'
requirements:
_permission: 'administer forums'
entity.taxonomy_term.forum_edit_container_form:
path: '/admin/structure/forum/edit/container/{taxonomy_term}'
defaults:
_entity_form: 'taxonomy_term.container'
_title: 'Edit container'
requirements:
_permission: 'administer forums'
entity.taxonomy_term.forum_edit_form:
path: '/admin/structure/forum/edit/forum/{taxonomy_term}'
defaults:
_entity_form: 'taxonomy_term.forum'
_title: 'Edit forum'
requirements:
_permission: 'administer forums'
forum.overview:
path: '/admin/structure/forum'
defaults:
_form: '\Drupal\forum\Form\Overview'
_title: 'Forums'
requirements:
_permission: 'administer forums'

View File

@@ -0,0 +1,30 @@
services:
forum_manager:
class: Drupal\forum\ForumManager
arguments: ['@config.factory', '@entity_type.manager', '@database', '@string_translation', '@comment.manager', '@entity_field.manager']
tags:
- { name: backend_overridable }
Drupal\forum\ForumManagerInterface: '@forum_manager'
forum.breadcrumb.node:
class: Drupal\forum\Breadcrumb\ForumNodeBreadcrumbBuilder
arguments: ['@entity_type.manager', '@config.factory', '@forum_manager', '@string_translation']
tags:
- { name: breadcrumb_builder, priority: 1001 }
forum.breadcrumb.listing:
class: Drupal\forum\Breadcrumb\ForumListingBreadcrumbBuilder
arguments: ['@entity_type.manager', '@config.factory', '@forum_manager', '@string_translation']
tags:
- { name: breadcrumb_builder, priority: 1001 }
forum.index_storage:
class: Drupal\forum\ForumIndexStorage
arguments: ['@database']
tags:
- { name: backend_overridable }
Drupal\forum\ForumIndexStorageInterface: '@forum.index_storage'
forum.uninstall_validator:
class: Drupal\forum\ForumUninstallValidator
tags:
- { name: module_install.uninstall_validator }
arguments: ['@entity_type.manager', '@config.factory', '@string_translation']
lazy: true

View File

@@ -0,0 +1,156 @@
<?php
/**
* @file
* Provide views data for forum.module.
*/
/**
* Implements hook_views_data().
*/
function forum_views_data() {
$data['forum_index']['table']['group'] = t('Forum');
$data['forum_index']['table']['base'] = [
'field' => 'nid',
'title' => t('Forum content'),
'access query tag' => 'node_access',
];
$data['forum_index']['nid'] = [
'title' => t('Nid'),
'help' => t('The content ID of the forum index entry.'),
'field' => [
'id' => 'numeric',
],
'filter' => [
'id' => 'numeric',
],
'argument' => [
'id' => 'numeric',
],
'sort' => [
'id' => 'standard',
],
'relationship' => [
'base' => 'node',
'base field' => 'nid',
'label' => t('Node'),
],
];
$data['forum_index']['title'] = [
'title' => t('Title'),
'help' => t('The content title.'),
'field' => [
'id' => 'standard',
'link_to_node default' => TRUE,
],
'sort' => [
'id' => 'standard',
],
'filter' => [
'id' => 'string',
],
'argument' => [
'id' => 'string',
],
];
$data['forum_index']['tid'] = [
'title' => t('Has taxonomy term ID'),
'help' => t('Display content if it has the selected taxonomy terms.'),
'argument' => [
'id' => 'taxonomy_index_tid',
'name table' => 'taxonomy_term_data',
'name field' => 'name',
'empty field name' => t('Uncategorized'),
'numeric' => TRUE,
'skip base' => 'taxonomy_term_data',
],
'field' => [
'id' => 'numeric',
],
'filter' => [
'title' => t('Has taxonomy term'),
'id' => 'taxonomy_index_tid',
'hierarchy table' => 'taxonomy_term__parent',
'numeric' => TRUE,
'skip base' => 'taxonomy_term_data',
'allow empty' => TRUE,
],
'relationship' => [
'base' => 'taxonomy_term',
'base field' => 'tid',
'label' => t('Term'),
],
];
$data['forum_index']['created'] = [
'title' => t('Post date'),
'help' => t('The date the content was posted.'),
'field' => [
'id' => 'date',
],
'sort' => [
'id' => 'date',
],
'filter' => [
'id' => 'date',
],
];
$data['forum_index']['sticky'] = [
'title' => t('Sticky'),
'help' => t('Whether or not the content is sticky.'),
'field' => [
'id' => 'boolean',
'click sortable' => TRUE,
'output formats' => [
'sticky' => [t('Sticky'), t('Not sticky')],
],
],
'filter' => [
'id' => 'boolean',
'label' => t('Sticky'),
'type' => 'yes-no',
],
'sort' => [
'id' => 'standard',
'help' => t('Whether or not the content is sticky. To list sticky content first, set this to descending.'),
],
];
$data['forum_index']['last_comment_timestamp'] = [
'title' => t('Last comment time'),
'help' => t('Date and time of when the last comment was posted.'),
'field' => [
'id' => 'comment_last_timestamp',
],
'sort' => [
'id' => 'date',
],
'filter' => [
'id' => 'date',
],
];
$data['forum_index']['comment_count'] = [
'title' => t('Comment count'),
'help' => t('The number of comments a node has.'),
'field' => [
'id' => 'numeric',
],
'filter' => [
'id' => 'numeric',
],
'sort' => [
'id' => 'standard',
],
'argument' => [
'id' => 'standard',
],
];
return $data;
}

View File

@@ -0,0 +1,17 @@
---
label: 'Creating threaded discussion forums'
top_level: true
---
<h2>{% trans %}What is a forum?{% endtrans %}</h2>
<p>{% trans %}A forum is nested hierarchy of discussions, where users post topics and comment on them.{% endtrans %}</p>
<h2>{% trans %}What is the forum hierarchy?{% endtrans %}</h2>
<p>{% trans %}The forum hierarchy consists of:{% endtrans %}</p>
<ul>
<li>{% trans %}<em>Forums</em> (for example, <em>Recipes for cooking vegetables</em>){% endtrans %}</li>
<li>{% trans %}Optional <em>containers</em> can be used to group similar forums (for example, <em>Recipes</em>). Forums can be inside containers and vice versa.{% endtrans %}</li>
<li>{% trans %}<em>Forum topics</em> submitted by users (for example, <em>How to cook potatoes</em>), which start discussions.{% endtrans %}</li>
<li>{% trans %}<em>Comments</em> submitted by users (for example, <em>You wash the potatoes first and then...</em>).{% endtrans %}</li>
</ul>
<p>{% trans %}The <em>forums</em> and <em>containers</em> in the hierarchy are implemented as terms in a hierarchical taxonomy vocabulary. The topics are content items posted in forums (not in containers), and replies are comments on each topic item. Deeply nested hierarchies of forums and containers are generally difficult for users to understand and navigate, so it is recommended to keep your hierarchy simple.{% endtrans %}</p>
<h2>{% trans %}Managing and using forums overview{% endtrans %}</h2>
<p>{% trans %}The core Forum module supplies a content type called <em>Forum topic</em>, along with associated comment type and taxonomy vocabulary. As with other comment types, you can configure comments on forum topics to be threaded or unthreaded. See the related topics listed below for specific tasks.{% endtrans %}</p>

View File

@@ -0,0 +1,26 @@
---
label: 'Configuring forums'
related:
- forum.concept
---
{% set forum_concept_topic = render_var(help_topic_link('forum.concept')) %}
{% set settings_link_text %}{% trans %}Settings{% endtrans %}{% endset %}
{% set settings_link = render_var(help_route_link(settings_link_text, 'forum.settings')) %}
{% set overview_link_text %}{% trans %}Forums{% endtrans %}{% endset %}
{% set overview_link = render_var(help_route_link(overview_link_text, 'forum.overview')) %}
{% set index_link_text %}{% trans %}Forums{% endtrans %}{% endset %}
{% set index_link = render_var(help_route_link(index_link_text, 'forum.index')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Configure settings for forums, and set up forum structure.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <em>Forums</em> &gt; <em>{{ settings_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Choose the desired settings for <em>Hot topic threshold</em>, <em>Topics per page</em>, and <em>Default order</em>. Click <em>Save configuration</em> if you have made any changes.{% endtrans %}</li>
<li>{% trans %}In the <em>Manage</em> administrative menu, navigate to <em>Structure</em> &gt; <em>{{ overview_link }}</em>.{% endtrans %}</li>
<li>{% trans %}Decide on the hierarchy of containers and forums you want for your site; see {{ forum_concept_topic }} for an overview of forum hierarchy.{% endtrans %}</li>
<li>{% trans %}Create the containers that you want in your forum hierarchy, starting at the top level, if any. To create a container, click <em>Add container</em>, enter the container name and optionally other settings, and click <em>Save</em>.{% endtrans %}</li>
<li>{% trans %}Create the forums that you want in your forum hierarchy, starting at the top level. To create a forum, click <em>Add forum</em> and enter the forum name. If your hierarchy has this forum inside a container or another forum, select the parent forum/container in the <em>Parent</em> field. Review and/or edit the other settings, and click <em>Save</em>.{% endtrans %}</li>
<li>{% trans %}Optionally, delete the provided <em>General discussion</em> forum, if you do not want this forum to be available on your site.{% endtrans %}</li>
<li>{% trans %}Review and/or edit the permissions related to forums. The administrative permission for editing the forum settings is in the <em>Forum</em> module section of the permissions page, and administrative permissions for editing the forum hierarchy are in the <em>Taxonomy</em> module section. The user permissions for creating forum topics are in the <em>Node</em> module section, and for commenting on topics are in the <em>Comment</em> module section.{% endtrans %}</li>
<li>{% trans %}Add links to the main <em>{{ index_link }}</em> page (path: <em>/forum</em>), and optionally to individual forum pages, to navigation menus on your site, so that users can find the forums.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,17 @@
---
label: 'Locking a forum topic'
related:
- forum.concept
---
{% set index_link_text %}{% trans %}Forums{% endtrans %}{% endset %}
{% set index_link = render_var(help_route_link(index_link_text, 'forum.index')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Lock a topic to prevent users from making any more comments.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Starting from {{ index_link }} (path: <em>/forums</em>), navigate to the forum that currently contains the topic.{% endtrans %}</li>
<li>{% trans %}Locate the topic within the forum, and click on the title to view the topic.{% endtrans %}</li>
<li>{% trans %}Click <em>Edit</em>.{% endtrans %}</li>
<li>{% trans %}Under <em>Comment settings</em>, check <em>Closed</em>.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,18 @@
---
label: 'Moving a topic to a new forum'
related:
- forum.concept
---
{% set index_link_text %}{% trans %}Forums{% endtrans %}{% endset %}
{% set index_link = render_var(help_route_link(index_link_text, 'forum.index')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Move a forum topic and all of its comments to a new forum. {% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Starting from {{ index_link }} (path: <em>/forums</em>), navigate to the forum that currently contains the topic.{% endtrans %}</li>
<li>{% trans %}Locate the topic within the forum, and click on the title to view the topic.{% endtrans %}</li>
<li>{% trans %}Click <em>Edit</em>.{% endtrans %}</li>
<li>{% trans %}In the <em>Forums</em> field, select the new forum that you want the topic to move to.{% endtrans %}</li>
<li>{% trans %}Check <em>Leave shadow copy</em> to create a link in the original forum pointing to the new location.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,16 @@
---
label: 'Starting a forum discussion'
related:
- forum.concept
---
{% set index_link_text %}{% trans %}Forums{% endtrans %}{% endset %}
{% set index_link = render_var(help_route_link(index_link_text, 'forum.index')) %}
<h2>{% trans %}Goal{% endtrans %}</h2>
<p>{% trans %}Post a new topic in a forum to start a discussion.{% endtrans %}</p>
<h2>{% trans %}Steps{% endtrans %}</h2>
<ol>
<li>{% trans %}Starting from {{ index_link }} (path: <em>/forums</em>), navigate to the forum that currently contains the topic.{% endtrans %}</li>
<li>{% trans %}Click <em>Add new Forum topic</em>.{% endtrans %}</li>
<li>{% trans %}Enter the topic's <em>Subject</em> and <em>Body</em>.{% endtrans %}</li>
<li>{% trans %}Click <em>Save</em>.{% endtrans %}</li>
</ol>

View File

@@ -0,0 +1,31 @@
id: d6_forum_settings
label: Forum configuration
migration_tags:
- Drupal 6
- Configuration
source:
plugin: variable
variables:
- forum_hot_topic
- forum_per_page
- forum_order
- forum_block_num_0
- forum_block_num_1
- forum_nav_vocabulary
source_module: forum
process:
'block/active/limit': forum_block_num_0
'block/new/limit': forum_block_num_1
'topics/hot_threshold': forum_hot_topic
'topics/page_limit': forum_per_page
'topics/order': forum_order
vocabulary:
plugin: migration_lookup
migration: d6_taxonomy_vocabulary
source: forum_nav_vocabulary
destination:
plugin: config
config_name: forum.settings
migration_dependencies:
required:
- d6_taxonomy_vocabulary

View File

@@ -0,0 +1,31 @@
id: d7_forum_settings
label: Forum configuration
migration_tags:
- Drupal 7
- Configuration
source:
plugin: variable
variables:
- forum_hot_topic
- forum_per_page
- forum_order
- forum_block_num_active
- forum_block_num_new
- forum_nav_vocabulary
source_module: forum
process:
'block/active/limit': forum_block_num_active
'block/new/limit': forum_block_num_new
'topics/hot_threshold': forum_hot_topic
'topics/page_limit': forum_per_page
'topics/order': forum_order
vocabulary:
plugin: migration_lookup
migration: d7_taxonomy_vocabulary
source: forum_nav_vocabulary
destination:
plugin: config
config_name: forum.settings
migration_dependencies:
required:
- d7_taxonomy_vocabulary

View File

@@ -0,0 +1,5 @@
finished:
6:
forum: forum
7:
forum: forum

View File

@@ -0,0 +1,90 @@
<?php
namespace Drupal\forum\Breadcrumb;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\forum\ForumManagerInterface;
/**
* Provides a forum breadcrumb base class.
*
* This just holds the dependency-injected config, entity type manager, and
* forum manager objects.
*/
abstract class ForumBreadcrumbBuilderBase implements BreadcrumbBuilderInterface {
use StringTranslationTrait;
/**
* Configuration object for this builder.
*
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The forum manager service.
*
* @var \Drupal\forum\ForumManagerInterface
*/
protected $forumManager;
/**
* The taxonomy term storage.
*
* @var \Drupal\taxonomy\TermStorageInterface
*/
protected $termStorage;
/**
* Constructs a forum breadcrumb builder object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\forum\ForumManagerInterface $forum_manager
* The forum manager service.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory, ForumManagerInterface $forum_manager, TranslationInterface $string_translation) {
$this->entityTypeManager = $entity_type_manager;
$this->config = $config_factory->get('forum.settings');
$this->forumManager = $forum_manager;
$this->setStringTranslation($string_translation);
$this->termStorage = $entity_type_manager->getStorage('taxonomy_term');
}
/**
* {@inheritdoc}
*/
public function build(RouteMatchInterface $route_match) {
$breadcrumb = new Breadcrumb();
$breadcrumb->addCacheContexts(['route']);
$links[] = Link::createFromRoute($this->t('Home'), '<front>');
$vocabulary = $this->entityTypeManager
->getStorage('taxonomy_vocabulary')
->load($this->config->get('vocabulary'));
$breadcrumb->addCacheableDependency($vocabulary);
$links[] = Link::createFromRoute($vocabulary->label(), 'forum.index');
return $breadcrumb->setLinks($links);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Drupal\forum\Breadcrumb;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Provides a breadcrumb builder base class for forum listing pages.
*/
class ForumListingBreadcrumbBuilder extends ForumBreadcrumbBuilderBase {
/**
* {@inheritdoc}
*/
public function applies(RouteMatchInterface $route_match) {
return $route_match->getRouteName() == 'forum.page' && $route_match->getParameter('taxonomy_term');
}
/**
* {@inheritdoc}
*/
public function build(RouteMatchInterface $route_match) {
$breadcrumb = parent::build($route_match);
$breadcrumb->addCacheContexts(['route']);
// Add all parent forums to breadcrumbs.
/** @var \Drupal\taxonomy\TermInterface $term */
$term = $route_match->getParameter('taxonomy_term');
$term_id = $term->id();
$breadcrumb->addCacheableDependency($term);
$parents = $this->termStorage->loadAllParents($term_id);
if ($parents) {
foreach (array_reverse($parents) as $parent) {
if ($parent->id() != $term_id) {
$breadcrumb->addCacheableDependency($parent);
$breadcrumb->addLink(Link::createFromRoute($parent->label(), 'forum.page', [
'taxonomy_term' => $parent->id(),
]));
}
}
}
return $breadcrumb;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Drupal\forum\Breadcrumb;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Breadcrumb builder for forum nodes.
*/
class ForumNodeBreadcrumbBuilder extends ForumBreadcrumbBuilderBase {
/**
* {@inheritdoc}
*/
public function applies(RouteMatchInterface $route_match) {
return $route_match->getRouteName() == 'entity.node.canonical'
&& $route_match->getParameter('node')
&& $this->forumManager->checkNodeType($route_match->getParameter('node'));
}
/**
* {@inheritdoc}
*/
public function build(RouteMatchInterface $route_match) {
$breadcrumb = parent::build($route_match);
$breadcrumb->addCacheContexts(['route']);
$parents = $this->termStorage->loadAllParents($route_match->getParameter('node')->forum_tid);
if ($parents) {
$parents = array_reverse($parents);
foreach ($parents as $parent) {
$breadcrumb->addCacheableDependency($parent);
$breadcrumb->addLink(Link::createFromRoute($parent->label(), 'forum.page',
[
'taxonomy_term' => $parent->id(),
]
));
}
}
return $breadcrumb;
}
}

View File

@@ -0,0 +1,348 @@
<?php
namespace Drupal\forum\Controller;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityAccessControlHandlerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\forum\ForumManagerInterface;
use Drupal\taxonomy\TermInterface;
use Drupal\taxonomy\TermStorageInterface;
use Drupal\taxonomy\VocabularyStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Controller routines for forum routes.
*/
class ForumController extends ControllerBase {
/**
* Forum manager service.
*
* @var \Drupal\forum\ForumManagerInterface
*/
protected $forumManager;
/**
* Vocabulary storage.
*
* @var \Drupal\taxonomy\VocabularyStorageInterface
*/
protected $vocabularyStorage;
/**
* Term storage.
*
* @var \Drupal\taxonomy\TermStorageInterface
*/
protected $termStorage;
/**
* Node access control handler.
*
* @var \Drupal\Core\Entity\EntityAccessControlHandlerInterface
*/
protected $nodeAccess;
/**
* Field map of existing fields on the site.
*
* @var array
*/
protected $fieldMap;
/**
* Node type storage handler.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $nodeTypeStorage;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Node entity type, we need to get cache tags from here.
*
* @var \Drupal\Core\Entity\EntityTypeInterface
*/
protected $nodeEntityTypeDefinition;
/**
* Comment entity type, we need to get cache tags from here.
*
* @var \Drupal\Core\Entity\EntityTypeInterface
*/
protected $commentEntityTypeDefinition;
/**
* Constructs a ForumController object.
*
* @param \Drupal\forum\ForumManagerInterface $forum_manager
* The forum manager service.
* @param \Drupal\taxonomy\VocabularyStorageInterface $vocabulary_storage
* Vocabulary storage.
* @param \Drupal\taxonomy\TermStorageInterface $term_storage
* Term storage.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current logged in user.
* @param \Drupal\Core\Entity\EntityAccessControlHandlerInterface $node_access
* Node access control handler.
* @param array $field_map
* Array of active fields on the site.
* @param \Drupal\Core\Entity\EntityStorageInterface $node_type_storage
* Node type storage handler.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\Core\Entity\EntityTypeInterface $node_entity_type_definition
* Node entity type definition object
* @param \Drupal\Core\Entity\EntityTypeInterface $comment_entity_type_definition
* Comment entity type definition object
*/
public function __construct(ForumManagerInterface $forum_manager, VocabularyStorageInterface $vocabulary_storage, TermStorageInterface $term_storage, AccountInterface $current_user, EntityAccessControlHandlerInterface $node_access, array $field_map, EntityStorageInterface $node_type_storage, RendererInterface $renderer, EntityTypeInterface $node_entity_type_definition, EntityTypeInterface $comment_entity_type_definition) {
$this->forumManager = $forum_manager;
$this->vocabularyStorage = $vocabulary_storage;
$this->termStorage = $term_storage;
$this->currentUser = $current_user;
$this->nodeAccess = $node_access;
$this->fieldMap = $field_map;
$this->nodeTypeStorage = $node_type_storage;
$this->renderer = $renderer;
$this->nodeEntityTypeDefinition = $node_entity_type_definition;
$this->commentEntityTypeDefinition = $comment_entity_type_definition;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
$entity_type_manager = $container->get('entity_type.manager');
return new static(
$container->get('forum_manager'),
$entity_type_manager->getStorage('taxonomy_vocabulary'),
$entity_type_manager->getStorage('taxonomy_term'),
$container->get('current_user'),
$entity_type_manager->getAccessControlHandler('node'),
$container->get('entity_field.manager')->getFieldMap(),
$entity_type_manager->getStorage('node_type'),
$container->get('renderer'),
$entity_type_manager->getDefinition('node'),
$entity_type_manager->getDefinition('comment')
);
}
/**
* Returns forum page for a given forum.
*
* @param \Drupal\taxonomy\TermInterface $taxonomy_term
* The forum to render the page for.
*
* @return array
* A render array.
*/
public function forumPage(TermInterface $taxonomy_term) {
// Get forum details.
$taxonomy_term->forums = $this->forumManager->getChildren($this->config('forum.settings')->get('vocabulary'), $taxonomy_term->id());
$taxonomy_term->parents = $this->termStorage->loadAllParents($taxonomy_term->id());
if (empty($taxonomy_term->forum_container->value)) {
$build = $this->forumManager->getTopics($taxonomy_term->id(), $this->currentUser());
$topics = $build['topics'];
$header = $build['header'];
}
else {
$topics = [];
$header = [];
}
return $this->build($taxonomy_term->forums, $taxonomy_term, $topics, $taxonomy_term->parents, $header);
}
/**
* Returns forum index page.
*
* @return array
* A render array.
*/
public function forumIndex() {
$vocabulary = $this->vocabularyStorage->load($this->config('forum.settings')->get('vocabulary'));
$index = $this->forumManager->getIndex();
$build = $this->build($index->forums, $index);
if (empty($index->forums)) {
// Root of empty forum.
$build['#title'] = $this->t('No forums defined');
}
else {
// Set the page title to forum's vocabulary name.
$build['#title'] = $vocabulary->label();
$this->renderer->addCacheableDependency($build, $vocabulary);
}
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.
* @param array $header
* Array of header cells.
*
* @return array
* A render array.
*/
protected function build($forums, TermInterface $term, $topics = [], $parents = [], $header = []) {
$config = $this->config('forum.settings');
$build = [
'#theme' => 'forums',
'#forums' => $forums,
'#topics' => $topics,
'#parents' => $parents,
'#header' => $header,
'#term' => $term,
'#sortby' => $config->get('topics.order'),
'#forums_per_page' => $config->get('topics.page_limit'),
];
if (empty($term->forum_container->value)) {
$build['#attached']['feed'][] = ['taxonomy/term/' . $term->id() . '/feed', 'RSS - ' . $term->getName()];
}
$this->renderer->addCacheableDependency($build, $config);
foreach ($forums as $forum) {
$this->renderer->addCacheableDependency($build, $forum);
}
foreach ($topics as $topic) {
$this->renderer->addCacheableDependency($build, $topic);
}
foreach ($parents as $parent) {
$this->renderer->addCacheableDependency($build, $parent);
}
$this->renderer->addCacheableDependency($build, $term);
$is_forum = empty($term->forum_container->value);
return [
'action' => ($is_forum) ? $this->buildActionLinks($config->get('vocabulary'), $term) : [],
'forum' => $build,
'#cache' => [
'tags' => Cache::mergeTags($this->nodeEntityTypeDefinition->getListCacheTags(), $this->commentEntityTypeDefinition->getListCacheTags()),
],
];
}
/**
* Returns add forum entity form.
*
* @return array
* Render array for the add form.
*/
public function addForum() {
$vid = $this->config('forum.settings')->get('vocabulary');
$taxonomy_term = $this->termStorage->create([
'vid' => $vid,
]);
return $this->entityFormBuilder()->getForm($taxonomy_term, 'forum');
}
/**
* Returns add container entity form.
*
* @return array
* Render array for the add form.
*/
public function addContainer() {
$vid = $this->config('forum.settings')->get('vocabulary');
$taxonomy_term = $this->termStorage->create([
'vid' => $vid,
'forum_container' => 1,
]);
return $this->entityFormBuilder()->getForm($taxonomy_term, 'container');
}
/**
* Generates an action link to display at the top of the forum listing.
*
* @param string $vid
* Vocabulary ID.
* @param \Drupal\taxonomy\TermInterface $forum_term
* The term for which the links are to be built.
*
* @return array
* Render array containing the links.
*/
protected function buildActionLinks($vid, ?TermInterface $forum_term = NULL) {
$user = $this->currentUser();
$links = [];
// Loop through all bundles for forum taxonomy vocabulary field.
foreach ($this->fieldMap['node']['taxonomy_forums']['bundles'] as $type) {
if ($this->nodeAccess->createAccess($type)) {
$node_type = $this->nodeTypeStorage->load($type);
$links[$type] = [
'#theme' => 'menu_local_action',
'#link' => [
'title' => $this->t('Add new @node_type', [
'@node_type' => $this->nodeTypeStorage->load($type)->label(),
]),
'url' => Url::fromRoute('node.add', ['node_type' => $type]),
],
'#cache' => [
'tags' => $node_type->getCacheTags(),
],
];
if ($forum_term && $forum_term->bundle() == $vid) {
// We are viewing a forum term (specific forum), append the tid to
// the URL.
$links[$type]['#link']['localized_options']['query']['forum_id'] = $forum_term->id();
}
}
}
if (empty($links)) {
// Authenticated user does not have access to create new topics.
if ($user->isAuthenticated()) {
$links['disallowed'] = [
'#markup' => $this->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'] = [
'#theme' => 'menu_local_action',
'#link' => [
'title' => $this->t('Log in to post new content in the forum.'),
'url' => Url::fromRoute('user.login', [], ['query' => $this->getDestinationArray()]),
],
// Without this workaround, the action links will be rendered as <li>
// with no wrapping <ul> element.
// @todo Find a better way for this in https://www.drupal.org/node/3181052.
'#prefix' => '<ul class="action-links">',
'#suffix' => '</ul>',
];
}
}
else {
// Without this workaround, the action links will be rendered as <li> with
// no wrapping <ul> element.
// @todo Find a better way for this in https://www.drupal.org/node/3181052.
$links['#prefix'] = '<ul class="action-links">';
$links['#suffix'] = '</ul>';
}
return $links;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Drupal\forum\Form;
use Drupal\Core\Form\FormStateInterface;
/**
* Base form for container term edit forms.
*
* @internal
*/
class ContainerForm extends ForumForm {
/**
* Reusable URL stub to use in watchdog messages.
*
* @var string
*/
protected $urlStub = 'container';
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
// Build the bulk of the form from the parent forum form.
$form = parent::form($form, $form_state);
// Set the title and description of the name field.
$form['name']['#title'] = $this->t('Container name');
$form['name']['#description'] = $this->t('Short but meaningful name for this collection of related forums.');
// Alternate description for the container parent.
$form['parent'][0]['#description'] = $this->t('Containers are usually placed at the top (root) level, but may also be placed inside another container or forum.');
$this->forumFormType = $this->t('forum container');
return $form;
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
$entity = parent::buildEntity($form, $form_state);
$entity->forum_container = TRUE;
return $entity;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Drupal\forum\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\taxonomy\TermInterface;
/**
* Builds the form to delete a forum term.
*
* @internal
*/
class DeleteForm extends ConfirmFormBase {
/**
* The taxonomy term being deleted.
*
* @var \Drupal\taxonomy\TermInterface
*/
protected $taxonomyTerm;
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'forum_confirm_delete';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete the forum %label?', ['%label' => $this->taxonomyTerm->label()]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('forum.overview');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?TermInterface $taxonomy_term = NULL) {
$this->taxonomyTerm = $taxonomy_term;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->taxonomyTerm->delete();
$this->messenger()->addStatus($this->t('The forum %label and all sub-forums have been deleted.', ['%label' => $this->taxonomyTerm->label()]));
$this->logger('forum')->notice('forum: deleted %label and all its sub-forums.', ['%label' => $this->taxonomyTerm->label()]);
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View File

@@ -0,0 +1,170 @@
<?php
namespace Drupal\forum\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\taxonomy\TermForm;
/**
* Base form for forum term edit forms.
*
* @internal
*/
class ForumForm extends TermForm {
/**
* Reusable type field to use in status messages.
*
* @var string
*/
protected $forumFormType;
/**
* Reusable URL stub to use in watchdog messages.
*
* @var string
*/
protected $urlStub = 'forum';
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
// Build the bulk of the form from the parent taxonomy term form.
$form = parent::form($form, $form_state);
// Set the title and description of the name field.
$form['name']['#title'] = $this->t('Forum name');
$form['name']['#description'] = $this->t('Short but meaningful name for this collection of threaded discussions.');
// Change the description.
$form['description']['#description'] = $this->t('Description and guidelines for discussions within this forum.');
// Re-use the weight field.
$form['weight'] = $form['relations']['weight'];
// Remove the remaining relations fields.
unset($form['relations']);
// Our parent field is different to the taxonomy term.
$form['parent']['#tree'] = TRUE;
$form['parent'][0] = $this->forumParentSelect($this->entity->id(), $this->t('Parent'));
$form['#theme_wrappers'] = ['form__forum'];
$this->forumFormType = $this->t('forum');
return $form;
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
$term = parent::buildEntity($form, $form_state);
// Assign parents from forum parent select field.
$term->parent = [$form_state->getValue(['parent', 0])];
return $term;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$term = $this->entity;
$term_storage = $this->entityTypeManager->getStorage('taxonomy_term');
$status = $term_storage->save($term);
$route_name = $this->urlStub == 'container' ? 'entity.taxonomy_term.forum_edit_container_form' : 'entity.taxonomy_term.forum_edit_form';
$route_parameters = ['taxonomy_term' => $term->id()];
$link = Link::fromTextAndUrl($this->t('Edit'), new Url($route_name, $route_parameters))->toString();
$view_link = $term->toLink($term->getName())->toString();
switch ($status) {
case SAVED_NEW:
$this->messenger()->addStatus($this->t('Created new @type %term.', ['%term' => $view_link, '@type' => $this->forumFormType]));
$this->logger('forum')->notice('Created new @type %term.', ['%term' => $term->getName(), '@type' => $this->forumFormType, 'link' => $link]);
$form_state->setValue('tid', $term->id());
break;
case SAVED_UPDATED:
$this->messenger()->addStatus($this->t('The @type %term has been updated.', ['%term' => $term->getName(), '@type' => $this->forumFormType]));
$this->logger('forum')->notice('Updated @type %term.', ['%term' => $term->getName(), '@type' => $this->forumFormType, 'link' => $link]);
break;
}
$form_state->setRedirect('forum.overview');
return $term;
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
if (!$this->entity->isNew() && $this->entity->hasLinkTemplate('forum-delete-form')) {
$actions['delete']['#url'] = $this->entity->toUrl('forum-delete-form');
}
else {
unset($actions['delete']);
}
return $actions;
}
/**
* Returns a select box for available parent terms.
*
* @param int $tid
* ID of the term that is being added or edited.
* @param string $title
* Title for the select box.
*
* @return array
* A select form element.
*/
protected function forumParentSelect($tid, $title) {
$taxonomy_storage = $this->entityTypeManager->getStorage('taxonomy_term');
$parents = $taxonomy_storage->loadParents($tid);
if ($parents) {
$parent = array_shift($parents);
$parent = $parent->id();
}
else {
$parent = 0;
}
$vid = $this->config('forum.settings')->get('vocabulary');
$children = $taxonomy_storage->loadTree($vid, $tid, NULL, TRUE);
// A term can't be the child of itself, nor of its children.
foreach ($children as $child) {
$exclude[] = $child->tid;
}
$exclude[] = $tid;
$tree = $taxonomy_storage->loadTree($vid, 0, NULL, TRUE);
$options[0] = '<' . $this->t('root') . '>';
if ($tree) {
foreach ($tree as $term) {
if (!in_array($term->id(), $exclude)) {
$options[$term->id()] = str_repeat(' -- ', $term->depth) . $term->getName();
}
}
}
$description = $this->t('Forums may be placed at the top (root) level, or inside another container or forum.');
return [
'#type' => 'select',
'#title' => $title,
'#default_value' => $parent,
'#options' => $options,
'#description' => $description,
'#required' => TRUE,
];
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Drupal\forum\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Drupal\taxonomy\Form\OverviewTerms;
use Drupal\taxonomy\VocabularyInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Provides forum overview form for the forum vocabulary.
*
* @internal
*/
class Overview extends OverviewTerms {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'forum_overview';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?VocabularyInterface $taxonomy_vocabulary = NULL) {
$forum_config = $this->config('forum.settings');
$vid = $forum_config->get('vocabulary');
$vocabulary = $this->entityTypeManager->getStorage('taxonomy_vocabulary')->load($vid);
if (!$vocabulary) {
throw new NotFoundHttpException();
}
// Build base taxonomy term overview.
$form = parent::buildForm($form, $form_state, $vocabulary);
foreach (Element::children($form['terms']) as $key) {
if (isset($form['terms'][$key]['#term'])) {
/** @var \Drupal\taxonomy\TermInterface $term */
$term = $form['terms'][$key]['#term'];
$form['terms'][$key]['term']['#url'] = Url::fromRoute('forum.page', ['taxonomy_term' => $term->id()]);
if (!empty($term->forum_container->value)) {
$title = $this->t('edit container');
$url = Url::fromRoute('entity.taxonomy_term.forum_edit_container_form', ['taxonomy_term' => $term->id()]);
}
else {
$title = $this->t('edit forum');
$url = Url::fromRoute('entity.taxonomy_term.forum_edit_form', ['taxonomy_term' => $term->id()]);
}
// Re-create the operations column and add only the edit link.
$form['terms'][$key]['operations'] = [
'#type' => 'operations',
'#links' => [
'edit' => [
'title' => $title,
'url' => $url,
],
],
];
}
}
// Remove the alphabetical reset.
unset($form['actions']['reset_alphabetical']);
// Use the existing taxonomy overview submit handler.
$form['terms']['#empty'] = $this->t('No containers or forums available. <a href=":container">Add container</a> or <a href=":forum">Add forum</a>.', [
':container' => Url::fromRoute('forum.add_container')->toString(),
':forum' => Url::fromRoute('forum.add_forum')->toString(),
]);
return $form;
}
}

View File

@@ -0,0 +1,164 @@
<?php
namespace Drupal\forum;
use Drupal\comment\CommentInterface;
use Drupal\Core\Database\Connection;
use Drupal\node\NodeInterface;
/**
* Handles CRUD operations to {forum_index} table.
*/
class ForumIndexStorage implements ForumIndexStorageInterface {
/**
* The active database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Constructs a ForumIndexStorage object.
*
* @param \Drupal\Core\Database\Connection $database
* The current database connection.
*/
public function __construct(Connection $database) {
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public function getOriginalTermId(NodeInterface $node) {
return $this->database->queryRange("SELECT [f].[tid] FROM {forum} [f] INNER JOIN {node} [n] ON [f].[vid] = [n].[vid] WHERE [n].[nid] = :nid ORDER BY [f].[vid] DESC", 0, 1, [':nid' => $node->id()])->fetchField();
}
/**
* {@inheritdoc}
*/
public function create(NodeInterface $node) {
$this->database->insert('forum')
->fields([
'tid' => $node->forum_tid,
'vid' => $node->getRevisionId(),
'nid' => $node->id(),
])
->execute();
}
/**
* {@inheritdoc}
*/
public function read(array $vids) {
return $this->database->select('forum', 'f')
->fields('f', ['nid', 'tid'])
->condition('f.vid', $vids, 'IN')
->execute();
}
/**
* {@inheritdoc}
*/
public function delete(NodeInterface $node) {
$this->database->delete('forum')
->condition('nid', $node->id())
->execute();
}
/**
* {@inheritdoc}
*/
public function deleteRevision(NodeInterface $node) {
$this->database->delete('forum')
->condition('nid', $node->id())
->condition('vid', $node->getRevisionId())
->execute();
}
/**
* {@inheritdoc}
*/
public function update(NodeInterface $node) {
$this->database->update('forum')
->fields(['tid' => $node->forum_tid])
->condition('vid', $node->getRevisionId())
->execute();
}
/**
* {@inheritdoc}
*/
public function updateIndex(NodeInterface $node) {
$nid = $node->id();
$count = $this->database->query("SELECT COUNT([cid]) FROM {comment_field_data} [c] INNER JOIN {forum_index} [i] ON [c].[entity_id] = [i].[nid] WHERE [c].[entity_id] = :nid AND [c].[field_name] = 'comment_forum' AND [c].[entity_type] = 'node' AND [c].[status] = :status AND [c].[default_langcode] = 1", [
':nid' => $nid,
':status' => CommentInterface::PUBLISHED,
])->fetchField();
if ($count > 0) {
// Comments exist.
$last_reply = $this->database->queryRange("SELECT [cid], [name], [created], [uid] FROM {comment_field_data} WHERE [entity_id] = :nid AND [field_name] = 'comment_forum' AND [entity_type] = 'node' AND [status] = :status AND [default_langcode] = 1 ORDER BY [cid] DESC", 0, 1, [
':nid' => $nid,
':status' => CommentInterface::PUBLISHED,
])->fetchObject();
$this->database->update('forum_index')
->fields([
'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
$this->database->update('forum_index')
->fields([
'comment_count' => 0,
'last_comment_timestamp' => $node->getCreatedTime(),
])
->condition('nid', $nid)
->execute();
}
}
/**
* {@inheritdoc}
*/
public function createIndex(NodeInterface $node) {
$query = $this->database->insert('forum_index')
->fields(['nid', 'title', 'tid', 'sticky', 'created', 'comment_count', 'last_comment_timestamp']);
foreach ($node->getTranslationLanguages() as $langcode => $language) {
$translation = $node->getTranslation($langcode);
foreach ($translation->taxonomy_forums as $item) {
$query->values([
'nid' => $node->id(),
'title' => $translation->label(),
'tid' => $item->target_id,
'sticky' => (int) $node->isSticky(),
'created' => $node->getCreatedTime(),
'comment_count' => 0,
'last_comment_timestamp' => $node->getCreatedTime(),
]);
}
}
$query->execute();
// The logic for determining last_comment_count is fairly complex, so
// update the index too.
if ($node->isNew()) {
$this->updateIndex($node);
}
}
/**
* {@inheritdoc}
*/
public function deleteIndex(NodeInterface $node) {
$this->database->delete('forum_index')
->condition('nid', $node->id())
->execute();
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Drupal\forum;
use Drupal\node\NodeInterface;
/**
* Handles CRUD operations to {forum_index} table.
*/
interface ForumIndexStorageInterface {
/**
* Returns the forum term id associated with an existing forum node.
*
* @param \Drupal\node\NodeInterface $node
* The existing forum node.
*
* @return int
* The forum term id currently associated with the node.
*/
public function getOriginalTermId(NodeInterface $node);
/**
* Creates a record in {forum} table for the given node.
*
* @param \Drupal\node\NodeInterface $node
* The node for which the record is to be created.
*/
public function create(NodeInterface $node);
/**
* Reads an array of {forum} records for the given revision ids.
*
* @param array $vids
* An array of node revision ids.
*
* @return \Drupal\Core\Database\StatementInterface
* The records from {forum} for the given vids.
*/
public function read(array $vids);
/**
* Updates the {forum} table for the given node.
*
* @param \Drupal\node\NodeInterface $node
* The node for which the record is to be updated.
*/
public function update(NodeInterface $node);
/**
* Deletes the records in {forum} table for the given node.
*
* @param \Drupal\node\NodeInterface $node
* The node for which the records are to be deleted.
*/
public function delete(NodeInterface $node);
/**
* Deletes the records in {forum} table for a given node revision.
*
* @param \Drupal\node\NodeInterface $node
* The node revision for which the records are to be deleted.
*/
public function deleteRevision(NodeInterface $node);
/**
* Creates a {forum_index} entry for the given node.
*
* @param \Drupal\node\NodeInterface $node
* The node for which the index records are to be created.
*/
public function createIndex(NodeInterface $node);
/**
* Updates the {forum_index} records for a given node.
*
* @param \Drupal\node\NodeInterface $node
* The node for which the index records are to be updated.
*/
public function updateIndex(NodeInterface $node);
/**
* Deletes the {forum_index} records for a given node.
*
* @param \Drupal\node\NodeInterface $node
* The node for which the index records are to be deleted.
*/
public function deleteIndex(NodeInterface $node);
}

View File

@@ -0,0 +1,525 @@
<?php
namespace Drupal\forum;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\PagerSelectExtender;
use Drupal\Core\Database\Query\TableSortExtender;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\comment\CommentManagerInterface;
use Drupal\node\NodeInterface;
/**
* Provides forum manager service.
*/
class ForumManager implements ForumManagerInterface {
use StringTranslationTrait;
use DependencySerializationTrait {
__wakeup as defaultWakeup;
__sleep as defaultSleep;
}
/**
* Forum sort order, newest first.
*/
const NEWEST_FIRST = 1;
/**
* Forum sort order, oldest first.
*/
const OLDEST_FIRST = 2;
/**
* Forum sort order, posts with most comments first.
*/
const MOST_POPULAR_FIRST = 3;
/**
* Forum sort order, posts with the least comments first.
*/
const LEAST_POPULAR_FIRST = 4;
/**
* Forum settings config object.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* Entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The comment manager service.
*
* @var \Drupal\comment\CommentManagerInterface
*/
protected $commentManager;
/**
* Array of last post information keyed by forum (term) id.
*
* @var array
*/
protected $lastPostData = [];
/**
* Array of forum statistics keyed by forum (term) id.
*
* @var array
*/
protected $forumStatistics = [];
/**
* Array of forum children keyed by parent forum (term) id.
*
* @var array
*/
protected $forumChildren = [];
/**
* Array of history keyed by nid.
*
* @var array
*/
protected $history = [];
/**
* Cached forum index.
*
* @var \Drupal\taxonomy\TermInterface
*/
protected $index;
/**
* Constructs the forum manager service.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Database\Connection $connection
* The current database connection.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The translation manager service.
* @param \Drupal\comment\CommentManagerInterface $comment_manager
* The comment manager service.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
*/
public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, Connection $connection, TranslationInterface $string_translation, CommentManagerInterface $comment_manager, EntityFieldManagerInterface $entity_field_manager) {
$this->configFactory = $config_factory;
$this->entityTypeManager = $entity_type_manager;
$this->connection = $connection;
$this->stringTranslation = $string_translation;
$this->commentManager = $comment_manager;
$this->entityFieldManager = $entity_field_manager;
}
/**
* {@inheritdoc}
*/
public function getTopics($tid, AccountInterface $account) {
$config = $this->configFactory->get('forum.settings');
$forum_per_page = $config->get('topics.page_limit');
$sortby = $config->get('topics.order');
$header = [
['data' => $this->t('Topic'), 'field' => 'f.title'],
['data' => $this->t('Replies'), 'field' => 'f.comment_count'],
['data' => $this->t('Last reply'), 'field' => 'f.last_comment_timestamp'],
];
$order = $this->getTopicOrder($sortby);
for ($i = 0; $i < count($header); $i++) {
if ($header[$i]['field'] == $order['field']) {
$header[$i]['sort'] = $order['sort'];
}
}
$query = $this->connection->select('forum_index', 'f')
->extend(PagerSelectExtender::class)
->extend(TableSortExtender::class);
$query->fields('f');
$query
->condition('f.tid', $tid)
->addTag('node_access')
->addMetaData('base_table', 'forum_index')
->orderBy('f.sticky', 'DESC')
->orderByHeader($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 = [];
foreach ($result as $record) {
$nids[] = $record->nid;
}
if ($nids) {
$nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($nids);
$query = $this->connection->select('node_field_data', 'n')
->extend(TableSortExtender::class);
$query->fields('n', ['nid']);
$query->join('comment_entity_statistics', 'ces', "[n].[nid] = [ces].[entity_id] AND [ces].[field_name] = 'comment_forum' AND [ces].[entity_type] = 'node'");
$query->fields('ces', [
'cid',
'last_comment_uid',
'last_comment_timestamp',
'comment_count',
]);
$query->join('forum_index', 'f', '[f].[nid] = [n].[nid]');
$query->addField('f', 'tid', 'forum_tid');
$query->join('users_field_data', 'u', '[n].[uid] = [u].[uid] AND [u].[default_langcode] = 1');
$query->addField('u', 'name');
$query->join('users_field_data', 'u2', '[ces].[last_comment_uid] = [u2].[uid] AND [u].[default_langcode] = 1');
$query->addExpression('CASE [ces].[last_comment_uid] WHEN 0 THEN [ces].[last_comment_name] ELSE [u2].[name] END', 'last_comment_name');
$query
->orderBy('f.sticky', 'DESC')
->orderByHeader($header)
->condition('n.nid', $nids, 'IN')
// @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 = [];
foreach ($query->execute() as $row) {
$topic = $nodes[$row->nid];
$topic->comment_mode = $topic->comment_forum->status;
foreach ($row as $key => $value) {
$topic->{$key} = $value;
}
$result[] = $topic;
}
}
else {
$result = [];
}
$topics = [];
$first_new_found = FALSE;
foreach ($result as $topic) {
if ($account->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(), $account);
$topic->new_replies = $this->commentManager->getCountNewComments($topic, 'comment_forum', $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' => $topics, 'header' => $header];
}
/**
* 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 ['field' => 'f.last_comment_timestamp', 'sort' => 'desc'];
case static::OLDEST_FIRST:
return ['field' => 'f.last_comment_timestamp', 'sort' => 'asc'];
case static::MOST_POPULAR_FIRST:
return ['field' => 'f.comment_count', 'sort' => 'desc'];
case static::LEAST_POPULAR_FIRST:
return ['field' => 'f.comment_count', 'sort' => 'asc'];
}
}
/**
* Gets the last time the user viewed a node.
*
* @param int $nid
* The node ID.
* @param \Drupal\Core\Session\AccountInterface $account
* Account to fetch last time for.
*
* @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, AccountInterface $account) {
if (empty($this->history[$nid])) {
$result = $this->connection->select('history', 'h')
->fields('h', ['nid', 'timestamp'])
->condition('uid', $account->id())
->execute();
foreach ($result as $t) {
$this->history[$t->nid] = $t->timestamp > HISTORY_READ_LIMIT ? $t->timestamp : HISTORY_READ_LIMIT;
}
}
return $this->history[$nid] ?? HISTORY_READ_LIMIT;
}
/**
* Provides the last post information for the given forum tid.
*
* @param int $tid
* The forum tid.
*
* @return object
* 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', [':tid' => $tid]);
$query->join('comment_entity_statistics', 'ces', "[n].[nid] = [ces].[entity_id] AND [ces].[field_name] = 'comment_forum' AND [ces].[entity_type] = 'node'");
$query->join('users_field_data', 'u', '[ces].[last_comment_uid] = [u].[uid] AND [u].[default_langcode] = 1');
$query->addExpression('CASE [ces].[last_comment_uid] WHEN 0 THEN [ces].[last_comment_name] ELSE [u].[name] END', 'last_comment_name');
$topic = $query
->fields('ces', ['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 object|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('comment_entity_statistics', 'ces', "[n].[nid] = [ces].[entity_id] AND [ces].[field_name] = 'comment_forum' AND [ces].[entity_type] = 'node'");
$query->join('forum', 'f', '[n].[vid] = [f].[vid]');
$query->addExpression('COUNT([n].[nid])', 'topic_count');
$query->addExpression('SUM([ces].[comment_count])', 'comment_count');
$this->forumStatistics = $query
->fields('f', ['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 = [];
$_forums = $this->entityTypeManager->getStorage('taxonomy_term')->loadTree($vid, $tid, NULL, TRUE);
foreach ($_forums as $forum) {
if (!$forum->access('view')) {
continue;
}
// 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->entityTypeManager->getStorage('taxonomy_term')->create([
'tid' => 0,
'container' => 1,
'parents' => [],
'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 = [];
}
/**
* {@inheritdoc}
*/
public function checkNodeType(NodeInterface $node) {
// Fetch information about the forum field.
$field_definitions = $this->entityFieldManager->getFieldDefinitions('node', $node->bundle());
return !empty($field_definitions['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', [':tid' => $term]);
$query->leftJoin('history', 'h', '[n].[nid] = [h].[nid] AND [h].[uid] = :uid', [':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 __sleep() {
$vars = $this->defaultSleep();
// Do not serialize static cache.
unset($vars['history'], $vars['index'], $vars['lastPostData'], $vars['forumChildren'], $vars['forumStatistics']);
return $vars;
}
/**
* {@inheritdoc}
*/
public function __wakeup() {
$this->defaultWakeup();
// Initialize static cache.
$this->history = [];
$this->lastPostData = [];
$this->forumChildren = [];
$this->forumStatistics = [];
$this->index = NULL;
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Drupal\forum;
use Drupal\Core\Session\AccountInterface;
use Drupal\node\NodeInterface;
/**
* Provides forum manager interface.
*/
interface ForumManagerInterface {
/**
* Gets list of forum topics.
*
* @param int $tid
* Term ID.
* @param \Drupal\Core\Session\AccountInterface $account
* Account to fetch topics for.
*
* @return array
* Array with keys 'topics' and 'header'.
*/
public function getTopics($tid, AccountInterface $account);
/**
* Utility method to fetch the child forums for a given forum.
*
* @param int $vid
* The forum vocabulary ID.
* @param int $tid
* The forum ID to fetch the children for.
*
* @return array
* Array of children.
*/
public function getChildren($vid, $tid);
/**
* Generates and returns the forum index.
*
* The forum index is a pseudo term that provides an overview of all forums.
*
* @return \Drupal\taxonomy\TermInterface
* A pseudo term representing the overview of all forums.
*/
public function getIndex();
/**
* Resets the ForumManager index and history.
*/
public function resetCache();
/**
* Checks whether a node can be used in a forum, based on its content type.
*
* @param \Drupal\node\NodeInterface $node
* A node entity.
*
* @return bool
* Boolean indicating if the node can be assigned to a forum.
*/
public function checkNodeType(NodeInterface $node);
/**
* 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 int $term
* The term ID of the forum.
* @param int $uid
* The user ID.
*
* @return int
* The number of new posts in the forum that have not been read by the user.
*/
public function unreadTopics($term, $uid);
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Drupal\forum;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Configure forum settings for this site.
*
* @internal
*/
class ForumSettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'forum_admin_settings';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames() {
return ['forum.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$config = $this->config('forum.settings');
$options = [5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 80, 100, 150, 200, 250, 300, 350, 400, 500];
$form['forum_hot_topic'] = [
'#type' => 'select',
'#title' => $this->t('Hot topic threshold'),
'#default_value' => $config->get('topics.hot_threshold'),
'#options' => array_combine($options, $options),
'#description' => $this->t('The number of replies a topic must have to be considered "hot".'),
];
$options = [10, 25, 50, 75, 100];
$form['forum_per_page'] = [
'#type' => 'select',
'#title' => $this->t('Topics per page'),
'#default_value' => $config->get('topics.page_limit'),
'#options' => array_combine($options, $options),
'#description' => $this->t('Default number of forum topics displayed per page.'),
];
$order = [
1 => $this->t('Date - newest first'),
2 => $this->t('Date - oldest first'),
3 => $this->t('Posts - most active first'),
4 => $this->t('Posts - least active first'),
];
$form['forum_order'] = [
'#type' => 'radios',
'#title' => $this->t('Default order'),
'#default_value' => $config->get('topics.order'),
'#options' => $order,
'#description' => $this->t('Default display order for topics.'),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('forum.settings')
->set('topics.hot_threshold', $form_state->getValue('forum_hot_topic'))
->set('topics.page_limit', $form_state->getValue('forum_per_page'))
->set('topics.order', $form_state->getValue('forum_order'))
->save();
parent::submitForm($form, $form_state);
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace Drupal\forum;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleUninstallValidatorInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\taxonomy\VocabularyInterface;
/**
* Prevents forum module from being uninstalled under certain conditions.
*
* These conditions are when any forum nodes exist or there are any terms in the
* forum vocabulary.
*/
class ForumUninstallValidator implements ModuleUninstallValidatorInterface {
use StringTranslationTrait;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* Constructs a new ForumUninstallValidator.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory, TranslationInterface $string_translation) {
$this->entityTypeManager = $entity_type_manager;
$this->configFactory = $config_factory;
$this->stringTranslation = $string_translation;
}
/**
* {@inheritdoc}
*/
public function validate($module) {
$reasons = [];
if ($module == 'forum') {
if ($this->hasForumNodes()) {
$reasons[] = $this->t('To uninstall Forum, first delete all <em>Forum</em> content');
}
$vocabulary = $this->getForumVocabulary();
if (!empty($vocabulary) && $this->hasTermsForVocabulary($vocabulary)) {
if ($vocabulary->access('view')) {
$reasons[] = $this->t('To uninstall Forum, first delete all <a href=":url">%vocabulary</a> terms', [
'%vocabulary' => $vocabulary->label(),
':url' => $vocabulary->toUrl('overview-form')->toString(),
]);
}
else {
$reasons[] = $this->t('To uninstall Forum, first delete all %vocabulary terms', [
'%vocabulary' => $vocabulary->label(),
]);
}
}
}
return $reasons;
}
/**
* Determines if there are any forum nodes or not.
*
* @return bool
* TRUE if there are forum nodes, FALSE otherwise.
*/
protected function hasForumNodes() {
$nodes = $this->entityTypeManager->getStorage('node')->getQuery()
->condition('type', 'forum')
->accessCheck(FALSE)
->range(0, 1)
->execute();
return !empty($nodes);
}
/**
* Determines if there are any taxonomy terms for a specified vocabulary.
*
* @param \Drupal\taxonomy\VocabularyInterface $vocabulary
* The vocabulary to check for terms.
*
* @return bool
* TRUE if there are terms for this vocabulary, FALSE otherwise.
*/
protected function hasTermsForVocabulary(VocabularyInterface $vocabulary) {
$terms = $this->entityTypeManager->getStorage('taxonomy_term')->getQuery()
->condition('vid', $vocabulary->id())
->accessCheck(FALSE)
->range(0, 1)
->execute();
return !empty($terms);
}
/**
* Returns the vocabulary configured for forums.
*
* @return \Drupal\taxonomy\VocabularyInterface
* The vocabulary entity for forums.
*/
protected function getForumVocabulary() {
$vid = $this->configFactory->get('forum.settings')->get('vocabulary');
if (!empty($vid)) {
return $this->entityTypeManager->getStorage('taxonomy_vocabulary')->load($vid);
}
else {
return NULL;
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Drupal\forum\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Database\Database;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides an 'Active forum topics' block.
*/
#[Block(
id: "forum_active_block",
admin_label: new TranslatableMarkup("Active forum topics"),
category: new TranslatableMarkup("Lists (Views)")
)]
class ActiveTopicsBlock extends ForumBlockBase {
/**
* {@inheritdoc}
*/
protected function buildForumQuery() {
return Database::getConnection()->select('forum_index', 'f')
->fields('f')
->addTag('node_access')
->addMetaData('base_table', 'forum_index')
->orderBy('f.last_comment_timestamp', 'DESC')
->range(0, $this->configuration['block_count']);
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Drupal\forum\Plugin\Block;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
/**
* Provides a base class for Forum blocks.
*/
abstract class ForumBlockBase extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
$result = $this->buildForumQuery()->execute();
$elements = [];
if ($node_title_list = node_title_list($result)) {
$elements['forum_list'] = $node_title_list;
$elements['forum_more'] = [
'#type' => 'more_link',
'#url' => Url::fromRoute('forum.index'),
'#attributes' => ['title' => $this->t('Read the latest forum topics.')],
];
}
return $elements;
}
/**
* Builds the select query to use for this forum block.
*
* @return \Drupal\Core\Database\Query\Select
* A Select object.
*/
abstract protected function buildForumQuery();
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'properties' => [
'administrative' => TRUE,
],
'block_count' => 5,
];
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
return AccessResult::allowedIfHasPermission($account, 'access content');
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$range = range(2, 20);
$form['block_count'] = [
'#type' => 'select',
'#title' => $this->t('Number of topics'),
'#default_value' => $this->configuration['block_count'],
'#options' => array_combine($range, $range),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
$this->configuration['block_count'] = $form_state->getValue('block_count');
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return Cache::mergeContexts(parent::getCacheContexts(), ['user.node_grants:view']);
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
return Cache::mergeTags(parent::getCacheTags(), ['node_list']);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Drupal\forum\Plugin\Block;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Database\Database;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Provides a 'New forum topics' block.
*/
#[Block(
id: "forum_new_block",
admin_label: new TranslatableMarkup("New forum topics"),
category: new TranslatableMarkup("Lists (Views)")
)]
class NewTopicsBlock extends ForumBlockBase {
/**
* {@inheritdoc}
*/
protected function buildForumQuery() {
return Database::getConnection()->select('forum_index', 'f')
->fields('f')
->addTag('node_access')
->addMetaData('base_table', 'forum_index')
->orderBy('f.created', 'DESC')
->range(0, $this->configuration['block_count']);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Drupal\forum\Plugin\Validation\Constraint;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
use Symfony\Component\Validator\Constraint as SymfonyConstraint;
/**
* Checks that the node is assigned only a "leaf" term in the forum taxonomy.
*/
#[Constraint(
id: 'ForumLeaf',
label: new TranslatableMarkup('Forum leaf', [], ['context' => 'Validation'])
)]
class ForumLeafConstraint extends SymfonyConstraint {
public $selectForum = 'Select a forum.';
public $noLeafMessage = 'The item %forum is a forum container, not a forum. Select one of the forums below instead.';
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Drupal\forum\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the ForumLeaf constraint.
*/
class ForumLeafConstraintValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*/
public function validate($items, Constraint $constraint) {
$item = $items->first();
if (!isset($item)) {
return NULL;
}
// Verify that a term has been selected.
if (!$item->entity) {
$this->context->addViolation($constraint->selectForum);
}
// The forum_container flag must not be set.
if (!empty($item->entity->forum_container->value)) {
$this->context->addViolation($constraint->noLeafMessage, ['%forum' => $item->entity->getName()]);
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Drupal\forum\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Checks if the vocabulary being migrated is the one used for forums.
*
* Drupal 8 Forum is expecting specific machine names for its field and
* vocabulary names. This process plugin forces a given machine name to the
* field or vocabulary that is being migrated.
*
* The 'forum_vocabulary' source property is evaluated in the
* d6_taxonomy_vocabulary or d7_taxonomy_vocabulary source plugins and is set to
* true if the vocabulary vid being migrated is the same as the one in the
* 'forum_nav_vocabulary' variable on the source site.
*
* Example:
*
* @code
* process:
* field_name:
* plugin: forum_vocabulary
* machine_name: taxonomy_forums
* @endcode
*/
#[MigrateProcess('forum_vocabulary')]
class ForumVocabulary extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
if ($row->getSourceProperty('forum_vocabulary') && !empty($this->configuration['machine_name'])) {
$value = $this->configuration['machine_name'];
}
return $value;
}
}

View File

@@ -0,0 +1,88 @@
<?php
// phpcs:ignoreFile
/**
* This file was generated via php core/scripts/generate-proxy-class.php 'Drupal\forum\ForumUninstallValidator' "core/modules/forum/src".
*/
namespace Drupal\forum\ProxyClass {
/**
* Provides a proxy class for \Drupal\forum\ForumUninstallValidator.
*
* @see \Drupal\Component\ProxyBuilder
*/
class ForumUninstallValidator implements \Drupal\Core\Extension\ModuleUninstallValidatorInterface
{
use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
/**
* The id of the original proxied service.
*
* @var string
*/
protected $drupalProxyOriginalServiceId;
/**
* The real proxied service, after it was lazy loaded.
*
* @var \Drupal\forum\ForumUninstallValidator
*/
protected $service;
/**
* The service container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* Constructs a ProxyClass Drupal proxy object.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The container.
* @param string $drupal_proxy_original_service_id
* The service ID of the original service.
*/
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
{
$this->container = $container;
$this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
}
/**
* Lazy loads the real service from the container.
*
* @return object
* Returns the constructed real service.
*/
protected function lazyLoadItself()
{
if (!isset($this->service)) {
$this->service = $this->container->get($this->drupalProxyOriginalServiceId);
}
return $this->service;
}
/**
* {@inheritdoc}
*/
public function validate($module)
{
return $this->lazyLoadItself()->validate($module);
}
/**
* {@inheritdoc}
*/
public function setStringTranslation(\Drupal\Core\StringTranslation\TranslationInterface $translation)
{
return $this->lazyLoadItself()->setStringTranslation($translation);
}
}
}

View File

@@ -0,0 +1,26 @@
{#
/**
* @file
* Default theme implementation to display a status icon for a forum post.
*
* Available variables:
* - attributes: HTML attributes to be applied to the wrapper element.
* - class: HTML classes that determine which icon to display. May be one of
* 'hot', 'hot-new', 'new', 'default', 'closed', or 'sticky'.
* - title: Text alternative for the forum icon.
* - icon_title: Text alternative for the forum icon, same as above.
* - new_posts: '1' when this topic contains new posts, otherwise '0'.
* - first_new: '1' when this is the first topic with new posts, otherwise '0'.
* - icon_status: Indicates which status icon should be used.
*
* @see template_preprocess_forum_icon()
*
* @ingroup themeable
*/
#}
<div{{ attributes }}>
{% if first_new -%}
<a id="new"></a>
{%- endif %}
<span class="visually-hidden">{{ icon_title }}</span>
</div>

View File

@@ -0,0 +1,77 @@
{#
/**
* @file
* Default theme implementation to display a list of forums and containers.
*
* Available variables:
* - forums: A collection of forums and containers to display. It is keyed to
* the numeric IDs of all child forums and containers. Each forum in forums
* contains:
* - is_container: A flag indicating if the forum can contain other
* forums. Otherwise, the forum can only contain topics.
* - depth: How deep the forum is in the current hierarchy.
* - zebra: 'even' or 'odd', used for row class.
* - icon_class: 'default' or 'new', used for forum icon class.
* - icon_title: Text alternative for the forum icon.
* - name: The name of the forum.
* - link: The URL to link to this forum.
* - description: The description field for the forum, containing:
* - value: The descriptive text for the forum.
* - new_topics: A flag indicating if the forum contains unread posts.
* - new_url: A URL to the forum's unread posts.
* - new_text: Text for the above URL, which tells how many new posts.
* - old_topics: A count of posts that have already been read.
* - num_posts: The total number of posts in the forum.
* - last_reply: Text representing the last time a forum was posted or
* commented in.
* - forum_id: Forum ID for the current forum. Parent to all items within the
* forums array.
*
* @see template_preprocess_forum_list()
*
* @ingroup themeable
*/
#}
<table>
<thead>
<tr>
<th>{{ 'Forum'|t }}</th>
<th>{{ 'Topics'|t }}</th>
<th>{{ 'Posts'|t }}</th>
<th>{{ 'Last post'|t }}</th>
</tr>
</thead>
<tbody>
{% for child_id, forum in forums %}
<tr>
<td{% if forum.is_container == true %} colspan="4"{% endif %}>
{#
Enclose the contents of this cell with X divs, where X is the
depth this forum resides at. This will allow us to use CSS
left-margin for indenting.
#}
{% if forum.depth > 0 %}{% for i in 1..forum.depth %}<div class="indent">{% endfor %}{% endif %}
<div title="{{ forum.icon_title }}">
<span class="visually-hidden">{{ forum.icon_title }}</span>
</div>
<div><a href="{{ forum.link }}">{{ forum.label }}</a></div>
{% if forum.description.value %}
<div>{{ forum.description.value }}</div>
{% endif %}
{% if forum.depth > 0 %}{% for i in 1..forum.depth %}</div>{% endfor %}{% endif %}
</td>
{% if forum.is_container == false %}
<td>
{{ forum.num_topics }}
{% if forum.new_topics == true %}
<br />
<a href="{{ forum.new_url }}">{{ forum.new_text }}</a>
{% endif %}
</td>
<td>{{ forum.num_posts }}</td>
<td>{{ forum.last_reply }}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>

View File

@@ -0,0 +1,23 @@
{#
/**
* @file
* Default theme implementation for a forum post submission string.
*
* The submission string indicates when and by whom a topic was submitted.
*
* Available variables:
* - author: The author of the post.
* - time: How long ago the post was created.
* - topic: An object with the raw data of the post. Potentially unsafe. Be
* sure to clean this data before printing.
*
* @see template_preprocess_forum_submitted()
*
* @ingroup themeable
*/
#}
{% if time %}
<span>{% trans %}By {{ author }} {{ time }} ago{% endtrans %}</span>
{% else %}
{{ 'n/a'|t }}
{% endif %}

View File

@@ -0,0 +1,18 @@
{#
/**
* @file
* Default theme implementation to display a forum topic.
*
* Available variables:
* - title_link: The forum title link to display.
* - submitted: Indicates when and by whom a topic was submitted.
*
* @see template_preprocess_forums()
*
* @ingroup themeable
*/
#}
<div class="forum__title">
<div>{{ title_link }}</div>
<div>{{ submitted }}</div>
</div>

View File

@@ -0,0 +1,24 @@
{#
/**
* @file
* Default theme implementation to display a forum.
*
* May contain forum containers as well as forum topics.
*
* Available variables:
* - forums: The forums to display (as processed by forum-list.html.twig).
* - topics: The topics to display.
* - topics_original: Original topics data before modification.
* - topics_pager: The topics pager.
* - forums_defined: A flag to indicate that the forums are configured.
*
* @see template_preprocess_forums()
*
* @ingroup themeable
*/
#}
{% if forums_defined %}
{{ forums }}
{{ topics }}
{{ topics_pager }}
{% endif %}

19786
core/modules/forum/tests/fixtures/drupal6.php vendored Executable file

File diff suppressed because it is too large Load Diff

30598
core/modules/forum/tests/fixtures/drupal7.php vendored Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
name: 'Forum test views'
type: module
description: 'Provides default views for views forum tests.'
package: Testing
# version: VERSION
dependencies:
- drupal:forum
- drupal:views
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,148 @@
langcode: en
status: true
dependencies: { }
id: test_forum_index
label: test_forum_index
module: views
description: ''
tag: ''
base_table: forum_index
base_field: nid
display:
default:
display_plugin: default
id: default
display_title: Default
position: null
display_options:
access:
type: none
cache:
type: tag
query:
type: views_query
exposed_form:
type: basic
pager:
type: full
style:
type: default
row:
type: fields
fields:
nid:
table: forum_index
field: nid
id: nid
sticky:
id: sticky
table: forum_index
field: sticky
relationship: none
group_type: group
admin_label: ''
label: Sticky
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
type: yes-no
type_custom_true: ''
type_custom_false: ''
not: false
plugin_id: boolean
comment_count:
id: comment_count
table: forum_index
field: comment_count
relationship: none
group_type: group
admin_label: ''
label: 'Comment count'
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: true
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
set_precision: false
precision: 0
decimal: .
separator: ','
format_plural: false
format_plural_string: "1\x03@count"
prefix: ''
suffix: ''
plugin_id: numeric
filters: { }
sorts: { }

View File

@@ -0,0 +1,10 @@
name: 'forum_Url_alter tests'
type: module
description: 'A support module to test altering the inbound and outbound path.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,6 @@
services:
forum_url_alter_test.path_processor:
class: Drupal\forum_url_alter_test\PathProcessorTest
tags:
- { name: path_processor_inbound, priority: 800 }
- { name: path_processor_outbound, priority: 100 }

View File

@@ -0,0 +1,31 @@
<?php
namespace Drupal\forum_url_alter_test;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\PathProcessor\OutboundPathProcessorInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Symfony\Component\HttpFoundation\Request;
/**
* Path processor for url_alter_test.
*/
class PathProcessorTest implements InboundPathProcessorInterface, OutboundPathProcessorInterface {
/**
* {@inheritdoc}
*/
public function processInbound($path, Request $request) {
// Rewrite community/ to forum/.
return preg_replace('@^/community(.*)@', '/forum$1', $path);
}
/**
* {@inheritdoc}
*/
public function processOutbound($path, &$options = [], ?Request $request = NULL, ?BubbleableMetadata $bubbleable_metadata = NULL) {
// Rewrite forum/ to community/.
return preg_replace('@^/forum(.*)@', '/community$1', $path);
}
}

View File

@@ -0,0 +1,192 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Functional;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\comment\Entity\Comment;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the forum blocks.
*
* @group forum
* @group legacy
*/
class ForumBlockTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['forum', 'block'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user with various administrative privileges.
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create users.
$this->adminUser = $this->drupalCreateUser([
'access administration pages',
'administer blocks',
'administer nodes',
'create forum content',
'post comments',
'skip comment approval',
]);
}
/**
* Tests the "New forum topics" block.
*/
public function testNewForumTopicsBlock(): void {
$this->drupalLogin($this->adminUser);
// Enable the new forum topics block.
$block = $this->drupalPlaceBlock('forum_new_block');
$this->drupalGet('');
// Create 5 forum topics.
$topics = $this->createForumTopics();
$this->assertSession()->linkExists('More', 0, 'New forum topics block has a "more"-link.');
$this->assertSession()->linkByHrefExists('forum', 0, 'New forum topics block has a "more"-link.');
// We expect all 5 forum topics to appear in the "New forum topics" block.
foreach ($topics as $topic) {
$this->assertSession()->linkExists($topic, 0, new FormattableMarkup('Forum topic @topic found in the "New forum topics" block.', ['@topic' => $topic]));
}
// Configure the new forum topics block to only show 2 topics.
$block->getPlugin()->setConfigurationValue('block_count', 2);
$block->save();
$this->drupalGet('');
// We expect only the 2 most recent forum topics to appear in the "New forum
// topics" block.
for ($index = 0; $index < 5; $index++) {
if (in_array($index, [3, 4])) {
$this->assertSession()->linkExists($topics[$index], 0, new FormattableMarkup('Forum topic @topic found in the "New forum topics" block.', ['@topic' => $topics[$index]]));
}
else {
$this->assertSession()->pageTextNotContains($topics[$index]);
}
}
}
/**
* Tests the "Active forum topics" block.
*/
public function testActiveForumTopicsBlock(): void {
$this->drupalLogin($this->adminUser);
// Create 10 forum topics.
$topics = $this->createForumTopics(10);
// Comment on the first 5 topics.
$date = new DrupalDateTime();
for ($index = 0; $index < 5; $index++) {
// Get the node from the topic title.
$node = $this->drupalGetNodeByTitle($topics[$index]);
$date->modify('+1 minute');
$comment = Comment::create([
'entity_id' => $node->id(),
'field_name' => 'comment_forum',
'entity_type' => 'node',
'node_type' => 'node_type_' . $node->bundle(),
'subject' => $this->randomString(20),
'comment_body' => $this->randomString(256),
'created' => $date->getTimestamp(),
]);
$comment->save();
}
// Enable the block.
$block = $this->drupalPlaceBlock('forum_active_block');
$this->drupalGet('');
$this->assertSession()->linkExists('More', 0, 'Active forum topics block has a "more"-link.');
$this->assertSession()->linkByHrefExists('forum', 0, 'Active forum topics block has a "more"-link.');
// We expect the first 5 forum topics to appear in the "Active forum topics"
// block.
$this->drupalGet('<front>');
for ($index = 0; $index < 10; $index++) {
if ($index < 5) {
$this->assertSession()->linkExists($topics[$index], 0, new FormattableMarkup('Forum topic @topic found in the "Active forum topics" block.', ['@topic' => $topics[$index]]));
}
else {
$this->assertSession()->pageTextNotContains($topics[$index]);
}
}
// Configure the active forum block to only show 2 topics.
$block->getPlugin()->setConfigurationValue('block_count', 2);
$block->save();
$this->drupalGet('');
// We expect only the 2 forum topics with most recent comments to appear in
// the "Active forum topics" block.
for ($index = 0; $index < 10; $index++) {
if (in_array($index, [3, 4])) {
$this->assertSession()->linkExists($topics[$index], 0, 'Forum topic found in the "Active forum topics" block.');
}
else {
$this->assertSession()->pageTextNotContains($topics[$index]);
}
}
}
/**
* Creates a forum topic.
*
* @return string
* The title of the newly generated topic.
*/
protected function createForumTopics($count = 5) {
$topics = [];
$date = new DrupalDateTime();
$date->modify('-24 hours');
for ($index = 0; $index < $count; $index++) {
// Generate a random subject/body.
$title = $this->randomMachineName(20);
$body = $this->randomMachineName(200);
// Forum posts are ordered by timestamp, so force a unique timestamp by
// changing the date.
$date->modify('+1 minute');
$edit = [
'title[0][value]' => $title,
'body[0][value]' => $body,
// Forum posts are ordered by timestamp, so force a unique timestamp by
// adding the index.
'created[0][value][date]' => $date->format('Y-m-d'),
'created[0][value][time]' => $date->format('H:i:s'),
];
// Create the forum topic, preselecting the forum ID via a URL parameter.
$this->drupalGet('node/add/forum', ['query' => ['forum_id' => 1]]);
$this->submitForm($edit, 'Save');
$topics[] = $title;
}
return $topics;
}
}

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests the forum index listing.
*
* @group forum
* @group legacy
*/
class ForumIndexTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['taxonomy', 'comment', 'forum'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a test user.
$web_user = $this->drupalCreateUser([
'create forum content',
'edit own forum content',
'edit any forum content',
'administer nodes',
'administer forums',
]);
$this->drupalLogin($web_user);
}
/**
* Tests the forum index for published and unpublished nodes.
*/
public function testForumIndexStatus(): void {
// The forum ID to use.
$tid = 1;
// Create a test node.
$title = $this->randomMachineName(20);
$edit = [
'title[0][value]' => $title,
'body[0][value]' => $this->randomMachineName(200),
];
// Create the forum topic, preselecting the forum ID via a URL parameter.
$this->drupalGet("forum/$tid");
$this->clickLink('Add new Forum topic');
$this->assertSession()->addressEquals("node/add/forum?forum_id=$tid");
$this->submitForm($edit, 'Save');
// Check that the node exists in the database.
$node = $this->drupalGetNodeByTitle($title);
$this->assertNotEmpty($node, 'New forum node found in database.');
// Create a child forum.
$edit = [
'name[0][value]' => $this->randomMachineName(20),
'description[0][value]' => $this->randomMachineName(200),
'parent[0]' => $tid,
];
$this->drupalGet('admin/structure/forum/add/forum');
$this->submitForm($edit, 'Save');
$this->assertSession()->linkExists('edit forum');
$tid_child = $tid + 1;
// Verify that the node appears on the index.
$this->drupalGet('forum/' . $tid);
$this->assertSession()->pageTextContains($title);
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'node_list');
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:node.type.forum');
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'comment_list');
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'node:' . $node->id());
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'taxonomy_term:' . $tid);
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'taxonomy_term:' . $tid_child);
// Unpublish the node.
$edit = ['status[value]' => FALSE];
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains('Access denied');
// Verify that the node no longer appears on the index.
$this->drupalGet('forum/' . $tid);
$this->assertSession()->pageTextNotContains($title);
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Functional;
use Drupal\Core\Site\Settings;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* Tests addition of the forum_index primary key.
*
* @group forum
*/
final class ForumIndexUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
dirname(__DIR__, 2) . '/fixtures/update/drupal-10.1.0.empty.testing.forum.gz',
];
}
/**
* Tests the update path to add the new primary key.
*/
public function testUpdatePath(): void {
// Set the batch size to 1 to validate the sandbox logic in the update hook.
$settings = Settings::getInstance() ? Settings::getAll() : [];
$settings['entity_update_batch_size'] = 1;
new Settings($settings);
$schema = \Drupal::database()->schema();
// We can't reliably call ::indexExists for each database driver as sqlite
// doesn't have named indexes for primary keys like mysql (PRIMARY) and
// pgsql (pkey).
$find_primary_key_columns = new \ReflectionMethod(get_class($schema), 'findPrimaryKeyColumns');
$columns = $find_primary_key_columns->invoke($schema, 'forum_index');
$this->assertEmpty($columns);
$count = \Drupal::database()->select('forum_index')->countQuery()->execute()->fetchField();
$this->assertEquals(9, $count);
$duplicates = \Drupal::database()->select('forum_index')->condition('nid', 1)->countQuery()->execute()->fetchField();
$this->assertEquals(2, $duplicates);
$duplicates = \Drupal::database()->select('forum_index')->condition('nid', 2)->countQuery()->execute()->fetchField();
$this->assertEquals(3, $duplicates);
$this->runUpdates();
$this->assertEquals(['nid', 'tid'], $find_primary_key_columns->invoke($schema, 'forum_index'));
$count = \Drupal::database()->select('forum_index')->countQuery()->execute()->fetchField();
$this->assertEquals(6, $count);
$duplicates = \Drupal::database()->select('forum_index')->condition('nid', 1)->countQuery()->execute()->fetchField();
$this->assertEquals(1, $duplicates);
$duplicates = \Drupal::database()->select('forum_index')->condition('nid', 2)->countQuery()->execute()->fetchField();
$this->assertEquals(1, $duplicates);
// This entry is associated with two terms so two records should remain.
$duplicates = \Drupal::database()->select('forum_index')->condition('nid', 4)->countQuery()->execute()->fetchField();
$this->assertEquals(2, $duplicates);
$entry = \Drupal::database()->select('forum_index', 'f')->fields('f')->condition('nid', 5)->execute()->fetchAssoc();
$this->assertEquals([
'nid' => 5,
'title' => 'AFL',
'tid' => 5,
'sticky' => 0,
'created' => 1695264369,
'last_comment_timestamp' => 1695264403,
'comment_count' => 1,
], $entry);
}
}

View File

@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\node\Entity\NodeType;
/**
* Tests forum block view for private node access.
*
* @group forum
* @group legacy
*/
class ForumNodeAccessTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'node',
'comment',
'forum',
'taxonomy',
'node_access_test',
'block',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
node_access_rebuild();
node_access_test_add_field(NodeType::load('forum'));
\Drupal::state()->set('node_access_test.private', TRUE);
}
/**
* Creates some users and creates a public node and a private node.
*
* Adds both active forum topics and new forum topics blocks to the sidebar.
* Tests to ensure private node/public node access is respected on blocks.
*/
public function testForumNodeAccess(): void {
// Create some users.
$access_user = $this->drupalCreateUser(['node test view']);
$no_access_user = $this->drupalCreateUser();
$admin_user = $this->drupalCreateUser([
'access administration pages',
'administer modules',
'administer blocks',
'create forum content',
]);
$this->drupalLogin($admin_user);
// Create a private node.
$private_node_title = $this->randomMachineName(20);
$edit = [
'title[0][value]' => $private_node_title,
'body[0][value]' => $this->randomMachineName(200),
'private[0][value]' => TRUE,
];
$this->drupalGet('node/add/forum', ['query' => ['forum_id' => 1]]);
$this->submitForm($edit, 'Save');
$private_node = $this->drupalGetNodeByTitle($private_node_title);
$this->assertNotEmpty($private_node, 'New private forum node found in database.');
// Create a public node.
$public_node_title = $this->randomMachineName(20);
$edit = [
'title[0][value]' => $public_node_title,
'body[0][value]' => $this->randomMachineName(200),
];
$this->drupalGet('node/add/forum', ['query' => ['forum_id' => 1]]);
$this->submitForm($edit, 'Save');
$public_node = $this->drupalGetNodeByTitle($public_node_title);
$this->assertNotEmpty($public_node, 'New public forum node found in database.');
// Enable the new and active forum blocks.
$this->drupalPlaceBlock('forum_active_block');
$this->drupalPlaceBlock('forum_new_block');
// Test for $access_user.
$this->drupalLogin($access_user);
$this->drupalGet('');
// Ensure private node and public node are found.
$this->assertSession()->pageTextContains($private_node->getTitle());
$this->assertSession()->pageTextContains($public_node->getTitle());
// Test for $no_access_user.
$this->drupalLogin($no_access_user);
$this->drupalGet('');
// Ensure private node is not found but public is found.
$this->assertSession()->pageTextNotContains($private_node->getTitle());
$this->assertSession()->pageTextContains($public_node->getTitle());
}
}

View File

@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\taxonomy\Entity\Term;
/**
* Tests forum taxonomy terms for access.
*
* @group forum
* @group legacy
*/
class ForumTermAccessTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'forum',
'taxonomy',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Creates some users and creates a public forum and an unpublished forum.
*
* Adds both published and unpublished forums.
* Tests to ensure publish/unpublished forums access is respected.
*/
public function testForumTermAccess(): void {
$assert_session = $this->assertSession();
// Create some users.
$public_user = $this->drupalCreateUser(['access content']);
$admin_user = $this->drupalCreateUser([
'access administration pages',
'administer forums',
'administer taxonomy',
'access taxonomy overview',
]);
$this->drupalLogin($admin_user);
// The vocabulary for forums.
$vid = $this->config('forum.settings')->get('vocabulary');
// Create an unpublished forum.
$unpublished_forum_name = $this->randomMachineName(8);
$unpublished_forum = Term::create([
'vid' => $vid,
'name' => $unpublished_forum_name,
'status' => 0,
]);
$unpublished_forum->save();
// Create a new published forum.
$published_forum_name = $this->randomMachineName(8);
$published_forum = Term::create([
'vid' => $vid,
'name' => $published_forum_name,
'status' => 1,
]);
$published_forum->save();
// Test for admin user.
// Go to the Forum index page.
$this->drupalGet('forum');
// The unpublished forum should be in this page for an admin user.
$assert_session->pageTextContains($unpublished_forum_name);
// Go to the unpublished forum page.
$this->drupalGet('forum/' . $unpublished_forum->id());
$assert_session->statusCodeEquals(200);
$assert_session->pageTextContains($unpublished_forum_name);
// Test for public user.
$this->drupalLogin($public_user);
// Go to the Forum index page.
$this->drupalGet('forum');
// The published forum should be in this page.
$assert_session->pageTextContains($published_forum_name);
// The unpublished forum should not be in this page.
$assert_session->pageTextNotContains($unpublished_forum_name);
// Go to the unpublished forum page.
$this->drupalGet('forum/' . $unpublished_forum->id());
// Public should not be able to access the unpublished forum.
$assert_session->statusCodeEquals(403);
$assert_session->pageTextNotContains($unpublished_forum_name);
}
}

View File

@@ -0,0 +1,753 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Functional;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\BrowserTestBase;
/**
* Tests for forum.module.
*
* Create, view, edit, delete, and change forum entries and verify its
* consistency in the database.
*
* @group forum
* @group legacy
* @group #slow
*/
class ForumTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'taxonomy',
'comment',
'forum',
'node',
'block',
'menu_ui',
'help',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'starterkit_theme';
/**
* A user with various administrative privileges.
*/
protected $adminUser;
/**
* A user that can create forum topics and edit its own topics.
*/
protected $editOwnTopicsUser;
/**
* A user that can create, edit, and delete forum topics.
*/
protected $editAnyTopicsUser;
/**
* A user with no special privileges.
*/
protected $webUser;
/**
* An administrative user who can bypass comment approval.
*/
protected $postCommentUser;
/**
* An array representing a forum container.
*/
protected $forumContainer;
/**
* An array representing a forum.
*/
protected $forum;
/**
* An array representing a root forum.
*/
protected $rootForum;
/**
* An array of forum topic node IDs.
*/
protected $nids;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->drupalPlaceBlock('system_breadcrumb_block');
$this->drupalPlaceBlock('page_title_block');
// Create users.
$this->adminUser = $this->drupalCreateUser([
'access administration pages',
'access help pages',
'administer modules',
'administer blocks',
'administer forums',
'administer menu',
'administer taxonomy',
'create forum content',
'access comments',
]);
$this->editAnyTopicsUser = $this->drupalCreateUser([
'access administration pages',
'access help pages',
'create forum content',
'edit any forum content',
'delete any forum content',
]);
$this->editOwnTopicsUser = $this->drupalCreateUser([
'create forum content',
'edit own forum content',
'delete own forum content',
]);
$this->webUser = $this->drupalCreateUser();
$this->postCommentUser = $this->drupalCreateUser([
'administer content types',
'create forum content',
'post comments',
'skip comment approval',
'access comments',
]);
$this->drupalPlaceBlock('help_block', ['region' => 'help']);
$this->drupalPlaceBlock('local_actions_block');
}
/**
* Tests forum functionality through the admin and user interfaces.
*/
public function testForum(): void {
// Check that the basic forum install creates a default forum topic
$this->drupalGet('/forum');
// Look for the "General discussion" default forum
$this->assertSession()->linkExists('General discussion');
$this->assertSession()->linkByHrefExists('/forum/1');
// Check the presence of expected cache tags.
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:forum.settings');
$this->drupalGet(Url::fromRoute('forum.page', ['taxonomy_term' => 1]));
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:forum.settings');
// Do the admin tests.
$this->doAdminTests($this->adminUser);
// Check display order.
$display = EntityViewDisplay::load('node.forum.default');
$body = $display->getComponent('body');
$comment = $display->getComponent('comment_forum');
$taxonomy = $display->getComponent('taxonomy_forums');
// Assert field order is body » taxonomy » comments.
$this->assertLessThan($body['weight'], $taxonomy['weight']);
$this->assertLessThan($comment['weight'], $body['weight']);
// Check form order.
$display = EntityFormDisplay::load('node.forum.default');
$body = $display->getComponent('body');
$comment = $display->getComponent('comment_forum');
$taxonomy = $display->getComponent('taxonomy_forums');
// Assert category comes before body in order.
$this->assertLessThan($body['weight'], $taxonomy['weight']);
$this->generateForumTopics();
// Log in an unprivileged user to view the forum topics and generate an
// active forum topics list.
$this->drupalLogin($this->webUser);
// Verify that this user is shown a message that they may not post content.
$this->drupalGet('forum/' . $this->forum['tid']);
$this->assertSession()->pageTextContains('You are not allowed to post new content in the forum');
// Log in, and do basic tests for a user with permission to edit any forum
// content.
$this->doBasicTests($this->editAnyTopicsUser, TRUE);
// Create a forum node authored by this user.
$any_topics_user_node = $this->createForumTopic($this->forum, FALSE);
// Log in, and do basic tests for a user with permission to edit only its
// own forum content.
$this->doBasicTests($this->editOwnTopicsUser, FALSE);
// Create a forum node authored by this user.
$own_topics_user_node = $this->createForumTopic($this->forum, FALSE);
// Verify that this user cannot edit forum content authored by another user.
$this->verifyForums($any_topics_user_node, FALSE, 403);
// Verify that this user is shown a local task to add new forum content.
$this->drupalGet('forum');
$this->assertSession()->linkExists('Add new Forum topic');
$this->drupalGet('forum/' . $this->forum['tid']);
$this->assertSession()->linkExists('Add new Forum topic');
// Log in a user with permission to edit any forum content.
$this->drupalLogin($this->editAnyTopicsUser);
// Verify that this user can edit forum content authored by another user.
$this->verifyForums($own_topics_user_node, TRUE);
// Verify the topic and post counts on the forum page.
$this->drupalGet('forum');
// Find the table row for the forum that has new posts. This cannot be
// reliably identified by any CSS selector or its position in the table,
// so look for an element with the "New posts" title and traverse up the
// document tree until we get to the table row that contains it.
$row = $this->assertSession()->elementExists('css', '[title="New posts"]');
while ($row && $row->getTagName() !== 'tr') {
$row = $row->getParent();
}
$this->assertNotEmpty($row);
$cells = $row->findAll('css', 'td');
$this->assertCount(4, $cells);
// Topics cell contains number of topics (6), number of unread topics (also
// 6), and the forum name.
$this->assertEquals('6 6 new posts in forum ' . $this->forum['name'], $cells[1]->getText(), 'Number of topics found.');
// Verify total number of posts in forum.
$this->assertEquals('6', $cells[2]->getText(), 'Number of posts found.');
// Test loading multiple forum nodes on the front page.
$this->drupalLogin($this->drupalCreateUser([
'administer content types',
'create forum content',
'post comments',
]));
$this->drupalGet('admin/structure/types/manage/forum');
$this->submitForm(['options[promote]' => 'promote'], 'Save');
$this->createForumTopic($this->forum, FALSE);
$this->createForumTopic($this->forum, FALSE);
$this->drupalGet('node');
// Test adding a comment to a forum topic.
$node = $this->createForumTopic($this->forum, FALSE);
$edit = [];
$edit['comment_body[0][value]'] = $this->randomMachineName();
$this->drupalGet('node/' . $node->id());
$this->submitForm($edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
// Test editing a forum topic that has a comment.
$this->drupalLogin($this->editAnyTopicsUser);
$this->drupalGet('forum/' . $this->forum['tid']);
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm([], 'Save');
$this->assertSession()->statusCodeEquals(200);
// Test the root forum page title change.
$this->drupalGet('forum');
$this->assertSession()->responseHeaderContains('X-Drupal-Cache-Tags', 'config:taxonomy.vocabulary.' . $this->forum['vid']);
$this->assertSession()->titleEquals('Forums | Drupal');
$vocabulary = Vocabulary::load($this->forum['vid']);
$vocabulary->set('name', 'Discussions');
$vocabulary->save();
$this->drupalGet('forum');
$this->assertSession()->titleEquals('Discussions | Drupal');
// Test anonymous action link.
$this->drupalLogout();
$this->drupalGet('forum/' . $this->forum['tid']);
$this->assertSession()->linkExists('Log in to post new content in the forum.');
}
/**
* Tests that forum nodes can't be added without a parent.
*
* Verifies that forum nodes are not created without choosing "forum" from the
* select list.
*/
public function testAddOrphanTopic(): void {
// Must remove forum topics to test creating orphan topics.
$vid = $this->config('forum.settings')->get('vocabulary');
$tids = \Drupal::entityQuery('taxonomy_term')
->accessCheck(FALSE)
->condition('vid', $vid)
->execute();
$term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
$terms = $term_storage->loadMultiple($tids);
$term_storage->delete($terms);
// Create an orphan forum item.
$edit = [];
$edit['title[0][value]'] = $this->randomMachineName(10);
$edit['body[0][value]'] = $this->randomMachineName(120);
$this->drupalLogin($this->adminUser);
$this->drupalGet('node/add/forum');
$this->submitForm($edit, 'Save');
$nid_count = $this->container->get('entity_type.manager')
->getStorage('node')
->getQuery()
->accessCheck(FALSE)
->count()
->execute();
$this->assertEquals(0, $nid_count, 'A forum node was not created when missing a forum vocabulary.');
// Reset the defaults for future tests.
\Drupal::service('module_installer')->install(['forum']);
}
/**
* Runs admin tests on the admin user.
*
* @param object $user
* The logged-in user.
*/
private function doAdminTests($user) {
// Log in the user.
$this->drupalLogin($user);
// Add forum to the Tools menu.
$edit = [];
$this->drupalGet('admin/structure/menu/manage/tools');
$this->submitForm($edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
// Edit forum taxonomy.
// Restoration of the settings fails and causes subsequent tests to fail.
$this->editForumVocabulary();
// Create forum container.
$this->forumContainer = $this->createForum('container');
// Verify "edit container" link exists and functions correctly.
$this->drupalGet('admin/structure/forum');
// Verify help text is shown.
$this->assertSession()->pageTextContains('Forums contain forum topics. Use containers to group related forums');
// Verify action links are there.
$this->assertSession()->linkExists('Add forum');
$this->assertSession()->linkExists('Add container');
$this->clickLink('edit container');
$this->assertSession()->pageTextContains('Edit container');
// Create forum inside the forum container.
$this->forum = $this->createForum('forum', $this->forumContainer['tid']);
// Verify the "edit forum" link exists and functions correctly.
$this->drupalGet('admin/structure/forum');
$this->clickLink('edit forum');
$this->assertSession()->pageTextContains('Edit forum');
// Navigate back to forum structure page.
$this->drupalGet('admin/structure/forum');
// Create second forum in container, destined to be deleted below.
$delete_forum = $this->createForum('forum', $this->forumContainer['tid']);
// Save forum overview.
$this->drupalGet('admin/structure/forum/');
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('The configuration options have been saved.');
// Delete this second forum.
$this->deleteForum($delete_forum['tid']);
// Create forum at the top (root) level.
$this->rootForum = $this->createForum('forum');
// Test vocabulary form alterations.
$this->drupalGet('admin/structure/taxonomy/manage/forums');
$this->assertSession()->buttonExists('Save');
$this->assertSession()->buttonNotExists('Delete');
// Test term edit form alterations.
$this->drupalGet('taxonomy/term/' . $this->forumContainer['tid'] . '/edit');
// Test parent field been hidden by forum module.
$this->assertSession()->fieldNotExists('parent[]');
// Create a default vocabulary named "Tags".
$description = 'Use tags to group articles on similar topics into categories.';
$help = 'Enter a comma-separated list of words to describe your content.';
$vocabulary = Vocabulary::create([
'name' => 'Tags',
'description' => $description,
'vid' => 'tags',
'langcode' => \Drupal::languageManager()->getDefaultLanguage()->getId(),
'help' => $help,
]);
$vocabulary->save();
// Test tags vocabulary form is not affected.
$this->drupalGet('admin/structure/taxonomy/manage/tags');
$this->assertSession()->buttonExists('Save');
$this->assertSession()->linkExists('Delete');
// Test tags vocabulary term form is not affected.
$this->drupalGet('admin/structure/taxonomy/manage/tags/add');
$this->assertSession()->fieldExists('parent[]');
// Test relations widget exists.
$this->assertSession()->elementExists('xpath', "//details[@id='edit-relations']");
}
/**
* Edits the forum taxonomy.
*/
public function editForumVocabulary() {
// Backup forum taxonomy.
$vid = $this->config('forum.settings')->get('vocabulary');
$original_vocabulary = Vocabulary::load($vid);
// Generate a random name and description.
$edit = [
'name' => $this->randomMachineName(10),
'description' => $this->randomMachineName(100),
];
// Edit the vocabulary.
$this->drupalGet('admin/structure/taxonomy/manage/' . $original_vocabulary->id());
$this->submitForm($edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains("Updated vocabulary {$edit['name']}.");
// Grab the newly edited vocabulary.
$current_vocabulary = Vocabulary::load($vid);
// Make sure we actually edited the vocabulary properly.
$this->assertEquals($edit['name'], $current_vocabulary->label(), 'The name was updated');
$this->assertEquals($edit['description'], $current_vocabulary->getDescription(), 'The description was updated');
// Restore the original vocabulary's name and description.
$current_vocabulary->set('name', $original_vocabulary->label());
$current_vocabulary->set('description', $original_vocabulary->getDescription());
$current_vocabulary->save();
// Reload vocabulary to make sure changes are saved.
$current_vocabulary = Vocabulary::load($vid);
$this->assertEquals($original_vocabulary->label(), $current_vocabulary->label(), 'The original vocabulary settings were restored');
}
/**
* Creates a forum container or a forum.
*
* @param string $type
* The forum type (forum container or forum).
* @param int $parent
* The forum parent. This defaults to 0, indicating a root forum.
*
* @return \Drupal\Core\Database\StatementInterface
* The created taxonomy term data.
*/
public function createForum($type, $parent = 0) {
// Generate a random name/description.
$name = $this->randomMachineName(10);
$description = $this->randomMachineName(100);
$edit = [
'name[0][value]' => $name,
'description[0][value]' => $description,
'parent[0]' => $parent,
'weight' => '0',
];
// Create forum.
$this->drupalGet('admin/structure/forum/add/' . $type);
$this->submitForm($edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
$type = ($type == 'container') ? 'forum container' : 'forum';
$this->assertSession()->pageTextContains('Created new ' . $type . ' ' . $name . '.');
// Verify that the creation message contains a link to a term.
$this->assertSession()->elementExists('xpath', '//div[@data-drupal-messages]//a[contains(@href, "term/")]');
/** @var \Drupal\taxonomy\TermStorageInterface $taxonomy_term_storage */
$taxonomy_term_storage = $this->container->get('entity_type.manager')->getStorage('taxonomy_term');
// Verify forum.
$term = $taxonomy_term_storage->loadByProperties([
'vid' => $this->config('forum.settings')->get('vocabulary'),
'name' => $name,
'description__value' => $description,
]);
$term = array_shift($term);
$this->assertNotEmpty($term, "The forum type '$type' should exist in the database.");
// Verify forum hierarchy.
$tid = $term->id();
$parent_tid = $taxonomy_term_storage->loadParents($tid);
$parent_tid = empty($parent_tid) ? 0 : array_shift($parent_tid)->id();
$this->assertSame($parent, $parent_tid, 'The ' . $type . ' is linked to its container');
$forum = $taxonomy_term_storage->load($tid);
$this->assertEquals(($type == 'forum container'), (bool) $forum->forum_container->value);
return [
'tid' => $tid,
'name' => $term->getName(),
'vid' => $term->bundle(),
];
}
/**
* Deletes a forum.
*
* @param int $tid
* The forum ID.
*/
public function deleteForum($tid) {
// Delete the forum.
$this->drupalGet('admin/structure/forum/edit/forum/' . $tid);
$this->clickLink('Delete');
$this->assertSession()->pageTextContains('Are you sure you want to delete the forum');
$this->assertSession()->pageTextNotContains('Add forum');
$this->assertSession()->pageTextNotContains('Add forum container');
$this->submitForm([], 'Delete');
// Assert that the forum no longer exists.
$this->drupalGet('forum/' . $tid);
$this->assertSession()->statusCodeEquals(404);
}
/**
* Runs basic tests on the indicated user.
*
* @param \Drupal\Core\Session\AccountInterface $user
* The logged in user.
* @param bool $admin
* User has 'access administration pages' privilege.
*/
private function doBasicTests($user, $admin) {
// Log in the user.
$this->drupalLogin($user);
// Attempt to create forum topic under a container.
$this->createForumTopic($this->forumContainer, TRUE);
// Create forum node.
$node = $this->createForumTopic($this->forum, FALSE);
// Verify the user has access to all the forum nodes.
$this->verifyForums($node, $admin);
}
/**
* Tests a forum with a new post displays properly.
*/
public function testForumWithNewPost(): void {
// Log in as the first user.
$this->drupalLogin($this->adminUser);
// Create a forum container.
$this->forumContainer = $this->createForum('container');
// Create a forum.
$this->forum = $this->createForum('forum');
// Create a topic.
$node = $this->createForumTopic($this->forum, FALSE);
// Log in as a second user.
$this->drupalLogin($this->postCommentUser);
// Post a reply to the topic.
$edit = [];
$edit['subject[0][value]'] = $this->randomMachineName();
$edit['comment_body[0][value]'] = $this->randomMachineName();
$this->drupalGet('node/' . $node->id());
$this->submitForm($edit, 'Save');
$this->assertSession()->statusCodeEquals(200);
// Test adding a new comment.
$this->clickLink('Add new comment');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->fieldExists('comment_body[0][value]');
// Log in as the first user.
$this->drupalLogin($this->adminUser);
// Check that forum renders properly.
$this->drupalGet("forum/{$this->forum['tid']}");
$this->assertSession()->statusCodeEquals(200);
// Verify there is no unintentional HTML tag escaping.
$this->assertSession()->assertNoEscaped('<');
}
/**
* Creates a forum topic.
*
* @param array $forum
* A forum array.
* @param bool $container
* TRUE if $forum is a container; FALSE otherwise.
*
* @return object|null
* The created topic node or NULL if the forum is a container.
*/
public function createForumTopic($forum, $container = FALSE) {
// Generate a random subject/body.
$title = $this->randomMachineName(20);
$body = $this->randomMachineName(200);
$edit = [
'title[0][value]' => $title,
'body[0][value]' => $body,
];
$tid = $forum['tid'];
// Create the forum topic, preselecting the forum ID via a URL parameter.
$this->drupalGet('node/add/forum', ['query' => ['forum_id' => $tid]]);
$this->submitForm($edit, 'Save');
if ($container) {
$this->assertSession()->pageTextNotContains("Forum topic $title has been created.");
$this->assertSession()->pageTextContains("The item {$forum['name']} is a forum container, not a forum.");
return;
}
else {
$this->assertSession()->pageTextContains("Forum topic $title has been created.");
$this->assertSession()->pageTextNotContains("The item {$forum['name']} is a forum container, not a forum.");
// Verify that the creation message contains a link to a node.
$this->assertSession()->elementExists('xpath', '//div[@data-drupal-messages]//a[contains(@href, "node/")]');
}
// Retrieve node object, ensure that the topic was created and in the proper forum.
$node = $this->drupalGetNodeByTitle($title);
$this->assertNotNull($node, "Node $title was loaded");
$this->assertEquals($tid, $node->taxonomy_forums->target_id, 'Saved forum topic was in the expected forum');
// View forum topic.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->pageTextContains($title);
$this->assertSession()->pageTextContains($body);
return $node;
}
/**
* Verifies that the logged in user has access to a forum node.
*
* @param \Drupal\Core\Entity\EntityInterface $node
* The node being checked.
* @param bool $admin
* Boolean to indicate whether the user can 'access administration pages'.
* @param int $response
* The expected HTTP response code.
*/
private function verifyForums(EntityInterface $node, $admin, $response = 200) {
$response2 = ($admin) ? 200 : 403;
// View forum help node.
$this->drupalGet('admin/help/forum');
$this->assertSession()->statusCodeEquals($response2);
if ($response2 == 200) {
$this->assertSession()->titleEquals('Forum | Drupal');
$this->assertSession()->pageTextContains('Forum');
}
// View forum container page.
$this->verifyForumView($this->forumContainer);
// View forum page.
$this->verifyForumView($this->forum, $this->forumContainer);
// View root forum page.
$this->verifyForumView($this->rootForum);
// View forum node.
$this->drupalGet('node/' . $node->id());
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->titleEquals($node->label() . ' | Drupal');
$breadcrumb_build = [
Link::createFromRoute('Home', '<front>'),
Link::createFromRoute('Forums', 'forum.index'),
Link::createFromRoute($this->forumContainer['name'], 'forum.page', ['taxonomy_term' => $this->forumContainer['tid']]),
Link::createFromRoute($this->forum['name'], 'forum.page', ['taxonomy_term' => $this->forum['tid']]),
];
$breadcrumb = [
'#theme' => 'breadcrumb',
'#links' => $breadcrumb_build,
];
$this->assertSession()->responseContains(\Drupal::service('renderer')->renderRoot($breadcrumb));
// View forum edit node.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertSession()->statusCodeEquals($response);
if ($response == 200) {
$this->assertSession()->titleEquals('Edit Forum topic ' . $node->label() . ' | Drupal');
}
if ($response == 200) {
// Edit forum node (including moving it to another forum).
$edit = [];
$edit['title[0][value]'] = 'node/' . $node->id();
$edit['body[0][value]'] = $this->randomMachineName(256);
// Assume the topic is initially associated with $forum.
$edit['taxonomy_forums'] = $this->rootForum['tid'];
$edit['shadow'] = TRUE;
$this->drupalGet('node/' . $node->id() . '/edit');
$this->submitForm($edit, 'Save');
$this->assertSession()->pageTextContains('Forum topic ' . $edit['title[0][value]'] . ' has been updated.');
// Verify topic was moved to a different forum.
$forum_tid = $this->container
->get('database')
->select('forum', 'f')
->fields('f', ['tid'])
->condition('nid', $node->id())
->condition('vid', $node->getRevisionId())
->execute()
->fetchField();
$this->assertSame($this->rootForum['tid'], $forum_tid, 'The forum topic is linked to a different forum');
// Delete forum node.
$this->drupalGet('node/' . $node->id() . '/delete');
$this->submitForm([], 'Delete');
$this->assertSession()->statusCodeEquals($response);
$this->assertSession()->pageTextContains("Forum topic {$edit['title[0][value]']} has been deleted.");
}
}
/**
* Verifies the display of a forum page.
*
* @param array $forum
* A row from the taxonomy_term_data table in an array.
* @param array $parent
* (optional) An array representing the forum's parent.
*/
private function verifyForumView($forum, $parent = NULL) {
// View forum page.
$this->drupalGet('forum/' . $forum['tid']);
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->titleEquals($forum['name'] . ' | Drupal');
$breadcrumb_build = [
Link::createFromRoute('Home', '<front>'),
Link::createFromRoute('Forums', 'forum.index'),
];
if (isset($parent)) {
$breadcrumb_build[] = Link::createFromRoute($parent['name'], 'forum.page', ['taxonomy_term' => $parent['tid']]);
}
$breadcrumb = [
'#theme' => 'breadcrumb',
'#links' => $breadcrumb_build,
];
$this->assertSession()->responseContains(\Drupal::service('renderer')->renderRoot($breadcrumb));
}
/**
* Generates forum topics.
*/
private function generateForumTopics() {
$this->nids = [];
for ($i = 0; $i < 5; $i++) {
$node = $this->createForumTopic($this->forum, FALSE);
$this->nids[] = $node->id();
}
}
/**
* Evaluate whether "Add new Forum topic" button is present or not.
*/
public function testForumTopicButton(): void {
$this->drupalLogin($this->adminUser);
// Validate that link doesn't exist on the forum container page.
$forum_container = $this->createForum('container');
$this->drupalGet('forum/' . $forum_container['tid']);
$this->assertSession()->linkNotExists('Add new Forum topic');
// Validate that link exists on forum page.
$forum = $this->createForum('forum');
$this->drupalGet('forum/' . $forum['tid']);
$this->assertSession()->linkExists('Add new Forum topic');
}
}

View File

@@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Functional;
use Drupal\comment\CommentInterface;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\node\Entity\NodeType;
use Drupal\comment\Entity\Comment;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\BrowserTestBase;
/**
* Tests forum module uninstallation.
*
* @group forum
* @group legacy
* @group #slow
*/
class ForumUninstallTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['forum'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests if forum module uninstallation properly deletes the field.
*/
public function testForumUninstallWithField(): void {
$this->drupalLogin($this->drupalCreateUser([
'administer taxonomy',
'administer nodes',
'administer modules',
'delete any forum content',
'administer content types',
]));
// Ensure that the field exists before uninstallation.
$field_storage = FieldStorageConfig::loadByName('node', 'taxonomy_forums');
$this->assertNotNull($field_storage, 'The taxonomy_forums field storage exists.');
// Create a taxonomy term.
$term = Term::create([
'name' => 'A term',
'langcode' => \Drupal::languageManager()->getDefaultLanguage()->getId(),
'description' => '',
'parent' => [0],
'vid' => 'forums',
'forum_container' => 0,
]);
$term->save();
// Create a forum node.
$node = $this->drupalCreateNode([
'title' => 'A forum post',
'type' => 'forum',
'taxonomy_forums' => [['target_id' => $term->id()]],
]);
// Create at least one comment against the forum node.
$comment = Comment::create([
'entity_id' => $node->nid->value,
'entity_type' => 'node',
'field_name' => 'comment_forum',
'pid' => 0,
'uid' => 0,
'status' => CommentInterface::PUBLISHED,
'subject' => $this->randomMachineName(),
'hostname' => '127.0.0.1',
]);
$comment->save();
// Attempt to uninstall forum.
$this->drupalGet('admin/modules/uninstall');
// Assert forum is required.
$this->assertSession()->fieldDisabled('uninstall[forum]');
$this->assertSession()->pageTextContains('To uninstall Forum, first delete all Forum content');
// Delete the node.
$this->drupalGet('node/' . $node->id() . '/delete');
$this->submitForm([], 'Delete');
// Attempt to uninstall forum.
$this->drupalGet('admin/modules/uninstall');
// Assert forum is still required.
$this->assertSession()->fieldDisabled('uninstall[forum]');
$this->assertSession()->pageTextContains('To uninstall Forum, first delete all Forums terms');
// Delete any forum terms.
$vid = $this->config('forum.settings')->get('vocabulary');
$storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
$terms = $storage->loadByProperties(['vid' => $vid]);
$storage->delete($terms);
// Ensure that the forum node type can not be deleted.
$this->drupalGet('admin/structure/types/manage/forum');
$this->assertSession()->linkNotExists('Delete');
// Now attempt to uninstall forum.
$this->drupalGet('admin/modules/uninstall');
// Assert forum is no longer required.
$this->assertSession()->fieldExists('uninstall[forum]');
$this->drupalGet('admin/modules/uninstall');
$this->submitForm(['uninstall[forum]' => 1], 'Uninstall');
$this->submitForm([], 'Uninstall');
// Check that the field is now deleted.
$field_storage = FieldStorageConfig::loadByName('node', 'taxonomy_forums');
$this->assertNull($field_storage, 'The taxonomy_forums field storage has been deleted.');
// Check that a node type with a machine name of forum can be created after
// uninstalling the forum module and the node type is not locked.
$edit = [
'name' => 'Forum',
'title_label' => 'title for forum',
'type' => 'forum',
];
$this->drupalGet('admin/structure/types/add');
$this->submitForm($edit, 'Save');
$this->assertTrue((bool) NodeType::load('forum'), 'Node type with machine forum created.');
$this->drupalGet('admin/structure/types/manage/forum');
$this->clickLink('Delete');
$this->submitForm([], 'Delete');
$this->assertSession()->statusCodeEquals(200);
$this->assertFalse((bool) NodeType::load('forum'), 'Node type with machine forum deleted.');
// Double check everything by reinstalling the forum module again.
$this->drupalGet('admin/modules');
$this->submitForm(['modules[forum][enable]' => 1], 'Install');
$this->submitForm([], 'Continue');
$this->assertSession()->pageTextContains('Module Forum has been installed.');
}
/**
* Tests uninstallation if the field storage has been deleted beforehand.
*/
public function testForumUninstallWithoutFieldStorage(): void {
// Manually delete the taxonomy_forums field before module uninstallation.
$field_storage = FieldStorageConfig::loadByName('node', 'taxonomy_forums');
$this->assertNotNull($field_storage, 'The taxonomy_forums field storage exists.');
$field_storage->delete();
// Check that the field is now deleted.
$field_storage = FieldStorageConfig::loadByName('node', 'taxonomy_forums');
$this->assertNull($field_storage, 'The taxonomy_forums field storage has been deleted.');
// Delete all terms in the Forums vocabulary. Uninstalling the forum module
// will fail unless this is done.
$terms = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadByProperties(['vid' => 'forums']);
foreach ($terms as $term) {
$term->delete();
}
// Ensure that uninstallation succeeds even if the field has already been
// deleted manually beforehand.
$this->container->get('module_installer')->uninstall(['forum']);
}
/**
* Tests uninstallation of forum module when vocabulary is deleted.
*/
public function testForumUninstallWithoutForumVocabulary(): void {
$this->drupalLogin($this->drupalCreateUser([
'administer modules',
]));
Vocabulary::load('forums')->delete();
// Now attempt to uninstall forum.
$this->drupalGet('admin/modules/uninstall');
$this->assertSession()->responseNotContains('The website encountered an unexpected error. Try again later');
$this->assertSession()->statusCodeEquals(200);
// Assert forum is no longer required.
$this->assertSession()->fieldExists('uninstall[forum]');
$this->drupalGet('admin/modules/uninstall');
$this->submitForm(['uninstall[forum]' => 1], 'Uninstall');
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for forum.
*
* @group forum
* @group legacy
*/
class GenericTest extends GenericModuleTestBase {
/**
* {@inheritdoc}
*/
protected function preUninstallSteps(): void {
$storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
$terms = $storage->loadMultiple();
$storage->delete($terms);
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Functional\Module;
use Drupal\Tests\system\Functional\Module\ModuleTestBase;
/**
* Enable module without dependency enabled.
*
* @group form
* @group legacy
*/
class DependencyTest extends ModuleTestBase {
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests attempting to uninstall a module that has installed dependents.
*/
public function testUninstallDependents(): void {
// Enable the forum module.
$edit = ['modules[forum][enable]' => 'forum'];
$this->drupalGet('admin/modules');
$this->submitForm($edit, 'Install');
$this->submitForm([], 'Continue');
$this->assertModules(['forum'], TRUE);
// Check that the comment module cannot be uninstalled.
$this->drupalGet('admin/modules/uninstall');
$this->assertSession()->fieldDisabled('uninstall[comment]');
// Delete any forum terms.
$vid = $this->config('forum.settings')->get('vocabulary');
// Ensure taxonomy has been loaded into the test-runner after forum was
// enabled.
\Drupal::moduleHandler()->load('taxonomy');
$storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
$terms = $storage->loadByProperties(['vid' => $vid]);
$storage->delete($terms);
// Uninstall the forum module, and check that taxonomy now can also be
// uninstalled.
$edit = ['uninstall[forum]' => 'forum'];
$this->drupalGet('admin/modules/uninstall');
$this->submitForm($edit, 'Uninstall');
$this->submitForm([], 'Uninstall');
$this->assertSession()->pageTextContains('The selected modules have been uninstalled.');
// Uninstall comment module.
$edit = ['uninstall[comment]' => 'comment'];
$this->drupalGet('admin/modules/uninstall');
$this->submitForm($edit, 'Uninstall');
$this->submitForm([], 'Uninstall');
$this->assertSession()->pageTextContains('The selected modules have been uninstalled.');
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Tests access controlled node views have the right amount of comment pages.
*
* @group form
* @group legacy
*/
class NodeAccessPagerTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['node_access_test', 'forum'];
/**
* A user account to use for the test.
*
* @var \Drupal\user\Entity\User
*/
protected $webUser;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
protected function setUp(): void {
parent::setUp();
node_access_rebuild();
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
$this->webUser = $this->drupalCreateUser([
'access content',
'node test view',
]);
}
/**
* Tests the forum node pager for nodes with multiple grants per realm.
*/
public function testForumPager(): void {
// Look up the forums vocabulary ID.
$vid = $this->config('forum.settings')->get('vocabulary');
$this->assertNotEmpty($vid, 'Forum navigation vocabulary ID is set.');
// Look up the general discussion term.
$tree = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->loadTree($vid, 0, 1);
$tid = reset($tree)->tid;
$this->assertNotEmpty($tid, 'General discussion term is found in the forum vocabulary.');
// Create 30 nodes.
for ($i = 0; $i < 30; $i++) {
$this->drupalCreateNode([
'nid' => NULL,
'type' => 'forum',
'taxonomy_forums' => [
['target_id' => $tid],
],
]);
}
// View the general discussion forum page. With the default 25 nodes per
// page there should be two pages for 30 nodes, no more.
$this->drupalLogin($this->webUser);
$this->drupalGet('forum/' . $tid);
$this->assertSession()->responseContains('page=1');
$this->assertSession()->responseNotContains('page=2');
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Functional;
use Drupal\Core\Database\Database;
use Drupal\Tests\BrowserTestBase;
use Drupal\taxonomy\Entity\Term;
/**
* Tests altering the inbound path and the outbound path.
*
* @group form
* @group legacy
*/
class UrlAlterFunctionalTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['path', 'forum', 'forum_url_alter_test'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that URL altering works and that it occurs in the correct order.
*/
public function testUrlAlter(): void {
// Ensure that the path_alias table exists after Drupal installation.
$this->assertTrue(Database::getConnection()->schema()->tableExists('path_alias'), 'The path_alias table exists after Drupal installation.');
// Test that 'forum' is altered to 'community' correctly, both at the root
// level and for a specific existing forum.
$this->drupalGet('community');
$this->assertSession()->pageTextContains('General discussion');
$this->assertUrlOutboundAlter('/forum', '/community');
$forum_vid = $this->config('forum.settings')->get('vocabulary');
$term_name = $this->randomMachineName();
$term = Term::create([
'name' => $term_name,
'vid' => $forum_vid,
]);
$term->save();
$this->drupalGet("community/" . $term->id());
$this->assertSession()->pageTextContains($term_name);
$this->assertUrlOutboundAlter("/forum/" . $term->id(), "/community/" . $term->id());
}
/**
* Assert that an outbound path is altered to an expected value.
*
* @param string $original
* A string with the original path that is run through generateFrommPath().
* @param string $final
* A string with the expected result after generateFrommPath().
*
* @internal
*/
protected function assertUrlOutboundAlter(string $original, string $final): void {
// Test outbound altering.
$result = $this->container->get('path_processor_manager')->processOutbound($original);
$this->assertSame($final, $result, "Altered outbound URL $original, expected $final, and got $result.");
}
/**
* Assert that an inbound path is altered to an expected value.
*
* @param string $original
* The original path before it has been altered by inbound URL processing.
* @param string $final
* A string with the expected result.
*
* @internal
*/
protected function assertUrlInboundAlter(string $original, string $final): void {
// Test inbound altering.
$result = $this->container->get('path_alias.manager')->getPathByAlias($original);
$this->assertSame($final, $result, "Altered inbound URL $original, expected $final, and got $result.");
}
}

View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Functional\Views;
use Drupal\node\NodeInterface;
use Drupal\views\Views;
use Drupal\Tests\views\Functional\ViewTestBase;
/**
* Tests the forum integration into views.
*
* @group forum
* @group legacy
*/
class ForumIntegrationTest extends ViewTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['forum_test_views'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_forum_index'];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE, $modules = ['forum_test_views']): void {
parent::setUp($import_test_views, $modules);
}
/**
* Tests the integration.
*/
public function testForumIntegration(): void {
// Create a forum.
$entity_type_manager = $this->container->get('entity_type.manager');
$term = $entity_type_manager->getStorage('taxonomy_term')->create(['vid' => 'forums', 'name' => $this->randomMachineName()]);
$term->save();
$comment_storage = $entity_type_manager->getStorage('comment');
// Create some nodes which are part of this forum with some comments.
$nodes = [];
for ($i = 0; $i < 3; $i++) {
$node = $this->drupalCreateNode(['type' => 'forum', 'taxonomy_forums' => [$term->id()], 'sticky' => $i == 0 ? NodeInterface::STICKY : NodeInterface::NOT_STICKY]);
$nodes[] = $node;
}
$account = $this->drupalCreateUser(['skip comment approval']);
$this->drupalLogin($account);
$comments = [];
foreach ($nodes as $index => $node) {
for ($i = 0; $i <= $index; $i++) {
$comment = $comment_storage->create(['entity_type' => 'node', 'entity_id' => $node->id(), 'field_name' => 'comment_forum']);
$comment->save();
$comments[$comment->get('entity_id')->target_id][$comment->id()] = $comment;
}
}
$view = Views::getView('test_forum_index');
$this->executeView($view);
$expected_result = [];
$expected_result[] = [
'nid' => $nodes[0]->id(),
'sticky' => NodeInterface::STICKY,
'comment_count' => 1.,
];
$expected_result[] = [
'nid' => $nodes[1]->id(),
'sticky' => NodeInterface::NOT_STICKY,
'comment_count' => 2.,
];
$expected_result[] = [
'nid' => $nodes[2]->id(),
'sticky' => NodeInterface::NOT_STICKY,
'comment_count' => 3.,
];
$column_map = [
'nid' => 'nid',
'forum_index_sticky' => 'sticky',
'forum_index_comment_count' => 'comment_count',
];
$this->assertIdenticalResultset($view, $expected_result, $column_map);
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Functional\migrate_drupal\d6;
use Drupal\Tests\migrate_drupal_ui\Functional\NoMultilingualReviewPageTestBase;
/**
* Tests migrate upgrade review page.
*
* @group forum
* @group legacy
*/
class NoMultilingualReviewPageTest extends NoMultilingualReviewPageTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'forum',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->loadFixture($this->getModulePath('forum') . '/tests/fixtures/drupal6.php');
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
return __DIR__;
}
/**
* Tests that Forum is displayed in the will be upgraded list.
*/
public function testMigrateUpgradeReviewPage(): void {
$this->prepare();
// Start the upgrade process.
$this->submitCredentialForm();
$session = $this->assertSession();
$this->submitForm([], 'I acknowledge I may lose data. Continue anyway.');
$session->statusCodeEquals(200);
// Confirm that Forum will be upgraded.
$session->elementExists('xpath', "//td[contains(@class, 'checked') and text() = 'Forum']");
$session->elementNotExists('xpath', "//td[contains(@class, 'error') and text() = 'Forum']");
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getIncompletePaths() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths() {
return [];
}
}

View File

@@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Functional\migrate_drupal\d6;
use Drupal\Tests\migrate_drupal_ui\Functional\MigrateUpgradeExecuteTestBase;
/**
* Tests Drupal 6 upgrade using the migrate UI.
*
* The test method is provided by the MigrateUpgradeTestBase class.
*
* @group forum
* @group #slow
* @group legacy
*/
class Upgrade6Test extends MigrateUpgradeExecuteTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'forum',
'migrate_drupal_ui',
];
/**
* The entity storage for node.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $nodeStorage;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->loadFixture($this->getModulePath('forum') . '/tests/fixtures/drupal6.php');
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
return __DIR__ . '/files';
}
/**
* {@inheritdoc}
*/
protected function getEntityCounts() {
return [
'action' => 24,
'base_field_override' => 22,
'block' => 33,
'block_content' => 1,
'block_content_type' => 1,
'comment' => 4,
'comment_type' => 8,
'contact_form' => 2,
'contact_message' => 0,
'date_format' => 12,
'editor' => 2,
'entity_form_display' => 18,
'entity_form_mode' => 1,
'entity_view_display' => 34,
'entity_view_mode' => 11,
'field_config' => 41,
'field_storage_config' => 25,
'file' => 1,
'filter_format' => 7,
'image_style' => 6,
'menu' => 8,
'menu_link_content' => 1,
'node' => 3,
'node_type' => 7,
'path_alias' => 4,
'search_page' => 3,
'shortcut' => 2,
'shortcut_set' => 1,
'taxonomy_term' => 7,
'taxonomy_vocabulary' => 4,
'user' => 3,
'user_role' => 4,
'view' => 14,
];
}
/**
* {@inheritdoc}
*/
protected function getEntityCountsIncremental() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths() {
return [
'Block',
'Comment',
'Content',
'Date',
'Date API',
'Date Timezone',
'Email',
'Event',
'FileField',
'Filter',
'Forum',
'ImageAPI',
'ImageCache',
'ImageField',
'Menu',
'Node',
'Path',
'Search',
'System',
'Taxonomy',
'Text',
'Upload',
'User',
'Variable admin',
];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths() {
return [];
}
/**
* Executes all steps of migrations upgrade.
*/
public function testUpgrade(): void {
// Start the upgrade process.
$this->submitCredentialForm();
$session = $this->assertSession();
$this->submitForm([], 'I acknowledge I may lose data. Continue anyway.');
$session->statusCodeEquals(200);
// Test the review form.
$this->assertReviewForm();
$this->submitForm([], 'Perform upgrade');
$this->assertUpgrade($this->getEntityCounts());
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Functional\migrate_drupal\d7;
use Drupal\Tests\migrate_drupal_ui\Functional\NoMultilingualReviewPageTestBase;
/**
* Tests Drupal 7 upgrade without translations.
*
* The test method is provided by the MigrateUpgradeTestBase class.
*
* @group forum
* @group legacy
*/
class NoMultilingualReviewPageTest extends NoMultilingualReviewPageTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'forum',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->loadFixture($this->getModulePath('forum') . '/tests/fixtures/drupal7.php');
}
/**
* Tests that Forum is displayed in the will be upgraded list.
*/
public function testMigrateUpgradeReviewPage(): void {
$this->prepare();
// Start the upgrade process.
$this->submitCredentialForm();
$session = $this->assertSession();
$this->submitForm([], 'I acknowledge I may lose data. Continue anyway.');
$session->statusCodeEquals(200);
// Confirm that Forum will be upgraded.
$session->elementExists('xpath', "//td[contains(@class, 'checked') and text() = 'Forum']");
$session->elementNotExists('xpath', "//td[contains(@class, 'error') and text() = 'Forum']");
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
return __DIR__ . '/files';
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getIncompletePaths() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths() {
return [];
}
}

View File

@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Functional\migrate_drupal\d7;
use Drupal\Tests\migrate_drupal_ui\Functional\MigrateUpgradeExecuteTestBase;
/**
* Tests Drupal 7 upgrade using the migrate UI.
*
* The test method is provided by the MigrateUpgradeTestBase class.
*
* @group forum
* @group #slow
* @group legacy
*/
class Upgrade7Test extends MigrateUpgradeExecuteTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'forum',
'migrate_drupal_ui',
];
/**
* The entity storage for node.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $nodeStorage;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// @todo remove in https://www.drupal.org/project/drupal/issues/3267040
// Delete the existing content made to test the ID Conflict form. Migrations
// are to be done on a site without content. The test of the ID Conflict
// form is being moved to its own issue which will remove the deletion
// of the created nodes.
// See https://www.drupal.org/project/drupal/issues/3087061.
$this->nodeStorage = $this->container->get('entity_type.manager')
->getStorage('node');
$this->nodeStorage->delete($this->nodeStorage->loadMultiple());
$this->loadFixture($this->getModulePath('forum') . '/tests/fixtures/drupal7.php');
}
/**
* {@inheritdoc}
*/
protected function getSourceBasePath() {
return __DIR__ . '/files';
}
/**
* {@inheritdoc}
*/
protected function getEntityCounts() {
return [
'action' => 24,
'base_field_override' => 3,
'block' => 26,
'block_content' => 1,
'block_content_type' => 1,
'comment' => 0,
'comment_type' => 7,
'contact_form' => 2,
'contact_message' => 0,
'date_format' => 12,
'editor' => 2,
'entity_form_display' => 16,
'entity_form_mode' => 1,
'entity_view_display' => 24,
'entity_view_mode' => 11,
'field_config' => 26,
'field_storage_config' => 16,
'file' => 1,
'filter_format' => 7,
'image_style' => 7,
'menu' => 5,
'menu_link_content' => 3,
'node' => 2,
'node_type' => 6,
'path_alias' => 1,
'search_page' => 3,
'shortcut' => 2,
'shortcut_set' => 1,
'taxonomy_term' => 6,
'taxonomy_vocabulary' => 2,
'user' => 4,
'user_role' => 4,
'view' => 14,
];
}
/**
* {@inheritdoc}
*/
protected function getEntityCountsIncremental() {
return [];
}
/**
* {@inheritdoc}
*/
protected function getAvailablePaths() {
return [
'Block',
'Comment',
'Content translation',
'Date',
'Field SQL storage',
'Field',
'File',
'Filter',
'Forum',
'Image',
'Menu',
'Node',
'Options',
'Path',
'Search',
'System',
'Taxonomy',
'Text',
'User',
'Contextual links',
'Date API',
'Field UI',
];
}
/**
* {@inheritdoc}
*/
protected function getMissingPaths() {
return [
'Locale',
'Entity Translation',
'Internationalization',
'String translation',
'Taxonomy translation',
'Translation sets',
'Variable',
];
}
/**
* Executes all steps of migrations upgrade.
*/
public function testUpgrade(): void {
// Start the upgrade process.
$this->submitCredentialForm();
$session = $this->assertSession();
$this->submitForm([], 'I acknowledge I may lose data. Continue anyway.');
$session->statusCodeEquals(200);
// Test the review form.
$this->assertReviewForm();
$this->submitForm([], 'Perform upgrade');
$this->assertUpgrade($this->getEntityCounts());
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Defines a class for testing the forum_index table.
*
* @group forum
*/
final class ForumIndexTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'node',
'history',
'taxonomy',
'forum',
'comment',
'options',
'text',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installEntitySchema('comment');
$this->installEntitySchema('taxonomy_term');
$this->installSchema('forum', ['forum_index']);
}
/**
* Tests there's a primary key on the forum_index table.
*/
public function testForumIndexIndex(): void {
$schema = \Drupal::database()->schema();
$this->assertTrue($schema->tableExists('forum_index'));
// We can't reliably call ::indexExists for each database driver as sqlite
// doesn't have named indexes for primary keys like mysql (PRIMARY) and
// pgsql (pkey).
$find_primary_key_columns = new \ReflectionMethod(get_class($schema), 'findPrimaryKeyColumns');
$this->assertEquals(['nid', 'tid'], $find_primary_key_columns->invoke($schema, 'forum_index'));
}
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Kernel;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\node\Entity\Node;
use Drupal\taxonomy\Entity\Term;
/**
* Tests forum validation constraints.
*
* @group forum
* @group legacy
*/
class ForumValidationTest extends EntityKernelTestBase {
/**
* Modules to install.
*
* @var array
*/
protected static $modules = [
'node',
'options',
'comment',
'taxonomy',
'forum',
];
/**
* Tests the forum validation constraints.
*/
public function testValidation(): void {
$this->installConfig('forum');
// Add a forum.
$forum = Term::create([
'name' => 'forum 1',
'vid' => 'forums',
'forum_container' => 0,
]);
// Add a container.
$container = Term::create([
'name' => 'container 1',
'vid' => 'forums',
'forum_container' => 1,
]);
// Add a forum post.
$forum_post = Node::create([
'type' => 'forum',
'title' => 'Do these pants make my butt look big?',
]);
$violations = $forum_post->validate();
$this->assertCount(1, $violations);
$this->assertEquals('This value should not be null.', $violations[0]->getMessage());
// Add the forum term.
$forum_post->set('taxonomy_forums', $forum);
$violations = $forum_post->validate();
$this->assertCount(0, $violations);
// Try to use a container.
$forum_post->set('taxonomy_forums', $container);
$violations = $forum_post->validate();
$this->assertCount(1, $violations);
$this->assertEquals(sprintf('The item %s is a forum container, not a forum. Select one of the forums below instead.', $container->label()), $violations[0]->getMessage());
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Kernel\Migrate;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\TermInterface;
/**
* Common assertions for migration tests.
*/
trait MigrateTestTrait {
/**
* The cached taxonomy tree items, keyed by vid and tid.
*
* @var array
*/
protected $treeData = [];
/**
* Validate a migrated term contains the expected values.
*
* @param int $id
* Entity ID to load and check.
* @param string $expected_language
* The language code for this term.
* @param $expected_label
* The label the migrated entity should have.
* @param $expected_vid
* The parent vocabulary the migrated entity should have.
* @param string|null $expected_description
* The description the migrated entity should have.
* @param string|null $expected_format
* The format the migrated entity should have.
* @param int $expected_weight
* The weight the migrated entity should have.
* @param array $expected_parents
* The parent terms the migrated entity should have.
* @param int|null $expected_container_flag
* The term should be a container entity.
*
* @internal
*/
protected function assertEntity(int $id, string $expected_language, string $expected_label, string $expected_vid, ?string $expected_description = '', ?string $expected_format = NULL, int $expected_weight = 0, array $expected_parents = [], int|null $expected_container_flag = NULL): void {
/** @var \Drupal\taxonomy\TermInterface $entity */
$entity = Term::load($id);
$this->assertInstanceOf(TermInterface::class, $entity);
$this->assertSame($expected_language, $entity->language()->getId());
$this->assertEquals($expected_label, $entity->label());
$this->assertEquals($expected_vid, $entity->bundle());
$this->assertEquals($expected_description, $entity->getDescription());
$this->assertEquals($expected_format, $entity->getFormat());
$this->assertEquals($expected_weight, $entity->getWeight());
$this->assertEquals($expected_parents, array_column($entity->get('parent')
->getValue(), 'target_id'));
$this->assertHierarchy($expected_vid, $id, $expected_parents);
if (isset($expected_container_flag)) {
$this->assertEquals($expected_container_flag, $entity->forum_container->value);
}
}
/**
* Retrieves the parent term IDs for a given term.
*
* @param $tid
* ID of the term to check.
*
* @return array
* List of parent term IDs.
*/
protected function getParentIDs($tid) {
return array_keys(\Drupal::entityTypeManager()
->getStorage('taxonomy_term')
->loadParents($tid));
}
/**
* Assert that a term is present in the tree storage, with the right parents.
*
* @param string $vid
* Vocabulary ID.
* @param int $tid
* ID of the term to check.
* @param array $parent_ids
* The expected parent term IDs.
*/
protected function assertHierarchy(string $vid, int $tid, array $parent_ids): void {
if (!isset($this->treeData[$vid])) {
$tree = \Drupal::entityTypeManager()
->getStorage('taxonomy_term')
->loadTree($vid);
$this->treeData[$vid] = [];
foreach ($tree as $item) {
$this->treeData[$vid][$item->tid] = $item;
}
}
$this->assertArrayHasKey($tid, $this->treeData[$vid], "Term $tid exists in taxonomy tree");
$term = $this->treeData[$vid][$tid];
$this->assertEquals($parent_ids, $term->parents, "Term $tid has correct parents in taxonomy tree");
}
}

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Kernel\Migrate\d6;
use Drupal\block\Entity\Block;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Tests migration of forum blocks.
*
* @group forum
*/
class MigrateBlockTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'block',
'block_content',
'comment',
'forum',
'node',
'path_alias',
'taxonomy',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('path_alias');
// Install the themes used for this test.
$this->installEntitySchema('block_content');
$this->container->get('theme_installer')->install(['olivero', 'test_theme']);
$this->installConfig(['block_content']);
// Set Olivero as the default public theme.
$config = $this->config('system.theme');
$config->set('default', 'olivero');
$config->save();
$this->executeMigrations([
'd6_filter_format',
'block_content_type',
'block_content_body_field',
'd6_custom_block',
'd6_user_role',
'd6_block',
]);
block_rebuild();
}
/**
* Gets the path to the fixture file.
*/
protected function getFixtureFilePath() {
return __DIR__ . '/../../../../fixtures/drupal6.php';
}
/**
* Asserts various aspects of a block.
*
* @param string $id
* The block ID.
* @param array $visibility
* The block visibility settings.
* @param string $region
* The display region.
* @param string $theme
* The theme.
* @param int $weight
* The block weight.
* @param array $settings
* (optional) The block settings.
* @param bool $status
* Whether the block is expected to be enabled or disabled.
*
* @internal
*/
public function assertEntity(string $id, array $visibility, string $region, string $theme, int $weight, array $settings = [], bool $status = TRUE): void {
$block = Block::load($id);
$this->assertInstanceOf(Block::class, $block);
$this->assertSame($visibility, $block->getVisibility());
$this->assertSame($region, $block->getRegion());
$this->assertSame($theme, $block->getTheme());
$this->assertSame($weight, $block->getWeight());
$this->assertSame($status, $block->status());
if ($settings) {
$block_settings = $block->get('settings');
$block_settings['id'] = current(explode(':', $block_settings['id']));
$this->assertEquals($settings, $block_settings);
}
}
/**
* Tests the block migration.
*/
public function testBlockMigration(): void {
// Check forum block settings.
$settings = [
'id' => 'forum_active_block',
'label' => '',
'provider' => 'forum',
'label_display' => '0',
'block_count' => 3,
'properties' => [
'administrative' => '1',
],
];
$this->assertEntity('forum', [], 'sidebar', 'olivero', -8, $settings);
$settings = [
'id' => 'forum_new_block',
'label' => '',
'provider' => 'forum',
'label_display' => '0',
'block_count' => 4,
'properties' => [
'administrative' => '1',
],
];
$this->assertEntity('forum_1', [], 'sidebar', 'olivero', -9, $settings);
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Kernel\Migrate\d6;
use Drupal\Tests\SchemaCheckTestTrait;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Upgrade variables to forum.settings.yml.
*
* @group migrate_drupal_6
* @group legacy
*/
class MigrateForumConfigsTest extends MigrateDrupal6TestBase {
use SchemaCheckTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['comment', 'forum', 'taxonomy'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigration('d6_taxonomy_vocabulary');
$this->executeMigration('d6_forum_settings');
}
/**
* Gets the path to the fixture file.
*/
protected function getFixtureFilePath() {
return __DIR__ . '/../../../../fixtures/drupal6.php';
}
/**
* Tests migration of forum variables to forum.settings.yml.
*/
public function testForumSettings(): void {
$config = $this->config('forum.settings');
$this->assertSame(15, $config->get('topics.hot_threshold'));
$this->assertSame(25, $config->get('topics.page_limit'));
$this->assertSame(1, $config->get('topics.order'));
$this->assertSame('forums', $config->get('vocabulary'));
// This is 'forum_block_num_0' in D6, but block:active:limit' in D8.
$this->assertSame(3, $config->get('block.active.limit'));
// This is 'forum_block_num_1' in D6, but 'block:new:limit' in D8.
$this->assertSame(4, $config->get('block.new.limit'));
$this->assertConfigSchema(\Drupal::service('config.typed'), 'forum.settings', $config->get());
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Kernel\Migrate\d6;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\node\Entity\Node;
use Drupal\Tests\node\Kernel\Migrate\d6\MigrateNodeTestBase;
/**
* Tests forum migration from Drupal 6 to Drupal 8.
*
* @group migrate_drupal_6
* @group legacy
*/
class MigrateForumTest extends MigrateNodeTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'comment',
'forum',
'menu_ui',
'taxonomy',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('comment');
$this->installSchema('comment', ['comment_entity_statistics']);
$this->installSchema('forum', ['forum', 'forum_index']);
$this->installConfig(['comment', 'forum']);
$this->migrateContent();
$this->migrateTaxonomy();
$this->executeMigrations([
'd6_comment_type',
'd6_comment_field',
'd6_comment_field_instance',
'd6_comment_entity_display',
'd6_comment_entity_form_display',
'd6_comment',
'd6_term_node',
]);
}
/**
* Gets the path to the fixture file.
*/
protected function getFixtureFilePath() {
return __DIR__ . '/../../../../fixtures/drupal6.php';
}
/**
* Tests forum migration.
*/
public function testForumMigration(): void {
// Tests that the taxonomy_forums field storage config exists.
$field_storage_config = FieldStorageConfig::load('node.taxonomy_forums');
$this->assertInstanceOf(FieldStorageConfig::class, $field_storage_config);
// Tests that the taxonomy_forums field config exists.
$field_config = FieldConfig::load('node.forum.taxonomy_forums');
$this->assertInstanceOf(FieldConfig::class, $field_config);
// Tests that the taxonomy_forums entity view display component exists.
$entity_view_display = EntityViewDisplay::load('node.forum.default')->getComponent('taxonomy_forums');
$this->assertIsArray($entity_view_display);
// Tests that the taxonomy_forums entity form display component exists.
$entity_form_display = EntityFormDisplay::load('node.forum.default')->getComponent('taxonomy_forums');
$this->assertIsArray($entity_form_display);
// Test that the taxonomy_forums field has the right value.
$node = Node::load(19);
$this->assertEquals(8, $node->taxonomy_forums->target_id);
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Kernel\Migrate\d6;
use Drupal\Tests\forum\Kernel\Migrate\MigrateTestTrait;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Test migration of forum taxonomy terms.
*
* @group forum
*/
class MigrateTaxonomyTermTest extends MigrateDrupal6TestBase {
use MigrateTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = ['taxonomy', 'comment', 'forum', 'menu_ui'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('taxonomy_term');
$this->installConfig('forum');
$this->executeMigrations(['d6_taxonomy_vocabulary', 'd6_taxonomy_term']);
}
/**
* Gets the path to the fixture file.
*/
protected function getFixtureFilePath() {
return __DIR__ . '/../../../../fixtures/drupal6.php';
}
/**
* Assert the forum taxonomy terms.
*/
public function testTaxonomyTerms(): void {
$this->assertEntity(8, 'en', 'General discussion', 'forums', '', NULL, 2, ['0'], 0);
$this->assertEntity(9, 'en', 'Earth', 'forums', '', NULL, 0, ['0'], 1);
$this->assertEntity(10, 'en', 'Birds', 'forums', '', NULL, 0, ['9'], 0);
$this->assertEntity(11, 'en', 'Oak', 'trees', '', NULL, 0, ['0'], NULL);
$this->assertEntity(12, 'en', 'Ash', 'trees', '', NULL, 0, ['0'], NULL);
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Kernel\Migrate\d6;
use Drupal\Tests\taxonomy\Kernel\Migrate\d6\MigrateTaxonomyVocabularyTest as TaxonomyVocabularyTest;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\taxonomy\VocabularyInterface;
/**
* Migrate forum vocabulary to taxonomy.vocabulary.*.yml.
*
* @group forum
*/
class MigrateTaxonomyVocabularyTest extends TaxonomyVocabularyTest {
/**
* {@inheritdoc}
*/
protected static $modules = [
'comment',
'forum',
];
/**
* Gets the path to the fixture file.
*/
protected function getFixtureFilePath() {
return __DIR__ . '/../../../../fixtures/drupal6.php';
}
/**
* Validate a migrated vocabulary contains the expected values.
*
* @param string $id
* Entity ID to load and check.
* @param $expected_label
* The label the migrated entity should have.
* @param $expected_description
* The description the migrated entity should have.
* @param $expected_weight
* The weight the migrated entity should have.
*
* @internal
*/
protected function assertEntity(string $id, string $expected_label, string $expected_description, int $expected_weight): void {
/** @var \Drupal\taxonomy\VocabularyInterface $entity */
$entity = Vocabulary::load($id);
$this->assertInstanceOf(VocabularyInterface::class, $entity);
$this->assertSame($expected_label, $entity->label());
$this->assertSame($expected_description, $entity->getDescription());
$this->assertSame($expected_weight, (int) $entity->get('weight'));
}
/**
* Tests the Drupal 6 taxonomy vocabularies migration.
*/
public function testTaxonomyVocabulary(): void {
$this->assertEntity('forums', 'Forums', '', 0);
$this->assertEntity('trees', 'Trees', 'A list of trees.', 0);
$this->assertEntity('freetags', 'FreeTags', '', 0);
}
}

View File

@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Kernel\Migrate\d6;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Vocabulary entity display migration.
*
* @group forum
*/
class MigrateVocabularyEntityDisplayTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'field',
'comment',
'forum',
'taxonomy',
'menu_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Execute Dependency Migrations.
$this->migrateContentTypes();
$this->installEntitySchema('taxonomy_term');
$this->executeMigrations([
'd6_node_type',
'd6_taxonomy_vocabulary',
'd6_vocabulary_field',
'd6_vocabulary_field_instance',
'd6_vocabulary_entity_display',
]);
}
/**
* Gets the path to the fixture file.
*/
protected function getFixtureFilePath() {
return __DIR__ . '/../../../../fixtures/drupal6.php';
}
/**
* Tests the Drupal 6 vocabulary-node type association to Drupal 8 migration.
*/
public function testVocabularyEntityDisplay(): void {
$this->assertEntity('node.forum.default');
$this->assertComponent('node.forum.default', 'taxonomy_forums', 'entity_reference_label', 'hidden', 20);
$this->assertComponent('node.forum.default', 'field_trees', 'entity_reference_label', 'hidden', 20);
$this->assertComponent('node.forum.default', 'field_freetags', 'entity_reference_label', 'hidden', 20);
}
/**
* Asserts various aspects of a view display.
*
* @param string $id
* The view display ID.
*
* @internal
*/
protected function assertEntity(string $id): void {
$display = EntityViewDisplay::load($id);
$this->assertInstanceOf(EntityViewDisplayInterface::class, $display);
}
/**
* Asserts various aspects of a particular component of a view display.
*
* @param string $display_id
* The view display ID.
* @param string $component_id
* The component ID.
* @param string $type
* The expected component type (formatter plugin ID).
* @param string $label
* The expected label of the component.
* @param int $weight
* The expected weight of the component.
*
* @internal
*/
protected function assertComponent(string $display_id, string $component_id, string $type, string $label, int $weight): void {
$component = EntityViewDisplay::load($display_id)->getComponent($component_id);
$this->assertIsArray($component);
$this->assertSame($type, $component['type']);
$this->assertSame($label, $component['label']);
$this->assertSame($weight, $component['weight']);
}
/**
* Asserts that a particular component is NOT included in a display.
*
* @param string $display_id
* The display ID.
* @param string $component_id
* The component ID.
*
* @internal
*/
protected function assertComponentNotExists(string $display_id, string $component_id): void {
$component = EntityViewDisplay::load($display_id)->getComponent($component_id);
$this->assertNull($component);
}
}

View File

@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Kernel\Migrate\d6;
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
/**
* Vocabulary entity form display migration.
*
* @group forum
*/
class MigrateVocabularyEntityFormDisplayTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['comment', 'forum', 'taxonomy', 'menu_ui'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Execute Dependency Migrations.
$this->migrateContentTypes();
$this->installEntitySchema('taxonomy_term');
$this->executeMigrations([
'd6_taxonomy_vocabulary',
'd6_vocabulary_field',
'd6_vocabulary_field_instance',
'd6_vocabulary_entity_display',
'd6_vocabulary_entity_form_display',
]);
}
/**
* Gets the path to the fixture file.
*/
protected function getFixtureFilePath() {
return __DIR__ . '/../../../../fixtures/drupal6.php';
}
/**
* Tests the Drupal 6 vocabulary-node type association to Drupal 8 migration.
*/
public function testVocabularyEntityFormDisplay(): void {
$this->assertEntity('node.forum.default', 'node', 'forum');
$this->assertComponent('node.forum.default', 'taxonomy_forums', 'options_select', 20);
$this->assertComponent('node.forum.default', 'field_trees', 'options_select', 20);
$this->assertComponent('node.forum.default', 'field_freetags', 'entity_reference_autocomplete_tags', 20);
}
/**
* Asserts various aspects of a form display entity.
*
* @param string $id
* The entity ID.
* @param string $expected_entity_type
* The expected entity type to which the display settings are attached.
* @param string $expected_bundle
* The expected bundle to which the display settings are attached.
*
* @internal
*/
protected function assertEntity(string $id, string $expected_entity_type, string $expected_bundle): void {
/** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $entity */
$entity = EntityFormDisplay::load($id);
$this->assertInstanceOf(EntityFormDisplayInterface::class, $entity);
$this->assertSame($expected_entity_type, $entity->getTargetEntityTypeId());
$this->assertSame($expected_bundle, $entity->getTargetBundle());
}
/**
* Asserts various aspects of a particular component of a form display.
*
* @param string $display_id
* The form display ID.
* @param string $component_id
* The component ID.
* @param string $widget_type
* The expected widget type.
* @param int $weight
* The expected weight of the component.
*
* @internal
*/
protected function assertComponent(string $display_id, string $component_id, string $widget_type, int $weight): void {
$component = EntityFormDisplay::load($display_id)->getComponent($component_id);
$this->assertIsArray($component);
$this->assertSame($widget_type, $component['type']);
$this->assertSame($weight, $component['weight']);
}
}

View File

@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Kernel\Migrate\d6;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\FieldConfigInterface;
/**
* Vocabulary field instance migration.
*
* @group forum
*/
class MigrateVocabularyFieldInstanceTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'comment',
'forum',
'menu_ui',
'taxonomy',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Execute Dependency Migrations.
$this->migrateContentTypes();
$this->installEntitySchema('taxonomy_term');
$this->executeMigrations([
'd6_node_type',
'd6_taxonomy_vocabulary',
'd6_vocabulary_field',
'd6_vocabulary_field_instance',
]);
}
/**
* Gets the path to the fixture file.
*/
protected function getFixtureFilePath() {
return __DIR__ . '/../../../../fixtures/drupal6.php';
}
/**
* Tests the Drupal 6 vocabulary-node type association migration.
*/
public function testVocabularyFieldInstance(): void {
$this->assertEntity('node.forum.taxonomy_forums', 'Forums', 'entity_reference', FALSE, FALSE);
$this->assertEntity('node.forum.field_trees', 'Trees', 'entity_reference', FALSE, FALSE);
$this->assertEntity('node.forum.field_freetags', 'FreeTags', 'entity_reference', FALSE, FALSE);
}
/**
* Asserts various aspects of a field config entity.
*
* @param string $id
* The entity ID in the form ENTITY_TYPE.BUNDLE.FIELD_NAME.
* @param string $expected_label
* The expected field label.
* @param string $expected_field_type
* The expected field type.
* @param bool $is_required
* Whether or not the field is required.
* @param bool $expected_translatable
* Whether or not the field is expected to be translatable.
*
* @internal
*/
protected function assertEntity(string $id, string $expected_label, string $expected_field_type, bool $is_required, bool $expected_translatable): void {
[$expected_entity_type, $expected_bundle, $expected_name] = explode('.', $id);
/** @var \Drupal\field\FieldConfigInterface $field */
$field = FieldConfig::load($id);
$this->assertInstanceOf(FieldConfigInterface::class, $field);
$this->assertEquals($expected_label, $field->label());
$this->assertEquals($expected_field_type, $field->getType());
$this->assertEquals($expected_entity_type, $field->getTargetEntityTypeId());
$this->assertEquals($expected_bundle, $field->getTargetBundle());
$this->assertEquals($expected_name, $field->getName());
$this->assertEquals($is_required, $field->isRequired());
$this->assertEquals($expected_entity_type . '.' . $expected_name, $field->getFieldStorageDefinition()->id());
$this->assertEquals($expected_translatable, $field->isTranslatable());
}
/**
* Asserts the settings of a link field config entity.
*
* @param string $id
* The entity ID in the form ENTITY_TYPE.BUNDLE.FIELD_NAME.
* @param int $title_setting
* The expected title setting.
*
* @internal
*/
protected function assertLinkFields(string $id, int $title_setting): void {
$field = FieldConfig::load($id);
$this->assertSame($title_setting, $field->getSetting('title'));
}
/**
* Asserts the settings of an entity reference field config entity.
*
* @param string $id
* The entity ID in the form ENTITY_TYPE.BUNDLE.FIELD_NAME.
* @param string[] $target_bundles
* An array of expected target bundles.
*
* @internal
*/
protected function assertEntityReferenceFields(string $id, array $target_bundles): void {
$field = FieldConfig::load($id);
$handler_settings = $field->getSetting('handler_settings');
$this->assertArrayHasKey('target_bundles', $handler_settings);
foreach ($handler_settings['target_bundles'] as $target_bundle) {
$this->assertContains($target_bundle, $target_bundles);
}
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Kernel\Migrate\d6;
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\FieldStorageConfigInterface;
/**
* Vocabulary field migration.
*
* @group forum
*/
class MigrateVocabularyFieldTest extends MigrateDrupal6TestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'comment',
'forum',
'taxonomy',
'menu_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->migrateTaxonomy();
}
/**
* Gets the path to the fixture file.
*/
protected function getFixtureFilePath() {
return __DIR__ . '/../../../../fixtures/drupal6.php';
}
/**
* Tests the Drupal 6 vocabulary-node type association migration.
*/
public function testVocabularyField(): void {
// Test that the field exists.
$this->assertEntity('node.field_freetags', 'entity_reference', TRUE, -1);
$this->assertEntity('node.field_trees', 'entity_reference', TRUE, 1);
$this->assertEntity('node.taxonomy_forums', 'entity_reference', TRUE, 1);
}
/**
* Asserts various aspects of a field_storage_config entity.
*
* @param string $id
* The entity ID in the form ENTITY_TYPE.FIELD_NAME.
* @param string $expected_type
* The expected field type.
* @param bool $expected_translatable
* Whether or not the field is expected to be translatable.
* @param int $expected_cardinality
* The expected cardinality of the field.
*
* @internal
*/
protected function assertEntity(string $id, string $expected_type, bool $expected_translatable, int $expected_cardinality): void {
[$expected_entity_type, $expected_name] = explode('.', $id);
/** @var \Drupal\field\FieldStorageConfigInterface $field */
$field = FieldStorageConfig::load($id);
$this->assertInstanceOf(FieldStorageConfigInterface::class, $field);
$this->assertEquals($expected_name, $field->getName());
$this->assertEquals($expected_type, $field->getType());
$this->assertEquals($expected_translatable, $field->isTranslatable());
$this->assertEquals($expected_entity_type, $field->getTargetEntityTypeId());
if ($expected_cardinality === 1) {
$this->assertFalse($field->isMultiple());
}
else {
$this->assertTrue($field->isMultiple());
}
$this->assertEquals($expected_cardinality, $field->getCardinality());
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Kernel\Migrate\d7;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
/**
* Tests migration of Forum's variables to configuration.
*
* @group forum
* @group legacy
*/
class MigrateForumSettingsTest extends MigrateDrupal7TestBase {
/**
* Modules to enable.
*
* Don't alphabetize these. They're in dependency order.
*
* @var array
*/
protected static $modules = [
'comment',
'field',
'filter',
'text',
'node',
'taxonomy',
'forum',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->executeMigration('d7_taxonomy_vocabulary');
$this->executeMigration('d7_forum_settings');
}
/**
* Gets the path to the fixture file.
*/
protected function getFixtureFilePath() {
return __DIR__ . '/../../../../fixtures/drupal7.php';
}
/**
* Tests the migration of Forum's settings to configuration.
*/
public function testForumSettingsMigration(): void {
$config = $this->config('forum.settings');
$this->assertSame(9, $config->get('block.active.limit'));
$this->assertSame(4, $config->get('block.new.limit'));
$this->assertSame(10, $config->get('topics.hot_threshold'));
$this->assertSame(25, $config->get('topics.page_limit'));
$this->assertSame(1, $config->get('topics.order'));
$this->assertSame('forums', $config->get('vocabulary'));
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Kernel\Migrate\d7;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Tests\language\Kernel\Migrate\d7\MigrateLanguageContentTaxonomyVocabularySettingsTest as CoreTest;
/**
* Tests migration of i18ntaxonomy vocabulary settings.
*
* @group forum
*/
class MigrateLanguageContentTaxonomyVocabularySettingsTest extends CoreTest {
/**
* {@inheritdoc}
*/
protected static $modules = [
'comment',
'forum',
];
/**
* Tests migration of 18ntaxonomy vocabulary settings.
*/
public function testLanguageContentTaxonomy(): void {
$this->assertLanguageContentSettings('taxonomy_term', 'forums', LanguageInterface::LANGCODE_NOT_SPECIFIED, FALSE, ['enabled' => FALSE]);
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Kernel\Migrate\d7;
use Drupal\Tests\forum\Kernel\Migrate\MigrateTestTrait;
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
use Drupal\taxonomy\Entity\Term;
/**
* Test migration of forum taxonomy terms.
*
* @group forum
*/
class MigrateTaxonomyTermTest extends MigrateDrupal7TestBase {
use MigrateTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'comment',
'forum',
'content_translation',
'datetime',
'datetime_range',
'image',
'language',
'menu_ui',
'node',
'taxonomy',
'text',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig('forum');
$this->installEntitySchema('comment');
$this->installEntitySchema('file');
$this->migrateTaxonomyTerms();
$this->executeMigrations([
'language',
'd7_user_role',
'd7_user',
'd7_entity_translation_settings',
'd7_taxonomy_term_entity_translation',
]);
}
/**
* Gets the path to the fixture file.
*/
protected function getFixtureFilePath() {
return __DIR__ . '/../../../../fixtures/drupal7.php';
}
/**
* Assert the forum taxonomy terms.
*/
public function testTaxonomyTerms(): void {
$this->assertEntity(1, 'en', 'General discussion', 'forums', '', NULL, 2, ['0'], 0);
$this->assertEntity(5, 'en', 'Custom Forum', 'forums', 'Where the cool kids are.', NULL, 3, ['0'], 0);
$this->assertEntity(6, 'en', 'Games', 'forums', NULL, '', 4, ['0'], 1);
$this->assertEntity(7, 'en', 'Minecraft', 'forums', '', NULL, 1, [6], 0);
$this->assertEntity(8, 'en', 'Half Life 3', 'forums', '', NULL, 0, [6], 0);
// Verify that we still can create forum containers after the migration.
$term = Term::create([
'vid' => 'forums',
'name' => 'Forum Container',
'forum_container' => 1,
]);
$term->save();
// Reset the forums tree data so this new term is included in the tree.
unset($this->treeData['forums']);
$this->assertEntity(9, 'en', 'Forum Container', 'forums', '', '', 0, ['0'], 1);
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Kernel\Migrate\d7;
use Drupal\Tests\taxonomy\Kernel\Migrate\d7\MigrateTaxonomyTermTranslationTest as TaxonomyTermTranslationTest;
/**
* Test migration of translated taxonomy terms.
*
* @group forum
*/
class MigrateTaxonomyTermTranslationTest extends TaxonomyTermTranslationTest {
/**
* {@inheritdoc}
*/
protected static $modules = [
'comment',
'forum',
];
/**
* Gets the path to the fixture file.
*/
protected function getFixtureFilePath() {
return __DIR__ . '/../../../../fixtures/drupal7.php';
}
/**
* Tests the Drupal i18n taxonomy term to Drupal 8 migration.
*/
public function testTaxonomyTermTranslation(): void {
// Forums vocabulary, no multilingual option.
$this->assertEntity(1, 'en', 'General discussion', 'forums', NULL, NULL, 2, []);
$this->assertEntity(5, 'en', 'Custom Forum', 'forums', 'Where the cool kids are.', NULL, 3, []);
$this->assertEntity(6, 'en', 'Games', 'forums', NULL, NULL, 4, []);
$this->assertEntity(7, 'en', 'Minecraft', 'forums', NULL, NULL, 1, ['6']);
$this->assertEntity(8, 'en', 'Half Life 3', 'forums', NULL, NULL, 0, ['6']);
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Kernel\Migrate\d7;
use Drupal\Tests\taxonomy\Kernel\Migrate\d7\MigrateTaxonomyVocabularyTest as TaxonomyVocabularyTest;
/**
* Migrate forum vocabulary to taxonomy.vocabulary.*.yml.
*
* @group forum
*/
class MigrateTaxonomyVocabularyTest extends TaxonomyVocabularyTest {
/**
* {@inheritdoc}
*/
protected static $modules = [
'comment',
'forum',
];
/**
* Gets the path to the fixture file.
*/
protected function getFixtureFilePath() {
return __DIR__ . '/../../../../fixtures/drupal7.php';
}
/**
* Tests the Drupal 7 taxonomy vocabularies to Drupal 8 migration.
*/
public function testTaxonomyVocabulary(): void {
$this->assertEntity('tags', 'Tags', 'Use tags to group articles on similar topics into categories.', 0);
$this->assertEntity('forums', 'Subject of discussion', 'Forum navigation vocabulary', -10);
}
}

View File

@@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Unit\Breadcrumb;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Link;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\DependencyInjection\Container;
/**
* @coversDefaultClass \Drupal\forum\Breadcrumb\ForumBreadcrumbBuilderBase
* @group forum
* @group legacy
*/
class ForumBreadcrumbBuilderBaseTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
->disableOriginalConstructor()
->getMock();
$cache_contexts_manager->method('assertValidTokens')->willReturn(TRUE);
$container = new Container();
$container->set('cache_contexts_manager', $cache_contexts_manager);
\Drupal::setContainer($container);
}
/**
* Tests ForumBreadcrumbBuilderBase::__construct().
*
* @covers ::__construct
*/
public function testConstructor(): void {
// Make some test doubles.
$entity_type_manager = $this->createMock(EntityTypeManagerInterface::class);
$config_factory = $this->getConfigFactoryStub(
[
'forum.settings' => ['IAmATestKey' => 'IAmATestValue'],
]
);
$forum_manager = $this->createMock('Drupal\forum\ForumManagerInterface');
$translation_manager = $this->createMock('Drupal\Core\StringTranslation\TranslationInterface');
// Make an object to test.
$builder = $this->getMockForAbstractClass(
'Drupal\forum\Breadcrumb\ForumBreadcrumbBuilderBase',
// Constructor array.
[
$entity_type_manager,
$config_factory,
$forum_manager,
$translation_manager,
]
);
// Test that the constructor made a config object with our info in it.
$reflector = new \ReflectionClass($builder);
$ref_property = $reflector->getProperty('config');
$config = $ref_property->getValue($builder);
$this->assertEquals('IAmATestValue', $config->get('IAmATestKey'));
}
/**
* Tests ForumBreadcrumbBuilderBase::build().
*
* @see \Drupal\forum\Breadcrumb\ForumBreadcrumbBuilderBase::build()
*
* @covers ::build
*/
public function testBuild(): void {
// Build all our dependencies, backwards.
$translation_manager = $this->getMockBuilder('Drupal\Core\StringTranslation\TranslationInterface')
->disableOriginalConstructor()
->getMock();
$forum_manager = $this->getMockBuilder('Drupal\forum\ForumManagerInterface')
->disableOriginalConstructor()
->getMock();
$prophecy = $this->prophesize('Drupal\taxonomy\VocabularyInterface');
$prophecy->label()->willReturn('Fora_is_the_plural_of_forum');
$prophecy->id()->willReturn(5);
$prophecy->getCacheTags()->willReturn(['taxonomy_vocabulary:5']);
$prophecy->getCacheContexts()->willReturn([]);
$prophecy->getCacheMaxAge()->willReturn(Cache::PERMANENT);
$vocab_storage = $this->createMock('Drupal\Core\Entity\EntityStorageInterface');
$vocab_storage->expects($this->any())
->method('load')
->willReturnMap([
['forums', $prophecy->reveal()],
]);
$entity_type_manager = $this->createMock(EntityTypeManagerInterface::class);
$entity_type_manager->expects($this->any())
->method('getStorage')
->willReturnMap([
['taxonomy_vocabulary', $vocab_storage],
]);
$config_factory = $this->getConfigFactoryStub(
[
'forum.settings' => [
'vocabulary' => 'forums',
],
]
);
// Build a breadcrumb builder to test.
$breadcrumb_builder = $this->getMockForAbstractClass(
'Drupal\forum\Breadcrumb\ForumBreadcrumbBuilderBase',
// Constructor array.
[
$entity_type_manager,
$config_factory,
$forum_manager,
$translation_manager,
]
);
// Add a translation manager for t().
$translation_manager = $this->getStringTranslationStub();
$breadcrumb_builder->setStringTranslation($translation_manager);
// Our empty data set.
$route_match = $this->createMock('Drupal\Core\Routing\RouteMatchInterface');
// Expected result set.
$expected = [
Link::createFromRoute('Home', '<front>'),
Link::createFromRoute('Fora_is_the_plural_of_forum', 'forum.index'),
];
// And finally, the test.
$breadcrumb = $breadcrumb_builder->build($route_match);
$this->assertEquals($expected, $breadcrumb->getLinks());
$this->assertEquals(['route'], $breadcrumb->getCacheContexts());
$this->assertEquals(['taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
$this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
}
}

View File

@@ -0,0 +1,218 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Unit\Breadcrumb;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Link;
use Drupal\forum\Breadcrumb\ForumListingBreadcrumbBuilder;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\TermStorageInterface;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\DependencyInjection\Container;
/**
* @coversDefaultClass \Drupal\forum\Breadcrumb\ForumListingBreadcrumbBuilder
* @group forum
* @group legacy
*/
class ForumListingBreadcrumbBuilderTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
->disableOriginalConstructor()
->getMock();
$cache_contexts_manager->method('assertValidTokens')->willReturn(TRUE);
$container = new Container();
$container->set('cache_contexts_manager', $cache_contexts_manager);
\Drupal::setContainer($container);
}
/**
* Tests ForumListingBreadcrumbBuilder::applies().
*
* @param bool $expected
* ForumListingBreadcrumbBuilder::applies() expected result.
* @param string|null $route_name
* (optional) A route name.
* @param array $parameter_map
* (optional) An array of parameter names and values.
*
* @dataProvider providerTestApplies
* @covers ::applies
*/
public function testApplies(bool $expected, ?string $route_name = NULL, array $parameter_map = []): void {
// Make some test doubles.
$entity_type_manager = $this->createMock(EntityTypeManagerInterface::class);
$config_factory = $this->getConfigFactoryStub([]);
$forum_manager = $this->createMock('Drupal\forum\ForumManagerInterface');
$translation_manager = $this->createMock('Drupal\Core\StringTranslation\TranslationInterface');
$map = [];
if ($parameter_map) {
foreach ($parameter_map as $parameter) {
$map[] = [
$parameter[0],
$parameter[1] === TRUE ? $this->getMockBuilder(Term::class)->disableOriginalConstructor()->getMock() : $parameter[1],
];
}
}
// Make an object to test.
$builder = new ForumListingBreadcrumbBuilder($entity_type_manager, $config_factory, $forum_manager, $translation_manager);
$route_match = $this->createMock('Drupal\Core\Routing\RouteMatchInterface');
$route_match->expects($this->once())
->method('getRouteName')
->willReturn($route_name);
$route_match->expects($this->any())
->method('getParameter')
->willReturnMap($map);
$this->assertEquals($expected, $builder->applies($route_match));
}
/**
* Provides test data for testApplies().
*
* @return \Generator
* Datasets for testApplies(). Structured as such:
* - ForumListBreadcrumbBuilder::applies() expected result.
* - ForumListBreadcrumbBuilder::applies() $attributes input array.
*/
public static function providerTestApplies(): \Generator {
yield [FALSE];
yield [FALSE, 'NOT.forum.page'];
yield [FALSE, 'forum.page'];
yield [TRUE, 'forum.page', [['taxonomy_term', 'anything']]];
yield [TRUE, 'forum.page', [['taxonomy_term', TRUE]]];
}
/**
* Tests ForumListingBreadcrumbBuilder::build().
*
* @see \Drupal\forum\ForumListingBreadcrumbBuilder::build()
*
* @covers ::build
*/
public function testBuild(): void {
// Build all our dependencies, backwards.
$translation_manager = $this->getMockBuilder('Drupal\Core\StringTranslation\TranslationInterface')
->disableOriginalConstructor()
->getMock();
$prophecy = $this->prophesize('Drupal\taxonomy\Entity\Term');
$prophecy->label()->willReturn('Something');
$prophecy->id()->willReturn(1);
$prophecy->getCacheTags()->willReturn(['taxonomy_term:1']);
$prophecy->getCacheContexts()->willReturn([]);
$prophecy->getCacheMaxAge()->willReturn(Cache::PERMANENT);
$term1 = $prophecy->reveal();
$prophecy = $this->prophesize('Drupal\taxonomy\Entity\Term');
$prophecy->label()->willReturn('Something else');
$prophecy->id()->willReturn(2);
$prophecy->getCacheTags()->willReturn(['taxonomy_term:2']);
$prophecy->getCacheContexts()->willReturn([]);
$prophecy->getCacheMaxAge()->willReturn(Cache::PERMANENT);
$term2 = $prophecy->reveal();
$term_storage = $this->getMockBuilder(TermStorageInterface::class)->getMock();
$term_storage->expects($this->exactly(2))
->method('loadAllParents')
->willReturnOnConsecutiveCalls(
[$term1],
[$term1, $term2],
);
// The root forum.
$prophecy = $this->prophesize('Drupal\taxonomy\VocabularyInterface');
$prophecy->label()->willReturn('Fora_is_the_plural_of_forum');
$prophecy->id()->willReturn(5);
$prophecy->getCacheTags()->willReturn(['taxonomy_vocabulary:5']);
$prophecy->getCacheContexts()->willReturn([]);
$prophecy->getCacheMaxAge()->willReturn(Cache::PERMANENT);
$vocab_storage = $this->createMock('Drupal\Core\Entity\EntityStorageInterface');
$vocab_storage->expects($this->any())
->method('load')
->willReturnMap([
['forums', $prophecy->reveal()],
]);
$entity_type_manager = $this->getMockBuilder(EntityTypeManagerInterface::class)
->disableOriginalConstructor()
->getMock();
$entity_type_manager->expects($this->any())
->method('getStorage')
->willReturnMap([
['taxonomy_vocabulary', $vocab_storage],
['taxonomy_term', $term_storage],
]);
$config_factory = $this->getConfigFactoryStub(
[
'forum.settings' => [
'vocabulary' => 'forums',
],
]
);
$forum_manager = $this->createMock('Drupal\forum\ForumManagerInterface');
// Build a breadcrumb builder to test.
$breadcrumb_builder = new ForumListingBreadcrumbBuilder($entity_type_manager, $config_factory, $forum_manager, $translation_manager);
// Add a translation manager for t().
$translation_manager = $this->getStringTranslationStub();
$breadcrumb_builder->setStringTranslation($translation_manager);
// The forum listing we need a breadcrumb back from.
$prophecy = $this->prophesize('Drupal\taxonomy\Entity\Term');
$prophecy->label()->willReturn('You_should_not_see_this');
$prophecy->id()->willReturn(23);
$prophecy->getCacheTags()->willReturn(['taxonomy_term:23']);
$prophecy->getCacheContexts()->willReturn([]);
$prophecy->getCacheMaxAge()->willReturn(Cache::PERMANENT);
$forum_listing = $prophecy->reveal();
// Our data set.
$route_match = $this->createMock('Drupal\Core\Routing\RouteMatchInterface');
$route_match->expects($this->exactly(2))
->method('getParameter')
->with('taxonomy_term')
->willReturn($forum_listing);
// First test.
$expected1 = [
Link::createFromRoute('Home', '<front>'),
Link::createFromRoute('Fora_is_the_plural_of_forum', 'forum.index'),
Link::createFromRoute('Something', 'forum.page', ['taxonomy_term' => 1]),
];
$breadcrumb = $breadcrumb_builder->build($route_match);
$this->assertEquals($expected1, $breadcrumb->getLinks());
$this->assertEqualsCanonicalizing(['route'], $breadcrumb->getCacheContexts());
$this->assertEqualsCanonicalizing(['taxonomy_term:1', 'taxonomy_term:23', 'taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
$this->assertEqualsCanonicalizing(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
// Second test.
$expected2 = [
Link::createFromRoute('Home', '<front>'),
Link::createFromRoute('Fora_is_the_plural_of_forum', 'forum.index'),
Link::createFromRoute('Something else', 'forum.page', ['taxonomy_term' => 2]),
Link::createFromRoute('Something', 'forum.page', ['taxonomy_term' => 1]),
];
$breadcrumb = $breadcrumb_builder->build($route_match);
$this->assertEquals($expected2, $breadcrumb->getLinks());
$this->assertEqualsCanonicalizing(['route'], $breadcrumb->getCacheContexts());
$this->assertEqualsCanonicalizing(['taxonomy_term:1', 'taxonomy_term:2', 'taxonomy_term:23', 'taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
$this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
}
}

View File

@@ -0,0 +1,222 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Unit\Breadcrumb;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Link;
use Drupal\forum\Breadcrumb\ForumNodeBreadcrumbBuilder;
use Drupal\node\Entity\Node;
use Drupal\taxonomy\TermStorageInterface;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\DependencyInjection\Container;
/**
* @coversDefaultClass \Drupal\forum\Breadcrumb\ForumNodeBreadcrumbBuilder
* @group forum
* @group legacy
*/
class ForumNodeBreadcrumbBuilderTest extends UnitTestCase {
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$cache_contexts_manager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager')
->disableOriginalConstructor()
->getMock();
$cache_contexts_manager->method('assertValidTokens')->willReturn(TRUE);
$container = new Container();
$container->set('cache_contexts_manager', $cache_contexts_manager);
\Drupal::setContainer($container);
}
/**
* Tests ForumNodeBreadcrumbBuilder::applies().
*
* @param bool $expected
* ForumNodeBreadcrumbBuilder::applies() expected result.
* @param string|null $route_name
* (optional) A route name.
* @param array $parameter_map
* (optional) An array of parameter names and values.
*
* @dataProvider providerTestApplies
* @covers ::applies
*/
public function testApplies(bool $expected, ?string $route_name = NULL, array $parameter_map = []): void {
// Make some test doubles.
$entity_type_manager = $this->createMock(EntityTypeManagerInterface::class);
$config_factory = $this->getConfigFactoryStub([]);
$map = [];
if ($parameter_map) {
foreach ($parameter_map as $parameter) {
$map[] = [
$parameter[0],
$parameter[1] === TRUE ? $this->getMockBuilder(Node::class)->disableOriginalConstructor()->getMock() : $parameter[1],
];
}
}
$forum_manager = $this->createMock('Drupal\forum\ForumManagerInterface');
$forum_manager->expects($this->any())
->method('checkNodeType')
->willReturn(TRUE);
$translation_manager = $this->createMock('Drupal\Core\StringTranslation\TranslationInterface');
// Make an object to test.
$builder = new ForumNodeBreadcrumbBuilder($entity_type_manager, $config_factory, $forum_manager, $translation_manager);
$route_match = $this->createMock('Drupal\Core\Routing\RouteMatchInterface');
$route_match->expects($this->once())
->method('getRouteName')
->willReturn($route_name);
$route_match->expects($this->any())
->method('getParameter')
->willReturnMap($map);
$this->assertEquals($expected, $builder->applies($route_match));
}
/**
* Provides test data for testApplies().
*
* Note that this test is incomplete, because we can't mock NodeInterface.
*
* @return \Generator
* Datasets for testApplies(). Structured as such:
* - ForumNodeBreadcrumbBuilder::applies() expected result.
* - ForumNodeBreadcrumbBuilder::applies() $attributes input array.
*/
public static function providerTestApplies(): \Generator {
yield [FALSE];
yield [FALSE, 'NOT.entity.node.canonical'];
yield [FALSE, 'entity.node.canonical'];
yield [FALSE, 'entity.node.canonical', [['node', NULL]]];
yield [TRUE, 'entity.node.canonical', [['node', TRUE]]];
}
/**
* Tests ForumNodeBreadcrumbBuilder::build().
*
* @see \Drupal\forum\ForumNodeBreadcrumbBuilder::build()
* @covers ::build
*/
public function testBuild(): void {
// Build all our dependencies, backwards.
$translation_manager = $this->getMockBuilder('Drupal\Core\StringTranslation\TranslationInterface')
->disableOriginalConstructor()
->getMock();
$prophecy = $this->prophesize('Drupal\taxonomy\Entity\Term');
$prophecy->label()->willReturn('Something');
$prophecy->id()->willReturn(1);
$prophecy->getCacheTags()->willReturn(['taxonomy_term:1']);
$prophecy->getCacheContexts()->willReturn([]);
$prophecy->getCacheMaxAge()->willReturn(Cache::PERMANENT);
$term1 = $prophecy->reveal();
$prophecy = $this->prophesize('Drupal\taxonomy\Entity\Term');
$prophecy->label()->willReturn('Something else');
$prophecy->id()->willReturn(2);
$prophecy->getCacheTags()->willReturn(['taxonomy_term:2']);
$prophecy->getCacheContexts()->willReturn([]);
$prophecy->getCacheMaxAge()->willReturn(Cache::PERMANENT);
$term2 = $prophecy->reveal();
$forum_manager = $this->getMockBuilder('Drupal\forum\ForumManagerInterface')
->disableOriginalConstructor()
->getMock();
$term_storage = $this->getMockBuilder(TermStorageInterface::class)->getMock();
$term_storage->expects($this->exactly(2))
->method('loadAllParents')
->willReturnOnConsecutiveCalls(
[$term1],
[$term1, $term2],
);
$prophecy = $this->prophesize('Drupal\taxonomy\VocabularyInterface');
$prophecy->label()->willReturn('Forums');
$prophecy->id()->willReturn(5);
$prophecy->getCacheTags()->willReturn(['taxonomy_vocabulary:5']);
$prophecy->getCacheContexts()->willReturn([]);
$prophecy->getCacheMaxAge()->willReturn(Cache::PERMANENT);
$vocab_storage = $this->createMock('Drupal\Core\Entity\EntityStorageInterface');
$vocab_storage->expects($this->any())
->method('load')
->willReturnMap([
['forums', $prophecy->reveal()],
]);
$entity_type_manager = $this->getMockBuilder(EntityTypeManagerInterface::class)
->disableOriginalConstructor()
->getMock();
$entity_type_manager->expects($this->any())
->method('getStorage')
->willReturnMap([
['taxonomy_vocabulary', $vocab_storage],
['taxonomy_term', $term_storage],
]);
$config_factory = $this->getConfigFactoryStub(
[
'forum.settings' => [
'vocabulary' => 'forums',
],
]
);
// Build a breadcrumb builder to test.
$breadcrumb_builder = new ForumNodeBreadcrumbBuilder($entity_type_manager,
$config_factory,
$forum_manager,
$translation_manager);
// Add a translation manager for t().
$translation_manager = $this->getStringTranslationStub();
$breadcrumb_builder->setStringTranslation($translation_manager);
// The forum node we need a breadcrumb back from.
$forum_node = $this->getMockBuilder('Drupal\node\Entity\Node')
->disableOriginalConstructor()
->getMock();
// Our data set.
$route_match = $this->createMock('Drupal\Core\Routing\RouteMatchInterface');
$route_match->expects($this->exactly(2))
->method('getParameter')
->with('node')
->willReturn($forum_node);
// First test.
$expected1 = [
Link::createFromRoute('Home', '<front>'),
Link::createFromRoute('Forums', 'forum.index'),
Link::createFromRoute('Something', 'forum.page', ['taxonomy_term' => 1]),
];
$breadcrumb = $breadcrumb_builder->build($route_match);
$this->assertEquals($expected1, $breadcrumb->getLinks());
$this->assertEqualsCanonicalizing(['route'], $breadcrumb->getCacheContexts());
$this->assertEqualsCanonicalizing(['taxonomy_term:1', 'taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
$this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
// Second test.
$expected2 = [
Link::createFromRoute('Home', '<front>'),
Link::createFromRoute('Forums', 'forum.index'),
Link::createFromRoute('Something else', 'forum.page', ['taxonomy_term' => 2]),
Link::createFromRoute('Something', 'forum.page', ['taxonomy_term' => 1]),
];
$breadcrumb = $breadcrumb_builder->build($route_match);
$this->assertEquals($expected2, $breadcrumb->getLinks());
$this->assertEqualsCanonicalizing(['route'], $breadcrumb->getCacheContexts());
$this->assertEqualsCanonicalizing(['taxonomy_term:1', 'taxonomy_term:2', 'taxonomy_vocabulary:5'], $breadcrumb->getCacheTags());
$this->assertEquals(Cache::PERMANENT, $breadcrumb->getCacheMaxAge());
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Unit;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\forum\ForumManager
* @group forum
* @group legacy
*/
class ForumManagerTest extends UnitTestCase {
/**
* Tests ForumManager::getIndex().
*/
public function testGetIndex(): void {
$entity_field_manager = $this->createMock(EntityFieldManagerInterface::class);
$entity_type_manager = $this->createMock(EntityTypeManagerInterface::class);
$storage = $this->getMockBuilder('\Drupal\taxonomy\VocabularyStorage')
->disableOriginalConstructor()
->getMock();
$config_factory = $this->createMock('\Drupal\Core\Config\ConfigFactoryInterface');
$config = $this->getMockBuilder('\Drupal\Core\Config\Config')
->disableOriginalConstructor()
->getMock();
$config_factory->expects($this->once())
->method('get')
->willReturn($config);
$config->expects($this->once())
->method('get')
->willReturn('forums');
$entity_type_manager->expects($this->once())
->method('getStorage')
->willReturn($storage);
// This is sufficient for testing purposes.
$term = new \stdClass();
$storage->expects($this->once())
->method('create')
->willReturn($term);
$connection = $this->getMockBuilder('\Drupal\Core\Database\Connection')
->disableOriginalConstructor()
->getMock();
$translation_manager = $this->getMockBuilder('\Drupal\Core\StringTranslation\TranslationManager')
->disableOriginalConstructor()
->getMock();
$comment_manager = $this->getMockBuilder('\Drupal\comment\CommentManagerInterface')
->disableOriginalConstructor()
->getMock();
$manager = $this->getMockBuilder('\Drupal\forum\ForumManager')
->onlyMethods(['getChildren'])
->setConstructorArgs([
$config_factory,
$entity_type_manager,
$connection,
$translation_manager,
$comment_manager,
$entity_field_manager,
])
->getMock();
$manager->expects($this->once())
->method('getChildren')
->willReturn([]);
// 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);
}
}

View File

@@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\forum\Unit;
use Drupal\Core\Url;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\forum\ForumUninstallValidator
* @group forum
* @group legacy
*/
class ForumUninstallValidatorTest extends UnitTestCase {
/**
* @var \Drupal\forum\ForumUninstallValidator|\PHPUnit\Framework\MockObject\MockObject
*/
protected $forumUninstallValidator;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->forumUninstallValidator = $this->getMockBuilder('Drupal\forum\ForumUninstallValidator')
->disableOriginalConstructor()
->onlyMethods(['hasForumNodes', 'hasTermsForVocabulary', 'getForumVocabulary'])
->getMock();
$this->forumUninstallValidator->setStringTranslation($this->getStringTranslationStub());
}
/**
* @covers ::validate
*/
public function testValidateNotForum(): void {
$this->forumUninstallValidator->expects($this->never())
->method('hasForumNodes');
$this->forumUninstallValidator->expects($this->never())
->method('hasTermsForVocabulary');
$this->forumUninstallValidator->expects($this->never())
->method('getForumVocabulary');
$module = 'not_forum';
$expected = [];
$reasons = $this->forumUninstallValidator->validate($module);
$this->assertEquals($expected, $reasons);
}
/**
* @covers ::validate
*/
public function testValidate(): void {
$this->forumUninstallValidator->expects($this->once())
->method('hasForumNodes')
->willReturn(FALSE);
$vocabulary = $this->createMock('Drupal\taxonomy\VocabularyInterface');
$this->forumUninstallValidator->expects($this->once())
->method('getForumVocabulary')
->willReturn($vocabulary);
$this->forumUninstallValidator->expects($this->once())
->method('hasTermsForVocabulary')
->willReturn(FALSE);
$module = 'forum';
$expected = [];
$reasons = $this->forumUninstallValidator->validate($module);
$this->assertEquals($expected, $reasons);
}
/**
* @covers ::validate
*/
public function testValidateHasForumNodes(): void {
$this->forumUninstallValidator->expects($this->once())
->method('hasForumNodes')
->willReturn(TRUE);
$vocabulary = $this->createMock('Drupal\taxonomy\VocabularyInterface');
$this->forumUninstallValidator->expects($this->once())
->method('getForumVocabulary')
->willReturn($vocabulary);
$this->forumUninstallValidator->expects($this->once())
->method('hasTermsForVocabulary')
->willReturn(FALSE);
$module = 'forum';
$expected = [
'To uninstall Forum, first delete all <em>Forum</em> content',
];
$reasons = $this->forumUninstallValidator->validate($module);
$this->assertEquals($expected, $reasons);
}
/**
* @covers ::validate
*/
public function testValidateHasTermsForVocabularyWithNodesAccess(): void {
$this->forumUninstallValidator->expects($this->once())
->method('hasForumNodes')
->willReturn(TRUE);
$url = $this->prophesize(Url::class);
$url->toString()->willReturn('/path/to/vocabulary/overview');
$vocabulary = $this->createMock('Drupal\taxonomy\VocabularyInterface');
$vocabulary->expects($this->once())
->method('label')
->willReturn('Vocabulary label');
$vocabulary->expects($this->once())
->method('toUrl')
->willReturn($url->reveal());
$vocabulary->expects($this->once())
->method('access')
->willReturn(TRUE);
$this->forumUninstallValidator->expects($this->once())
->method('getForumVocabulary')
->willReturn($vocabulary);
$this->forumUninstallValidator->expects($this->once())
->method('hasTermsForVocabulary')
->willReturn(TRUE);
$module = 'forum';
$expected = [
'To uninstall Forum, first delete all <em>Forum</em> content',
'To uninstall Forum, first delete all <a href="/path/to/vocabulary/overview"><em class="placeholder">Vocabulary label</em></a> terms',
];
$reasons = $this->forumUninstallValidator->validate($module);
$this->assertEquals($expected, $reasons);
}
/**
* @covers ::validate
*/
public function testValidateHasTermsForVocabularyWithNodesNoAccess(): void {
$this->forumUninstallValidator->expects($this->once())
->method('hasForumNodes')
->willReturn(TRUE);
$vocabulary = $this->createMock('Drupal\taxonomy\VocabularyInterface');
$vocabulary->expects($this->once())
->method('label')
->willReturn('Vocabulary label');
$vocabulary->expects($this->never())
->method('toUrl');
$vocabulary->expects($this->once())
->method('access')
->willReturn(FALSE);
$this->forumUninstallValidator->expects($this->once())
->method('getForumVocabulary')
->willReturn($vocabulary);
$this->forumUninstallValidator->expects($this->once())
->method('hasTermsForVocabulary')
->willReturn(TRUE);
$module = 'forum';
$expected = [
'To uninstall Forum, first delete all <em>Forum</em> content',
'To uninstall Forum, first delete all <em class="placeholder">Vocabulary label</em> terms',
];
$reasons = $this->forumUninstallValidator->validate($module);
$this->assertEquals($expected, $reasons);
}
/**
* @covers ::validate
*/
public function testValidateHasTermsForVocabularyAccess(): void {
$this->forumUninstallValidator->expects($this->once())
->method('hasForumNodes')
->willReturn(FALSE);
$url = $this->prophesize(Url::class);
$url->toString()->willReturn('/path/to/vocabulary/overview');
$vocabulary = $this->createMock('Drupal\taxonomy\VocabularyInterface');
$vocabulary->expects($this->once())
->method('toUrl')
->willReturn($url->reveal());
$vocabulary->expects($this->once())
->method('label')
->willReturn('Vocabulary label');
$vocabulary->expects($this->once())
->method('access')
->willReturn(TRUE);
$this->forumUninstallValidator->expects($this->once())
->method('getForumVocabulary')
->willReturn($vocabulary);
$this->forumUninstallValidator->expects($this->once())
->method('hasTermsForVocabulary')
->willReturn(TRUE);
$module = 'forum';
$expected = [
'To uninstall Forum, first delete all <a href="/path/to/vocabulary/overview"><em class="placeholder">Vocabulary label</em></a> terms',
];
$reasons = $this->forumUninstallValidator->validate($module);
$this->assertEquals($expected, $reasons);
}
/**
* @covers ::validate
*/
public function testValidateHasTermsForVocabularyNoAccess(): void {
$this->forumUninstallValidator->expects($this->once())
->method('hasForumNodes')
->willReturn(FALSE);
$vocabulary = $this->createMock('Drupal\taxonomy\VocabularyInterface');
$vocabulary->expects($this->once())
->method('label')
->willReturn('Vocabulary label');
$vocabulary->expects($this->never())
->method('toUrl');
$vocabulary->expects($this->once())
->method('access')
->willReturn(FALSE);
$this->forumUninstallValidator->expects($this->once())
->method('getForumVocabulary')
->willReturn($vocabulary);
$this->forumUninstallValidator->expects($this->once())
->method('hasTermsForVocabulary')
->willReturn(TRUE);
$module = 'forum';
$expected = [
'To uninstall Forum, first delete all <em class="placeholder">Vocabulary label</em> terms',
];
$reasons = $this->forumUninstallValidator->validate($module);
$this->assertEquals($expected, $reasons);
}
}