diff --git a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php index a9f08abf45d42a22d30bc92b52b8b2e42313fd69..109dcb05e68de758f013c3dd481b6b231ccb1612 100644 --- a/core/lib/Drupal/Core/Entity/DatabaseStorageController.php +++ b/core/lib/Drupal/Core/Entity/DatabaseStorageController.php @@ -357,6 +357,8 @@ protected function buildQuery($ids, $revision_id = FALSE) { // Compare revision id of the base and revision table, if equal then this // is the default revision. $query->addExpression('base.' . $this->revisionKey . ' = revision.' . $this->revisionKey, 'isDefaultRevision'); + // Alias the default revision id to default_revision. + $query->addField('base', $this->revisionKey, 'default_revision'); } $query->fields('base', $entity_fields); diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index adbb0476b70c911e11680b70c131284de5327809..9c9c5bef6dcd4f7089aee69b07fc0235bd0c38cb 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -58,6 +58,13 @@ class Entity implements IteratorAggregate, EntityInterface { protected $isDefaultRevision = TRUE; /** + * Indicates whether this is the edit revision. + * + * @var bool + */ + protected $isEditRevision = TRUE; + + /** * Constructs an Entity object. * * @param array $values @@ -363,6 +370,13 @@ public function getRevisionId() { } /** + * Implements Drupal\Core\Entity\EntityInterface::getEditRevisionId(). + */ + public function getEditRevisionId() { + return NULL; + } + + /** * Implements Drupal\Core\Entity\EntityInterface::isDefaultRevision(). */ public function isDefaultRevision($new_value = NULL) { @@ -374,6 +388,17 @@ public function isDefaultRevision($new_value = NULL) { } /** + * Implements Drupal\Core\Entity\EntityInterface::isEditRevision(). + */ + public function isEditRevision($new_value = NULL) { + $return = $this->isEditRevision; + if (isset($new_value)) { + $this->isEditRevision = (bool) $new_value; + } + return $return; + } + + /** * Implements Drupal\Core\Entity\EntityInterface::getExportProperties(). */ public function getExportProperties() { diff --git a/core/lib/Drupal/Core/Entity/EntityInterface.php b/core/lib/Drupal/Core/Entity/EntityInterface.php index 94ecc3bd5dc4d704063d0afe3554867086db1551..03a5ab3f651fe35f56fc5de067671ca3301c6851 100644 --- a/core/lib/Drupal/Core/Entity/EntityInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityInterface.php @@ -172,6 +172,15 @@ public function entityInfo(); public function getRevisionId(); /** + * Returns the revision identifier of the entity's edit revision. + * + * @return + * The revision identifier of the entity's edit revision, or NULL if the entity does not + * have a revision identifier. + */ + public function getEditRevisionId(); + + /** * Checks if this entity is the default revision. * * @param bool $new_value @@ -184,6 +193,18 @@ public function getRevisionId(); public function isDefaultRevision($new_value = NULL); /** + * Checks if this entity is the edit revision. + * + * @param bool $new_value + * (optional) A Boolean to (re)set the isEditRevision flag. + * + * @return bool + * TRUE if the entity is the edit revision, FALSE otherwise. If + * $new_value was passed, the previous value is returned. + */ + public function isEditRevision($new_value = NULL); + + /** * Retrieves the exportable properties of the entity. * * @return array diff --git a/core/modules/node/lib/Drupal/node/NodeFormController.php b/core/modules/node/lib/Drupal/node/NodeFormController.php index 4f5315e40b6adff3462041eee8caf05d7ca6d5d4..48f7652c053ae7d8602746d9d6788695ebbca697 100644 --- a/core/modules/node/lib/Drupal/node/NodeFormController.php +++ b/core/modules/node/lib/Drupal/node/NodeFormController.php @@ -72,7 +72,7 @@ public function form(array $form, array &$form_state, EntityInterface $node) { // Basic node information. // These elements are just values so they are not even sent to the client. - foreach (array('nid', 'vid', 'uid', 'created', 'type') as $key) { + foreach (array('nid', 'vid', 'uid', 'created', 'type', 'status', 'edit_vid', 'default_revision') as $key) { $form[$key] = array( '#type' => 'value', '#value' => isset($node->$key) ? $node->$key : NULL, @@ -216,10 +216,12 @@ public function form(array $form, array &$form_state, EntityInterface $node) { '#weight' => 95, ); - $form['options']['status'] = array( - '#type' => 'checkbox', - '#title' => t('Published'), - '#default_value' => $node->status, + $state_options = ($node->default_revision == $node->edit_vid && !$node->status) ? array('draft' => t('Draft'), 'published' => t('Published')) : array('draft' => t('Draft'), 'published' => t('Published'), 'archived' => t('Archived')); + $form['options']['state'] = array( + '#type' => 'select', + '#title' => t('Moderation state'), + '#options' => $state_options, + '#default_value' => ($node->status) ? 'published' : 'draft', ); $form['options']['promote'] = array( @@ -327,6 +329,29 @@ public function submit(array $form, array &$form_state) { $node->setNewRevision(); } + // Make this the new edit revision. + $node->set('isEditRevision', TRUE); + + // Based on the moderation state, set the revision status and determine if + // this should be saved as the default revision. + switch ($form_state['values']['state']) { + case 'draft': + $node->set('status', FALSE); + $is_default = ($node->default_status) ? FALSE : TRUE; + $node->set('isDefaultRevision', $is_default); + break; + + case 'published': + $node->set('status', TRUE); + $node->set('isDefaultRevision', TRUE); + break; + + case 'archived': + $node->set('status', FALSE); + $node->set('isDefaultRevision', TRUE); + break; + } + node_submit($node); foreach (module_implements('node_submit') as $module) { $function = $module . '_node_submit'; @@ -373,7 +398,12 @@ public function save(array $form, array &$form_state) { if ($node->nid) { $form_state['values']['nid'] = $node->nid; $form_state['nid'] = $node->nid; - $form_state['redirect'] = node_access('view', $node) ? 'node/' . $node->nid : ''; + if (!$node->isDefaultRevision && node_access('update', $node)) { + $form_state['redirect'] = 'node/' . $node->nid . '/draft'; + } + else { + $form_state['redirect'] = node_access('view', $node) ? 'node/' . $node->nid : ''; + } } else { // In the unlikely case something went wrong on save, the node will be diff --git a/core/modules/node/lib/Drupal/node/NodeStorageController.php b/core/modules/node/lib/Drupal/node/NodeStorageController.php index d855384e95a821682ff64e63e5bf5a926ed66920..3556620155ca5118a40900a663715b41d3b89e40 100644 --- a/core/modules/node/lib/Drupal/node/NodeStorageController.php +++ b/core/modules/node/lib/Drupal/node/NodeStorageController.php @@ -41,6 +41,8 @@ protected function attachLoad(&$nodes, $load_revision = FALSE) { $typed_nodes = array(); foreach ($nodes as $id => $entity) { $typed_nodes[$entity->type][$id] = $entity; + // Set the default value of $node->isEditRevision. + $nodes[$id]->isEditRevision = ($entity->vid == $entity->edit_vid) ? TRUE : FALSE; } // Call object type specific callbacks on each typed array of nodes. @@ -70,6 +72,8 @@ protected function buildQuery($ids, $revision_id = FALSE) { $query->addField('revision', 'timestamp', 'revision_timestamp'); $fields['uid']['table'] = 'base'; $query->addField('revision', 'uid', 'revision_uid'); + // Alias {node}.status to default_status. + $query->addField('base', 'status', 'default_status'); return $query; } @@ -140,7 +144,16 @@ function postSave(EntityInterface $node, $update) { if ($node->isDefaultRevision()) { node_access_acquire_grants($node, $update); } + + // If this is a new edit revision, update the pointer in {node}.edit_vid. + if ($node->isNew() || $node->isEditRevision()) { + db_update('node') + ->condition('nid', $node->nid, '=') + ->fields(array('edit_vid' => $node->vid,)) + ->execute(); + } } + /** * Overrides Drupal\Core\Entity\DatabaseStorageController::preDelete(). */ diff --git a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php index 216cfc9fa412dc637964eeefca3988a9781b6e3f..6e504381835ef2cac363d4ef039aa07d81e05965 100644 --- a/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php +++ b/core/modules/node/lib/Drupal/node/Plugin/Core/Entity/Node.php @@ -80,7 +80,17 @@ class Node extends Entity implements ContentEntityInterface { * * @var boolean */ - public $isDefaultRevision = TRUE; + public $isDefaultRevision = FALSE; + + /** + * Indicates whether this is the edit revision of the node. + * + * The edit revision of a node is the one loaded on the edit page when no + * specific revision has been specified. + * + * @var boolean + */ + public $isEditRevision = FALSE; /** * The node UUID. @@ -234,4 +244,11 @@ public function createDuplicate() { public function getRevisionId() { return $this->vid; } + + /** + * Overrides Drupal\Core\Entity\Entity::getEditRevisionId(). + */ + public function getEditRevisionId() { + return $this->edit_vid; + } } diff --git a/core/modules/node/node.install b/core/modules/node/node.install index 697d3a7c437d6d3b8a782e8680f74e71d2f2f334..4628781a822729b8e87dca4b36f6ac9f05cd8970 100644 --- a/core/modules/node/node.install +++ b/core/modules/node/node.install @@ -33,6 +33,13 @@ function node_schema() { 'not null' => FALSE, 'default' => NULL, ), + 'edit_vid' => array( + 'description' => 'The version identifier for this node\'s edit revision.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'default' => NULL, + ), 'type' => array( 'description' => 'The {node_type}.type of this node.', 'type' => 'varchar', @@ -124,12 +131,13 @@ function node_schema() { ), 'unique keys' => array( 'vid' => array('vid'), + 'edit_vid' => array('edit_vid'), 'uuid' => array('uuid'), ), 'foreign keys' => array( 'node_revision' => array( 'table' => 'node_revision', - 'columns' => array('vid' => 'vid'), + 'columns' => array('vid' => 'vid', 'edit_vid' => 'vid'), ), 'node_author' => array( 'table' => 'users', @@ -734,6 +742,75 @@ function node_update_8013() { } /** + * Create a edit_vid column for nodes. + */ +function node_update_8014(&$sandbox) { + // This is a multi-pass update. On the first call we need to update the + // database schema and initialize some variables. + if (!isset($sandbox['total'])) { + // Add a published column to the node table. + $spec = array( + 'description' => 'The version identifier for this node\'s edit revision.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + 'default' => NULL, + ); + $keys = array( + 'unique keys' => array( + 'edit_vid' => array('edit_vid'), + ), + 'foreign keys' => array( + 'node_revision' => array( + 'table' => 'node_revision', + 'columns' => array('edit_vid' => 'vid'), + ), + ), + ); + db_add_field('node', 'edit_vid', $spec, $keys); + + // Initialize update variables. + $sandbox['last'] = 0; + $sandbox['count'] = 0; + $sandbox['total'] = db_query('SELECT COUNT(*) FROM {node}')->fetchField(); + } + else { + // We do each pass in batches of 1000. + $batch = 1000; + + $nids = db_select('node', 'n') + ->fields('n', array(nid)) + ->range($sandbox['last'],$batch) + ->execute() + ->fetchCol(); + + foreach ($nids as $nid) { + $sandbox['count'] += 1; + $highest_vid = db_select('node_revision', 'nr') + ->fields('nr', array('vid')) + ->condition('nid', $nid, '=') + ->range(0,1) + ->orderBy('nr.' . 'vid', 'DESC') + ->execute() + ->fetchField(); + + db_update('node') + ->condition('nid', $nid, '=') + ->fields(array('edit_vid' => $highest_vid,)) + ->execute(); + } + $sandbox['last'] += $batch; + } + + if ($sandbox['count'] < $sandbox['total']) { + $sandbox['#finished'] = FALSE; + } + else { + $sandbox['#finished'] = TRUE; + } +} + +/** * @} End of "addtogroup updates-7.x-to-8.x" * The next series of updates should start at 9000. */ diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 7cbba867571c0d0bd75491cd608bd6f0e3b09a7a..3c36a7db0fb47e159dcd5715187477f8e638a460 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -1737,12 +1737,25 @@ function node_menu() { 'access arguments' => array('view', 1), ); $items['node/%node/view'] = array( - 'title' => 'View', + 'title callback' => 'node_view_tab_title', + 'title arguments' => array(1), 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -10, ); + $items['node/%node/draft'] = array( + 'title' => 'View draft', + 'page callback' => 'node_draft_page_view', + 'page arguments' => array(1), + 'access callback' => 'node_access_draft', + 'access arguments' => array(1), + 'weight' => -9, + 'type' => MENU_LOCAL_TASK, + 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, + 'file' => 'node.pages.inc', + ); $items['node/%node/edit'] = array( - 'title' => 'Edit', + 'title callback' => 'node_edit_tab_title', + 'title arguments' => array(1), 'page callback' => 'node_page_edit', 'page arguments' => array(1), 'access callback' => 'node_access', @@ -1846,6 +1859,38 @@ function node_page_title(Node $node) { } /** + * Title callback: Displays the node view tab title. + * + * @param Drupal\node\Node $node + * The node entity. + * + * @return + * The title of the view tab. + * + * @see node_menu() + */ +function node_view_tab_title(Node $node) { + $title = ($node->default_status == NODE_PUBLISHED) ? t("View published") : t("View draft"); + return $title; +} + +/** + * Title callback: Displays the node edit tab title. + * + * @param Drupal\node\Node $node + * The node entity. + * + * @return + * The title of the edit tab. + * + * @see node_menu() + */ +function node_edit_tab_title(Node $node) { + $title = ($node->default_revision == $node->edit_vid) ? t("Edit") : t("Edit draft"); + return $title; +} + +/** * Finds the last time a node was changed. * * @param $nid @@ -2277,6 +2322,23 @@ function node_page_view(Node $node) { } /** + * Draft page callback: Displays a single node. + * + * @param Drupal\node\Node $node + * The node entity. + * + * @return + * A page array suitable for use by drupal_render(). + * + * @see node_menu() + */ +function node_draft_page_view(Node $node) { + // Load the edit revision and pass that to node_page_view(). + $edit_revision = ($node->isEditRevision()) ? $node : node_revision_load($node->edit_vid); + return node_page_view($edit_revision); +} + +/** * Implements hook_update_index(). */ function node_update_index() { @@ -2689,6 +2751,21 @@ function node_access($op, $node, $account = NULL, $langcode = NULL) { } /** + * Access callback: Checks a user's permission for viewing a node's draft. + * + * @param Drupal\node\Node $node + * The node entity. + * + * @return + * TRUE if the view draft tab should be visible, FALSE otherwise. + * + * @see node_menu() + */ +function node_access_draft($node) { + return ($node->default_revision != $node->edit_vid && node_access('update', $node)); +} + +/** * Implements hook_node_access(). */ function node_node_access($node, $op, $account) { diff --git a/core/modules/node/node.pages.inc b/core/modules/node/node.pages.inc index 245fef793928747f8895f1c64f640a9b1bbd8f4f..9f84f830a38a464ab9d64b193174c9d8ff0f2bcf 100644 --- a/core/modules/node/node.pages.inc +++ b/core/modules/node/node.pages.inc @@ -23,8 +23,10 @@ * @see node_menu() */ function node_page_edit($node) { - drupal_set_title(t('Edit @type @title', array('@type' => node_get_type_label($node), '@title' => $node->label())), PASS_THROUGH); - return entity_get_form($node); + // Make sure we're loading the edit revision. + $edit_revision = ($node->isEditRevision()) ? $node : node_revision_load($node->edit_vid); + drupal_set_title(t('Edit @type @title', array('@type' => node_get_type_label($edit_revision), '@title' => $edit_revision->label())), PASS_THROUGH); + return entity_get_form($edit_revision); } /** diff --git a/modules/custom/moderation/lib/Drupal/moderation/Tests/ModerationNodeTest.php b/modules/custom/moderation/lib/Drupal/moderation/Tests/ModerationNodeTest.php new file mode 100644 index 0000000000000000000000000000000000000000..640aa75086efccfa7859244266c7023c60fc022b --- /dev/null +++ b/modules/custom/moderation/lib/Drupal/moderation/Tests/ModerationNodeTest.php @@ -0,0 +1,180 @@ + 'Node Moderation', + 'description' => 'Create and edit forward revisions of a node.', + 'group' => 'Moderation', + ); + } + + function setUp() { + parent::setUp(); + + // Use revisions by default. + $node_options = variable_get('node_options_article', array('status', 'promote')); + $node_options[] = 'revision'; + variable_set('node_options_article', $node_options); + + // Set up an admin user. + $this->admin_user = $this->drupalCreateUser(array('bypass node access', 'administer nodes')); + } + + /** + * Helper function for checking if the draft tab exists. + */ + function _checkForDraftTab() { + $this->assertText('View draft', 'Draft Tab is present'); + } + + /** + * Helper function for checking if the draft tab does not exists. + */ + function _checkForNoDraftTab() { + $this->assertNoText('View draft', 'Draft Tab is not present'); + } + + /** + * Checks node edit functionality. + */ + function testBasicForwarRevisions() { + $this->drupalLogin($this->admin_user); + $this->drupalGet('node/add/article'); + + // Verify moderation select list is present. + // @todo, this check will change when the moderation select list is not coming + // from a field. + $this->assertRaw('edit-field-moderation-und', t('Moderation select list is present.')); + + // Verify that the published checkbox is hidden. + // @todo, Show this be an xpath check? + $this->assertNoRaw('id="edit-status"', t('The published checkbox is not present')); + + + // Helper variables for forms. + $langcode = LANGUAGE_NOT_SPECIFIED; + $title_key = "title"; + $body_key = "body[$langcode][0][value]"; + $moderation_key = "field_moderation[$langcode]"; + + /** + * Make a draft revision. + */ + $draft_one_values = array( + // @todo, should these values go in t()? + // It seems to be a standard to use randomName() for these kinds of values. + // Human-readable text seems much easier to debug. + $title_key => 'Draft One Title', + $body_key => 'Draft One Body field', + $moderation_key => 'draft', + ); + $this->drupalPost('node/add/article', $draft_one_values, t('Save')); + + // Verify that the admin user can see the node's page. + $this->drupalGet('node/1'); + $this->assertResponse(200, t('node/1 is accessible to the admin user.')); + $this->_checkForNoDraftTab(); + + // Verify that the anonymous user can't see the node's page. + $this->drupalLogout(); + $this->drupalGet('node/1'); + $this->assertResponse(403, t('node/1 is inaccessible as a 403 to anonymous user.')); + + + /** + * Make another draft revision. These values should be visible at node/1. + */ + $this->drupalLogin($this->admin_user); + $draft_two_values = array( + $title_key => 'Draft Two Title', + $body_key => 'Draft Two Body field', + $moderation_key => 'draft', + ); + $this->drupalPost('node/1/edit', $draft_two_values, t('Save')); + + // Verify that the admin user can see the node's page and sees the published values there. + $this->drupalGet('node/1'); + $this->assertResponse(200, t('node/1 is accessible to the admin user.')); + $this->assertText($draft_two_values[$title_key], t('draft_two_values Title found')); + $this->assertText($draft_two_values[$body_key], t('draft_two_values Body found')); + $this->_checkForNoDraftTab(); + + /** + * Make a published revision. + */ + $published_one_values = array( + $title_key => 'Published One Title', + $body_key => 'Published One Body field', + $moderation_key => 'published', + ); + $this->drupalPost('node/1/edit', $published_one_values, t('Save')); + + // Verify that the admin user can see the node's page. + $this->drupalGet('node/1'); + $this->assertResponse(200, t('node/1 is accessible to the admin user.')); + $this->_checkForNoDraftTab(); + + // Verify that the anonymous user can't see the node's page. + $this->drupalLogout(); + $this->drupalGet('node/1'); + $this->assertResponse(200, t('node/1 is accessible to the anonymous user.')); + + + /** + * Make a draft revision. + */ + $this->drupalLogin($this->admin_user); + $revision_three_values = array( + $title_key => 'Revision Three Title', + $body_key => 'Revision Three Body field', + $moderation_key => 'draft', + ); + $this->drupalPost('node/1/edit', $revision_three_values, t('Save')); + + // Verify that the admin user can see the node's page and sees the published values there. + $this->drupalGet('node/1'); + $this->assertResponse(200, t('node/1 is accessible to the admin user.')); + $this->assertText($published_one_values[$title_key], t('published_one_values Title found')); + $this->assertText($published_one_values[$body_key], t('published_one_values Body found')); + + + // Verify that the admin user sees revision three at node/1/draft. + $this->_checkForDraftTab(); + $this->drupalGet('node/1/draft'); + $this->assertResponse(200, t('node/1/draft is accessible to the admin user.')); + $this->assertText($revision_three_values[$title_key], t('revision_three_values Title found')); + $this->assertText($revision_three_values[$body_key], t('revision_three_values Body found')); + + + /** + * Go to edit page. Verify that draft node loads. + */ + $this->drupalGet('node/1/edit'); + // @todo Is there a better way to check field default values? + $this->assertRaw($revision_three_values[$title_key], 'Draft title is loaded into the form'); + $this->assertRaw($revision_three_values[$body_key], 'Draft body is loaded into the form'); + } +} diff --git a/modules/custom/moderation/moderation.info b/modules/custom/moderation/moderation.info new file mode 100644 index 0000000000000000000000000000000000000000..5d989257badd1e4f29f2a6b845cfc35858c09861 --- /dev/null +++ b/modules/custom/moderation/moderation.info @@ -0,0 +1,4 @@ +name = Moderation +description = Moderate revisionable entities between states. +core = 8.x +dependencies[] = options \ No newline at end of file diff --git a/modules/custom/moderation/moderation.install b/modules/custom/moderation/moderation.install new file mode 100644 index 0000000000000000000000000000000000000000..ac6daf5700902954f562072b0478f3cb5e280ece --- /dev/null +++ b/modules/custom/moderation/moderation.install @@ -0,0 +1,150 @@ + '0', + 'entity_types' => + array ( + ), + 'settings' => + array ( + 'allowed_values' => + array ( + 'draft' => 'Draft', + 'published' => 'Published', + ), + 'allowed_values_function' => '', + 'allowed_values_function_display' => '', + ), + 'storage' => + array ( + 'type' => 'field_sql_storage', + 'settings' => + array ( + ), + 'module' => 'field_sql_storage', + 'active' => '1', + 'details' => + array ( + 'sql' => + array ( + 'FIELD_LOAD_CURRENT' => + array ( + 'field_data_field_moderation' => + array ( + 'value' => 'field_moderation_value', + ), + ), + 'FIELD_LOAD_REVISION' => + array ( + 'field_revision_field_moderation' => + array ( + 'value' => 'field_moderation_value', + ), + ), + ), + ), + ), + 'foreign keys' => + array ( + ), + 'indexes' => + array ( + 'value' => + array ( + 0 => 'value', + ), + ), + 'id' => '6', + 'field_name' => 'field_moderation', + 'type' => 'list_text', + 'module' => 'options', + 'active' => '1', + 'locked' => '0', + 'cardinality' => '1', + 'deleted' => '0', + 'columns' => + array ( + 'value' => + array ( + 'type' => 'varchar', + 'length' => 255, + 'not null' => false, + ), + ), + 'bundles' => + array ( + 'node' => + array ( + 0 => 'article', + ), + ), + ); + + // This code was generated from debug(field_info_instance('node', 'field_moderation', 'article')); + $field_instance = + array ( + 'label' => 'Moderation', + 'widget' => + array ( + 'weight' => 0, + 'type' => 'options_select', + 'module' => 'options', + 'active' => 0, + 'settings' => + array ( + ), + ), + 'settings' => + array ( + 'user_register_form' => false, + ), + 'display' => + array ( + 'default' => + array ( + 'label' => 'above', + 'type' => 'list_default', + 'settings' => + array ( + ), + 'module' => 'options', + 'weight' => 11, + ), + ), + // @todo, this should be required. + 'required' => TRUE, + 'description' => '', + 'default_value' => NULL, + 'id' => '8', + 'field_id' => '6', + 'field_name' => 'field_moderation', + 'entity_type' => 'node', + 'bundle' => 'article', + 'deleted' => '0', + ); + field_cache_clear(); + field_create_field($field_config); + field_cache_clear(); + field_create_instance($field_instance); +} diff --git a/modules/custom/moderation/moderation.module b/modules/custom/moderation/moderation.module new file mode 100644 index 0000000000000000000000000000000000000000..3ead5ec9c3c6890c9a6ea9317562be9431aab42c --- /dev/null +++ b/modules/custom/moderation/moderation.module @@ -0,0 +1,199 @@ +entityType(), moderation_load_active_vid($entity)); + // @todo, hard-coding for nodes might be unavoidable here. + return node_page_edit($active_revision); +} + +/** + * Implements hook_menu(). + */ +function moderation_menu() { + + $items = array(); + + // View the current draft of a node. + $items["node/%node/draft"] = array( + 'title' => 'View draft', + 'page callback' => 'moderation_view_draft', + 'page arguments' => array(1), + 'access callback' => 'moderation_draft_tab_access', + 'access arguments' => array(1), + 'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE, + 'type' => MENU_LOCAL_TASK, + 'weight' => -9, + ); + + return $items; +} + +/** + * @todo, this should be switched to an entity. + */ +function moderation_draft_tab_access($node) { + + if (node_access('update', $node)) { + $active_vid = moderation_load_active_vid($node); + // return TRUE if the active vid is different from the incoming vid. + return ($active_vid != $node->vid); + } + return FALSE; +} + +/** + * Utility function to load the active/editing revision of an entity. + * + * @param $entity + * The entity being acted upon. + * + * @return + * The entity with the highest revision id. + * @todo, a more intelligent way of determining active vid. + * + * @todo, make tests for just this function. + */ +function moderation_load_active_vid(Drupal\Core\Entity\EntityInterface $entity) { + + // @todo, loading entity info to get things that should be method calls. + $entity_info = entity_get_info($entity->entityType()); + + // The id() check should ensure that this entity has been saved to the db. + // Checking the revision table as a proxy for isRevisionable(). + if ($entity->id() && !empty($entity_info['revision_table'])) { + + // @todo Seems like there should be a method call for these. + if (!empty($entity_info['entity_keys']['id']) && !empty($entity_info['entity_keys']['revision'])) { + $id_key = $entity_info['entity_keys']['id']; + $revision_key = $entity_info['entity_keys']['revision']; + + $revision_table = $entity_info['revision_table']; + // @todo, node_revision can be derived from entity_info. Along with all of the + // other keys here. + $highest_vid = db_select($revision_table, 'rt') + ->fields('rt', array($revision_key)) + ->condition($id_key, $entity->id()) + ->range(0,1) + ->orderBy('rt.' . $revision_key, 'DESC') + ->execute() + ->fetchCol(); + + $highest_vid = reset($highest_vid); + return $highest_vid; + } + } + + // @todo, better error handling? + return FALSE; +} + +/** + * Displays the current draft the node, if it is not published. + * + * @param $node + * The node being acted upon. + * + * @return + * A fully themed node page. + */ +function moderation_view_draft(Drupal\Core\Entity\EntityInterface $entity) { + + // @todo, this function is not tied to nodes but the related functions are. + + // Replace "vid" with something abstracted. + $forward_revision = entity_revision_load($entity->entityType(), moderation_load_active_vid($entity)); + return moderation_router_item_page_callback($forward_revision); +} + +/** + * Get the menu router item for nodes. + * + * @param $node + * The node being acted upon. + * @return + * A fully themed node page. + */ +function moderation_router_item_page_callback($node) { + $router_item = menu_get_item('node/' . $node->nid); + if ($router_item['include_file']) { + require_once DRUPAL_ROOT . '/' . $router_item['include_file']; + } + + // Call whatever function is assigned to the main node path but pass the + // current node as an argument. This approach allows for the reuse of of Panel + // definition acting on node/%node. + return $router_item['page_callback']($node); +} + +/** + * Implements hook_entity_presave(); + **/ +function moderation_entity_presave(Drupal\Core\Entity\EntityInterface $entity) { + + $field_moderation = $entity->get('field_moderation'); + + // @todo, In a full implementation of this module there should be a much cleaner + // way of getting this value. + if (!empty($field_moderation[LANGUAGE_NOT_SPECIFIED][0]['value'])) { + $moderation_value = $field_moderation[LANGUAGE_NOT_SPECIFIED][0]['value']; + + // If "published," set status as TRUE; + // @todo A full implementation of this module would do better than check for a hardcoded string. + // @todo, this might belong better in hook_node_presave() since 'status' + // is a node property here. + if ($moderation_value === 'published') { + $entity->set('status', TRUE); + } else { + $entity->set('status', FALSE); + + // A new revision entity would also be the default entity. + if (!$entity->isNew()) { + + $original_revision = $entity->get('original'); + + if (!empty($original_revision)) { + $entity_status = $original_revision->get('status'); + if (!empty($entity_status)) { + $entity->set('isDefaultRevision', FALSE); + } + } + } + } + } +} + +/** + * Implments hook_form_alter(). + */ +function moderation_form_alter(&$form, &$form_state, $form_id) { + + // @todo, In a real implementation, this would need a more intelligent check for + // whether this is a form that should be altered. + if (!empty($form['field_moderation']) && !empty($form_state['entity'])) { + + // Restrict access to the Published checkbox. Moderation module will take over + // the published behavior. + if (!empty($form['options']['status'])) { + $form['options']['status']['#access'] = FALSE; + // @todo validate the state selected. There could be restrictions on when + // a state can be entered. + // $form['#validate'][] = 'moderation_form_alter_validate'; + } + } +}