diff --git a/publication_date.info.yml b/publication_date.info.yml index d3eaf0f..e502ace 100644 --- a/publication_date.info.yml +++ b/publication_date.info.yml @@ -3,3 +3,6 @@ description: "Add a field containing the publication date." type: module core: 8.x configure: admin/config/content/publication-date + +dependencies: + - drupal:node diff --git a/publication_date.install b/publication_date.install index 1cb9fcb..30668d4 100755 --- a/publication_date.install +++ b/publication_date.install @@ -5,32 +5,201 @@ * Installation functions for the Publication Date module. */ +use Drupal\Core\Database\Driver\mysql\Connection as MysqlConnection; +use Drupal\Core\Database\Driver\pgsql\Connection as PgsqlConnection; +use Drupal\Core\Messenger\MessengerInterface; + /** * Implements hook_install(). */ function publication_date_install() { - // Set publication dates for existing nodes. - _publication_date_update_existing(); + // Populate the publication date field with the timestamp of the first + // published revision. + _publication_date_populate_database_field(); // This module must be called after some other modules (i.e. Scheduler). module_set_weight('publication_date', 99); } /** - * Helper function to update the existing nodes on install. + * Helper function to populate the published on date for unpublished nodes. * - * We can not know the exact date of publication, so $node->published_at will - * initially contain the creation date for already publisned nodes. + * This function makes sure that all existing nodes have the publication date + * set. The publication date is derived from the first published revision. */ -function _publication_date_update_existing() { - // @todo: Convert to entity query. - return; - - $query = db_select('node'); - $query->addField('node', 'nid'); - $query->addField('node', 'created', 'published_at'); - $nids = $query->condition('status', 1); - db_insert('publication_date') - ->from($nids) - ->execute(); +function _publication_date_populate_database_field() { + $connection = \Drupal::database(); + + // The Drupal database API does not yet support update queries that use joins, + // meaning that this operation needs to be implemented using direct queries. + // It is currently tested only for MySQL, MariaDB, and Postgres. + if ($connection instanceof MysqlConnection) { + $queries = [ + // Update nodes with multiple revisions that have at least one published + // revision so the publication date is set to the timestamp of the first + // published revision. + [ + 'query' => <<= t.vid; +SQL + , + 'arguments' => [], + ], + + // Update the revisions table so that nodes that have a single published + // revision have the publication timestamp set to the creation timestamp. + [ + 'query' => << [], + ], + + // Set the remainder of the publication dates in the revisions table to the + // default timestamp. This applies to all revisions that were created before + // the node was first published. + [ + 'query' => << [':default_timestamp' => PUBLICATION_DATE_DEFAULT], + ], + + // Copy the publication date from the revisions table to the node table. + [ + 'query' => << [], + ], + ]; + } + elseif ($connection instanceof PgsqlConnection) { + $queries = [ + // Update nodes with multiple revisions that have at least one published + // revision so the publication date is set to the timestamp of the first + // published revision. + [ + 'query' => <<= t.vid; +SQL + , + 'arguments' => [], + ], + + // Update the revisions table so that nodes that have a single published + // revision have the publication timestamp set to the creation timestamp. + [ + 'query' => << [], + ], + + // Set the remainder of the publication dates in the revisions table to the + // default timestamp. This applies to all revisions that were created before + // the node was first published. + [ + 'query' => << [':default_timestamp' => PUBLICATION_DATE_DEFAULT], + ], + + // Copy the publication date from the revisions table to the node table. + [ + 'query' => << [], + ], + ]; + } + else { + $message = t('Populating the publication date on existing nodes is currently only supported on MySQL and Postgres databases.'); + \Drupal::messenger()->addMessage($message, MessengerInterface::TYPE_WARNING); + return; + } + + // Perform the operations in a single atomic transaction. + $transaction = $connection->startTransaction(); + try { + foreach ($queries as $query_data) { + \Drupal::database()->query($query_data['query'], $query_data['arguments']); + } + } + catch (Exception $e) { + $transaction->rollBack(); + throw new Exception('Database error', 0, $e); + } } diff --git a/publication_date.post_update.php b/publication_date.post_update.php new file mode 100755 index 0000000..687aa50 --- /dev/null +++ b/publication_date.post_update.php @@ -0,0 +1,16 @@ +getEntity(); return ($entity instanceof EntityPublishedInterface) ? $entity->isPublished() : FALSE; } /** - * @inheritDoc + * {@inheritdoc} */ public function isEmpty() { return FALSE; diff --git a/tests/src/Functional/PublicationDateInstallationTest.php b/tests/src/Functional/PublicationDateInstallationTest.php new file mode 100644 index 0000000..432511a --- /dev/null +++ b/tests/src/Functional/PublicationDateInstallationTest.php @@ -0,0 +1,98 @@ +createNode(['status' => 1]); + + // Create a node that was never published. + $unpublished = $this->createNode(['status' => 0]); + + // Create a node that only has one revision but has been edited after being + // created. As a result the changed time is later than the creation time. + $single_revision = $this->createNode(['status' => 1]); + $single_revision->setNewRevision(FALSE); + $single_revision->setCreatedTime(strtotime('2019-06-10 10:00')); + $single_revision->setChangedTime(strtotime('2019-07-20 10:00')); + $single_revision->save(); + + // Create a node that is currently unpublished but has been published in a + // previous revision. Start with an unpublished revision. + $previously_published = $this->createNode(['status' => 0]); + + // Create an published revision. + $previously_published->setNewRevision(); + $previously_published->setPublished(); + $previously_published->setRevisionCreationTime(strtotime('2019-07-20 10:00')); + $previously_published->save(); + + // Create a final unpublished revision. + $previously_published->setNewRevision(); + $previously_published->setUnpublished(); + $previously_published->setRevisionCreationTime(strtotime('2019-07-20 11:00')); + $previously_published->save(); + + // Install the module. This should populate our 'published_at' field data. + $this->container->get('module_installer')->install(['publication_date']); + + // The published node should have the right publication time set. + $this->assertEquals($published->getCreatedTime(), $this->select('node_field_data')->condition('nid', $published->id())->fields('node_field_data', ['published_at'])->execute()->fetchField()); + + // The unpublished node should have the default publication time. + $this->assertEquals(PUBLICATION_DATE_DEFAULT, $this->select('node_field_data')->condition('nid', $unpublished->id())->fields('node_field_data', ['published_at'])->execute()->fetchField()); + + // The node with a single revision which has been changed later should have + // the publication time set to the time the node was created. + $this->assertEquals($single_revision->getCreatedTime(), $this->select('node_field_data')->condition('nid', $single_revision->id())->fields('node_field_data', ['published_at'])->execute()->fetchField()); + + // The previously published node should have the right publication time. + $this->assertEquals(strtotime('2019-07-20 10:00'), $this->select('node_field_data')->condition('nid', $previously_published->id())->fields('node_field_data', ['published_at'])->execute()->fetchField()); + + // The first revision of the previously published node was unpublished, so + // it should have the default publication time. + $revision_ids = $this->container->get('entity_type.manager')->getStorage('node')->revisionIds($previously_published); + $this->assertEquals(PUBLICATION_DATE_DEFAULT, $this->select('node_field_revision')->condition('vid', $revision_ids[0])->fields('node_field_revision', ['published_at'])->execute()->fetchField()); + + // The second revision of the previously published node was the first + // published revision, so it should have the publication time. + $this->assertEquals(strtotime('2019-07-20 10:00'), $this->select('node_field_revision')->condition('vid', $revision_ids[1])->fields('node_field_revision', ['published_at'])->execute()->fetchField()); + + // The third revision of the previously published node is unpublished, but + // should have the publication time set. + $this->assertEquals(strtotime('2019-07-20 10:00'), $this->select('node_field_revision')->condition('vid', $revision_ids[2])->fields('node_field_revision', ['published_at'])->execute()->fetchField()); + } + + /** + * Returns a select query for the given database table. + * + * @param string $table + * The database table for which to return the query. + * + * @return \Drupal\Core\Database\Query\SelectInterface + * The select query. + */ + protected function select($table) { + return $this->container->get('database')->select($table); + } + +}