diff --git a/migrations/agreement.yml b/migrations/agreement.yml new file mode 100644 index 0000000..8eb040a --- /dev/null +++ b/migrations/agreement.yml @@ -0,0 +1,25 @@ +id: agreement +label: Agreements +migration_tags: + - Drupal 6 + - Drupal 7 +source: + plugin: agreement +process: + id: id + type: + - plugin: get + source: type + - + # @todo The default agreement may not exist in Drupal 8? + plugin: default_value + default_value: 'default' + uid: uid + agreed: agreed + sid: sid + agreed_date: agreed_date +destination: + plugin: agreement +migration_dependencies: + optional: + - agreement_types diff --git a/migrations/agreement_types.yml b/migrations/agreement_types.yml new file mode 100644 index 0000000..9d8c4f1 --- /dev/null +++ b/migrations/agreement_types.yml @@ -0,0 +1,39 @@ +id: agreement_types +label: Agreement types +migration_tags: + - Drupal 7 +source: + plugin: agreement_type +process: + id: + - + plugin: machine_name + source: name + label: type + path: + - + plugin: callback + callable: + - '\Drupal\agreement\AgreementHandler' + - prefixPath + source: path + agreement: agreement + settings: + - + plugin: default_value + source: settings + default_value: "a:0:{}" + - + plugin: callback + callable: unserialize + - + plugin: agreement_settings + migrations: + - d7_user_role + +destination: + plugin: entity:agreement + config_name: agreement.agreement +migration_dependencies: + required: + - d7_user_role \ No newline at end of file diff --git a/src/AgreementHandler.php b/src/AgreementHandler.php index fdc5f37..d63b235 100644 --- a/src/AgreementHandler.php +++ b/src/AgreementHandler.php @@ -8,6 +8,7 @@ use Drupal\Core\Database\DatabaseExceptionWrapper; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Path\PathMatcherInterface; use Drupal\Core\Session\AccountProxyInterface; +use Drupal\user\UserInterface; /** * Agreement handler provides methods for looking up agreements. @@ -115,6 +116,21 @@ class AgreementHandler implements AgreementHandlerInterface { return $agreed !== NULL && $agreed > 0; } + /** + * {@inheritdoc} + */ + public function lastAgreed(Agreement $agreement, UserInterface $account) { + $query = $this->connection->select('agreement'); + $query + ->fields('agreement', ['agreed_date']) + ->condition('uid', $account->id()) + ->condition('type', $agreement->id()) + ->range(0, 1); + + $agreed_date = $query->execute()->fetchField(); + return $agreed_date === FALSE || $agreed_date === NULL ? -1 : $agreed_date; + } + /** * {@inheritdoc} */ @@ -181,4 +197,11 @@ class AgreementHandler implements AgreementHandlerInterface { return $info; } + /** + * {@inheritdoc} + */ + public static function prefixPath($value) { + return $value ? '/' . $value : $value; + } + } diff --git a/src/AgreementHandlerInterface.php b/src/AgreementHandlerInterface.php index 9d81d54..5ea3377 100644 --- a/src/AgreementHandlerInterface.php +++ b/src/AgreementHandlerInterface.php @@ -4,6 +4,7 @@ namespace Drupal\agreement; use Drupal\agreement\Entity\Agreement; use Drupal\Core\Session\AccountProxyInterface; +use Drupal\user\UserInterface; /** * Agreement handler interface. @@ -23,6 +24,19 @@ interface AgreementHandlerInterface { */ public function hasAgreed(Agreement $agreement, AccountProxyInterface $account); + /** + * Get the last agreement for the user for the agreement. + * + * @param \Drupal\agreement\Entity\Agreement $agreement + * The agreement to check if a user has agreed. + * @param \Drupal\user\UserInterface $account + * The user account to check. + * + * @return int + * The timestamp that the user last agreed or -1 if never agreed. + */ + public function lastAgreed(Agreement $agreement, UserInterface $account); + /** * Check if an user can bypass the agreement or if the agreement applies. * @@ -61,4 +75,15 @@ interface AgreementHandlerInterface { */ public function getAgreementByUserAndPath(AccountProxyInterface $account, $path); + /** + * Adds leading slash to a path string. + * + * @param string $value + * The value. + * + * @return string + * The new value. + */ + public static function prefixPath($value); + } diff --git a/src/Plugin/migrate/destination/Agreement.php b/src/Plugin/migrate/destination/Agreement.php new file mode 100644 index 0000000..b3a350a --- /dev/null +++ b/src/Plugin/migrate/destination/Agreement.php @@ -0,0 +1,107 @@ +connection = $connection; + } + + /** + * {@inheritdoc} + */ + public function getIds() { + return ['id' => ['type' => 'integer']]; + } + + /** + * {@inheritdoc} + */ + public function fields(MigrationInterface $migration = NULL) { + return [ + 'id' => $this->t('Unique Identifier'), + 'type' => $this->t('Agreement type name (Drupal 7)'), + 'uid' => $this->t('User Identifier'), + 'sid' => $this->t('Session Identifier'), + 'agreed' => $this->t('Agreed?'), + 'agreed_date' => $this->t('Agreement timestamp'), + ]; + } + + /** + * {@inheritdoc} + */ + public function import(Row $row, array $old_destination_id_values = []) { + $values = array_intersect_key($row->getDestination(), $this->fields()); + + try { + $status = $this->connection->merge('agreement') + ->key('id') + ->fields($values) + ->execute(); + } + catch (DatabaseExceptionWrapper $e) { + var_dump($e); + throw new MigrateSkipProcessException($e->getMessage()); + } + + return $status ? [$row->getDestinationProperty('id')] : NULL; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) { + $db_key = !empty($configuration['database_key']) ? $configuration['database_key'] : NULL; + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $migration, + Database::getConnection('default', $db_key) + ); + } + +} diff --git a/src/Plugin/migrate/process/AgreementSettings.php b/src/Plugin/migrate/process/AgreementSettings.php new file mode 100644 index 0000000..088dddd --- /dev/null +++ b/src/Plugin/migrate/process/AgreementSettings.php @@ -0,0 +1,179 @@ +migration = $migration; + $this->migrationPluginManager = $migration_plugin_manager; + $this->processPluginManager = $process_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + // Change property name for email recipient. + $value['recipient'] = $value['email_recipient']; + unset($value['email_recipient']); + + // Deal with roles which may not have been upgraded. + if (!is_array($value['role'])) { + $value['roles'] = [ + $this->getRoleId($value['role'], $migrate_executable), + ]; + } + else { + $value['roles'] = []; + foreach ($value['role'] as $role) { + $value['roles'] = $this->getRoleId($role, $migrate_executable); + } + } + unset($value['role']); + + // Map visibility settings and pages. + $value['visibility'] = [ + 'settings' => (int) $value['visibility_settings'], + 'pages' => [], + ]; + $pages = preg_split('/\r?\n/', $value['visibility_pages']); + if (!empty($pages)) { + foreach ($pages as $page) { + if ($page) { + $value['visibility']['pages'][] = '/' . $page; + } + } + } + unset($value['visibility_pages']); + unset($value['visibility_settings']); + + // Set a reset date. + $value['reset_date'] = 0; + + // Prefix destination path. + $value['destination'] = !empty($value['destination']) ? '/' . $value['destination'] : ''; + + return $value; + } + + /** + * Gets the new role ID from the old role name. + * + * @param string $value + * The role name. + * @param \Drupal\migrate\MigrateExecutableInterface $executable + * The migration execution. + * + * @return string + * The new role ID. + * + * @see \Drupal\migrate\Plugin\migrate\process\MachineName::transform() + * @see \Drupal\user\Plugin\migrate\process\UserUpdate8002::transform() + */ + protected function getRoleId($value, MigrateExecutableInterface $executable) { + if ($value === 1) { + return 'anonymous'; + } + elseif ($value === 2) { + return 'authenticated'; + } + + try { + $row = new Row(['rid' => $value], ['rid' => ['type' => 'integer']]); + $migration = $this->migrationPluginManager->createInstance('d7_user_role'); + $configuration = ['source' => 'rid']; + + $source_rid = $this->processPluginManager + ->createInstance('get', $configuration, $this->migration) + ->transform(NULL, $executable, $row, 'rid'); + + if (!is_array($source_rid)) { + $source_rid = [$source_rid]; + } + $source_id_values['d7_user_role'] = $source_rid; + + // Break out of the loop as soon as a destination ID is found. + if ($destination_ids = $migration->getIdMap()->lookupDestinationId($source_id_values['d7_user_role'])) { + return reset($destination_ids); + } + return $value; + } + catch (PluginException $e) { + return $value; + } + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $migration, + $container->get('plugin.manager.migration'), + $container->get('plugin.manager.migrate.process') + ); + } + +} diff --git a/src/Plugin/migrate/source/Agreement.php b/src/Plugin/migrate/source/Agreement.php new file mode 100644 index 0000000..473f980 --- /dev/null +++ b/src/Plugin/migrate/source/Agreement.php @@ -0,0 +1,47 @@ + ['type' => 'integer']]; + } + + /** + * {@inheritdoc} + */ + public function fields() { + return [ + 'id' => $this->t('Unique Identifier'), + 'type' => $this->t('Agreement type name (Drupal 7)'), + 'uid' => $this->t('User Identifier'), + 'sid' => $this->t('Session Identifier'), + 'agreed' => $this->t('Agreed?'), + 'agreed_date' => $this->t('Agreement timestamp'), + ]; + } + + /** + * {@inheritdoc} + */ + public function query() { + return $this->select('agreement', 'agreement') + ->fields('agreement', + ['id', 'uid', 'type', 'sid', 'agreed', 'agreed_date']); + } + +} diff --git a/src/Plugin/migrate/source/d7/AgreementType.php b/src/Plugin/migrate/source/d7/AgreementType.php new file mode 100644 index 0000000..f153ae7 --- /dev/null +++ b/src/Plugin/migrate/source/d7/AgreementType.php @@ -0,0 +1,51 @@ + $this->t('Unique name'), + 'type' => $this->t('Label'), + 'path' => $this->t('Path'), + 'settings' => $this->t('Settings'), + 'agreement' => $this->t('Agreement'), + ]; + } + + /** + * {@inheritdoc} + */ + public function getIds() { + return [ + 'name' => [ + 'type' => 'string', + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function query() { + return $this->select('agreement_type', 'agreement_type') + ->fields( + 'agreement_type', + ['name', 'type', 'path', 'settings', 'agreement']); + } + +} diff --git a/tests/fixtures/drupal7.php b/tests/fixtures/drupal7.php new file mode 100644 index 0000000..9c6038b --- /dev/null +++ b/tests/fixtures/drupal7.php @@ -0,0 +1,145 @@ +schema()->createTable('agreement_type', array( + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'name' => array( + 'type' => 'varchar', + 'length' => 100, + 'not null' => TRUE, + ), + 'type' => array( + 'type' => 'varchar', + 'length' => 150, + 'not null' => TRUE, + ), + 'path' => array( + 'type' => 'varchar', + 'length' => 150, + 'not null' => TRUE, + ), + 'settings' => array( + 'type' => 'blob', + 'size' => 'big', + 'not null' => TRUE, + ), + 'agreement' => array( + 'type' => 'text', + 'not null' => FALSE, + ), + ), + 'primary key' => array('id'), + 'unique keys' => array( + 'name' => array('name'), + 'path' => array('path'), + ), +)); + +$connection->insert('agreement_type') + ->fields(array('name', 'type', 'path', 'settings', 'agreement')) + ->values(array( + 'name' => 'default', + 'type' => 'Default agreement', + 'path' => 'agreement', + 'settings' => serialize(array( + // @todo https://www.drupal.org/project/agreement/issues/2374539 + 'role' => 2, + 'title' => 'Our agreement', + 'format' => 'filtered_html', + 'frequency' => 0, + 'success' => 'Thank you for accepting our agreement.', + 'failure' => 'You must accept our agreement to continue.', + 'revoked' => 'You successfully revoked your acceptance of our agreement', + 'checkbox' => 'I agree.', + 'submit' => 'Submit', + 'destination' => '', + 'visibility_settings' => 0, + 'visibility_pages' => '', + 'email_recipient' => '', + 'reset_date' => 0, + )), + 'agreement' => 'Default agreement.', + )) + ->values(array( + 'name' => 'node_1_agreement', + 'type' => 'Node 1 agreement', + 'path' => 'agree-to-node-1', + 'settings' => serialize(array( + // @todo https://www.drupal.org/project/agreement/issues/2374539 + 'role' => 3, + 'title' => 'Node 1 agreement', + 'format' => 'filtered_html', + 'frequency' => -1, + 'success' => 'Thank you for accepting our agreement.', + 'failure' => 'You must accept our agreement to continue.', + 'revoked' => 'You successfully revoked your acceptance of our agreement', + 'checkbox' => 'I agree to node 1', + 'submit' => 'Agree', + 'destination' => 'node/1', + 'visibility_settings' => 1, + 'visibility_pages' => 'node/1', + 'email_recipient' => '', + 'reset_date' => 0, + )), + 'agreement' => 'Agree to node 1.', + )) + ->execute(); + +$connection->schema()->createTable('agreement', array( + 'fields' => array( + 'id' => array( + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'type' => array( + 'type' => 'varchar', + 'length' => 100, + 'not null' => TRUE, + 'default' => 'default', + ), + 'uid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'agreed' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'sid' => array( + 'type' => 'varchar', + 'length' => 46, + ), + 'agreed_date' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + ), + 'primary key' => array('id'), + 'indexes' => array( + 'type_uid' => array('type', 'uid'), + ), +)); + +$connection->insert('agreement') + ->fields(array('type', 'uid', 'agreed', 'sid', 'agreed_date')) + ->values(array('default', 2, 1, '', 1444945097)) + ->values(array('node_1_agreement', 3, 1, '', 0)) + ->execute(); diff --git a/tests/src/Kernel/d7/AgreementMigrateTest.php b/tests/src/Kernel/d7/AgreementMigrateTest.php new file mode 100644 index 0000000..bdd7409 --- /dev/null +++ b/tests/src/Kernel/d7/AgreementMigrateTest.php @@ -0,0 +1,118 @@ +assertNotFalse(realpath($agreementFixture)); + $this->loadFixture($agreementFixture); + + $this->installEntitySchema('user_role'); + $this->installEntitySchema('filter_format'); + $this->installEntitySchema('agreement'); + $this->installSchema('agreement', ['agreement']); + + $migrations = [ + 'd7_filter_format', + 'd7_user_role', + 'd7_user', + 'agreement_types', + 'agreement', + ]; + + $this->executeMigrations($migrations); + } + + /** + * Asserts that agreement types and agreements migrated. + */ + public function testDataMigration() { + $agreementHandler = $this->container->get('agreement.handler'); + $entityTypeManager = $this->container->get('entity_type.manager'); + /** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $configStorage */ + $configStorage = $entityTypeManager->getStorage('agreement'); + $agreement_types = $configStorage->loadMultiple(); + + $this->assertEquals(2, count($agreement_types), 'Found two agreement types.'); + + $expected_default_settings = [ + 'roles' => ['authenticated'], + 'title' => 'Our agreement', + 'format' => 'filtered_html', + 'frequency' => 0, + 'success' => 'Thank you for accepting our agreement.', + 'failure' => 'You must accept our agreement to continue.', + 'revoked' => 'You successfully revoked your acceptance of our agreement', + 'checkbox' => 'I agree.', + 'submit' => 'Submit', + 'destination' => '', + 'visibility' => [ + 'settings' => 0, + 'pages' => [], + ], + 'recipient' => '', + 'reset_date' => 0, + ]; + + $expected_node1Agreement_settings = [ + 'roles' => ['administrator'], + 'title' => 'Node 1 agreement', + 'format' => 'filtered_html', + 'frequency' => -1, + 'success' => 'Thank you for accepting our agreement.', + 'failure' => 'You must accept our agreement to continue.', + 'revoked' => 'You successfully revoked your acceptance of our agreement', + 'checkbox' => 'I agree to node 1', + 'submit' => 'Agree', + 'destination' => '/node/1', + 'visibility' => [ + 'settings' => 1, + 'pages' => ['/node/1'], + ], + 'recipient' => '', + 'reset_date' => 0, + ]; + + /** @var \Drupal\agreement\Entity\Agreement $default */ + $default = $configStorage->load('default'); + $this->assertEquals('Default agreement', $default->label()); + $this->assertEquals('Default agreement.', $default->get('agreement')); + $this->assertEquals('/agreement', $default->get('path')); + $this->assertEquals($expected_default_settings, $default->getSettings()); + + /** @var \Drupal\agreement\Entity\Agreement $node1Agreement */ + $node1Agreement = $configStorage->load('node_1_agreement'); + $this->assertEquals('Node 1 agreement', $node1Agreement->label()); + $this->assertEquals('Agree to node 1.', $node1Agreement->get('agreement')); + $this->assertEquals('/agree-to-node-1', $node1Agreement->get('path')); + $this->assertEquals($expected_node1Agreement_settings, $node1Agreement->getSettings()); + + $user2 = User::load(2); + $user3 = User::load(3); + + $this->assertGreaterThan(-1, $agreementHandler->lastAgreed($default, $user2), 'Odo agreed to default agreement.'); + $this->assertEquals(-1, $agreementHandler->lastAgreed($default, $user3), 'Bob did not agree to default agreement.'); + + $this->assertGreaterThan(-1, $agreementHandler->lastAgreed($node1Agreement, $user3), 'Bob agreed to node 1 agreement.'); + $this->assertEquals(-1, $agreementHandler->lastAgreed($node1Agreement, $user2), 'Odo did not agree to node 1 agreement.'); + } + +}