diff --git a/lingotek.module b/lingotek.module index 6ae4297..af7dd9c 100644 --- a/lingotek.module +++ b/lingotek.module @@ -123,6 +123,16 @@ function lingotek_entity_insert(EntityInterface $entity) { } } + // If workbench moderation is enabled, we need to prevent that content is + // processed if not the right state. + /** @var \Drupal\lingotek\Moderation\LingotekModerationFactoryInterface $moderation_factory */ + $moderation_factory = \Drupal::service('lingotek.moderation_factory'); + $moderation_handler = $moderation_factory->getModerationHandler(); + $prevent = $moderation_handler->shouldModerationPreventUpload($entity); + if ($prevent) { + return; + } + $profile = $configuration_service->getEntityProfile($entity); $has_autoupload = $profile->hasAutomaticUpload(); @@ -237,6 +247,16 @@ function lingotek_entity_update(EntityInterface $entity) { } } + // If workbench moderation is enabled, we need to prevent that content is + // processed if not the right state. + /** @var \Drupal\lingotek\Moderation\LingotekModerationFactoryInterface $moderation_factory */ + $moderation_factory = \Drupal::service('lingotek.moderation_factory'); + $moderation_handler = $moderation_factory->getModerationHandler(); + $prevent = $moderation_handler->shouldModerationPreventUpload($entity); + if ($prevent) { + return; + } + $profile = $configuration_service->getEntityProfile($entity); $has_autoupload = $profile->hasAutomaticUpload(); diff --git a/lingotek.services.yml b/lingotek.services.yml index cfd1052..63f092a 100644 --- a/lingotek.services.yml +++ b/lingotek.services.yml @@ -19,6 +19,13 @@ services: arguments: ['@entity_type.bundle.info'] lingotek.configuration: class: Drupal\lingotek\LingotekConfigurationService + lingotek.moderation_factory: + class: Drupal\lingotek\Moderation\LingotekModerationFactory + arguments: ['@config.factory'] + tags: + - { name: service_collector, tag: lingotek_moderation_configuration, call: addModerationConfiguration } + - { name: service_collector, tag: lingotek_moderation_form, call: addModerationForm } + - { name: service_collector, tag: lingotek_moderation_handler, call: addModerationHandler } lingotek.content_translation: class: Drupal\lingotek\LingotekContentTranslationService arguments: ['@lingotek', '@lingotek.language_locale_mapper', '@lingotek.configuration', '@lingotek.config_translation', '@entity.manager', '@language_manager'] @@ -41,3 +48,35 @@ services: arguments: ['@lingotek.config_translation', '@plugin.manager.config_translation.mapper'] tags: - { name: event_subscriber } + + lingotek.no_moderation_configuration: + class: Drupal\lingotek\Moderation\LingotekNoModerationConfigurationService + arguments: ['@module_handler'] + tags: + - { name: lingotek_moderation_configuration, priority: 0 } + lingotek.no_moderation_form: + class: Drupal\lingotek\Moderation\LingotekNoModerationSettingsForm + arguments: ['@module_handler'] + tags: + - { name: lingotek_moderation_form, priority: 0 } + lingotek.no_moderation_handler: + class: Drupal\lingotek\Moderation\LingotekNoModerationHandler + arguments: ['@module_handler'] + tags: + - { name: lingotek_moderation_handler, priority: 0 } + + lingotek.workbench_moderation_configuration: + class: Drupal\lingotek\Moderation\LingotekWorkbenchModerationConfigurationService + arguments: ['@module_handler', '@config.factory'] + tags: + - { name: lingotek_moderation_configuration, priority: 10 } + lingotek.workbench_moderation_form: + class: Drupal\lingotek\Moderation\LingotekWorkbenchModerationSettingsForm + arguments: ['@module_handler', '@entity_type.manager', '@lingotek.workbench_moderation_configuration', '@service_container'] + tags: + - { name: lingotek_moderation_form, priority: 10 } + lingotek.workbench_moderation_handler: + class: Drupal\lingotek\Moderation\LingotekWorkbenchModerationHandler + arguments: ['@module_handler', '@entity_type.manager', '@lingotek.workbench_moderation_configuration', '@service_container'] + tags: + - { name: lingotek_moderation_handler, priority: 10 } diff --git a/src/Form/LingotekSettingsTabContentForm.php b/src/Form/LingotekSettingsTabContentForm.php index 81d8b44..43aeae7 100644 --- a/src/Form/LingotekSettingsTabContentForm.php +++ b/src/Form/LingotekSettingsTabContentForm.php @@ -69,14 +69,24 @@ class LingotekSettingsTabContentForm extends LingotekConfigFormBase { 'content' => array(), ); + /** @var \Drupal\lingotek\Moderation\LingotekModerationFactoryInterface $moderationFactory */ + $moderationFactory = \Drupal::service('lingotek.moderation_factory'); + /** @var \Drupal\lingotek\Moderation\LingotekModerationSettingsFormInterface $moderationForm */ + $moderationForm = $moderationFactory->getModerationSettingsForm(); + $bundle_label = $entity_type_definitions[$entity_id]->getBundleLabel(); $header = array( $this->t('Enable'), $bundle_label, $this->t('Translation Profile'), + 'moderation' => $moderationForm->getColumnHeader(), $this->t('Fields'), ); + if (!$moderationForm->needsColumn($entity_id)) { + unset($header['moderation']); + } + $table = array( '#type' => 'table', '#header' => $header, @@ -95,6 +105,12 @@ class LingotekSettingsTabContentForm extends LingotekConfigFormBase { '#markup' => $bundle['label'], ); $row['profiles'] = $this->retrieveProfiles($entity_id, $bundle_id); + + $moderation = $moderationForm->form($entity_id, $bundle_id); + if (!empty($moderation)) { + $row['moderation'] = $moderation; + } + $row['fields'] = $this->retrieveFields($entity_id, $bundle_id); $table[$bundle_id] = $row; } @@ -129,11 +145,10 @@ class LingotekSettingsTabContentForm extends LingotekConfigFormBase { $lingotek_config = \Drupal::service('lingotek.configuration'); $form_values = $form_state->getValues(); - $data = array(); // For every content type, save the profile and fields in the Lingotek object foreach ($this->translatable_bundles as $entity_id => $bundles) { - foreach($form_values[$entity_id] as $bundle_id => $bundle) { + foreach ($form_values[$entity_id] as $bundle_id => $bundle) { // Only process if we have marked the checkbox. if ($bundle['enabled']) { if (!$lingotek_config->isEnabled($entity_id, $bundle_id)) { @@ -156,6 +171,12 @@ class LingotekSettingsTabContentForm extends LingotekConfigFormBase { } } $lingotek_config->setDefaultProfileId($entity_id, $bundle_id, $form_values[$entity_id][$bundle_id]['profiles']); + + /** @var \Drupal\lingotek\Moderation\LingotekModerationFactoryInterface $moderationFactory */ + $moderationFactory = \Drupal::service('lingotek.moderation_factory'); + /** @var \Drupal\lingotek\Moderation\LingotekModerationSettingsFormInterface $moderationForm */ + $moderationForm = $moderationFactory->getModerationSettingsForm(); + $moderationForm->submitHandler($entity_id, $bundle_id, $bundle); } else { // If we removed it, unable it. diff --git a/src/LingotekContentTranslationService.php b/src/LingotekContentTranslationService.php index 6306f9c..b3875cf 100644 --- a/src/LingotekContentTranslationService.php +++ b/src/LingotekContentTranslationService.php @@ -1051,6 +1051,13 @@ class LingotekContentTranslationService implements LingotekContentTranslationSer } } + // If there is any content moderation module is enabled, we may need to + // perform a transition in their workflow. + /** @var \Drupal\lingotek\Moderation\LingotekModerationFactoryInterface $moderation_factory */ + $moderation_factory = \Drupal::service('lingotek.moderation_factory'); + $moderation_handler = $moderation_factory->getModerationHandler(); + $moderation_handler->performModerationTransitionIfNeeded($entity); + $translation->save(); return $entity; diff --git a/src/Moderation/LingotekModerationConfigurationServiceInterface.php b/src/Moderation/LingotekModerationConfigurationServiceInterface.php new file mode 100644 index 0000000..251d19a --- /dev/null +++ b/src/Moderation/LingotekModerationConfigurationServiceInterface.php @@ -0,0 +1,68 @@ +config = []; + $this->forms = []; + $this->handlers = []; + } + + /** + * {@inheritdoc} + */ + public function addModerationConfiguration(LingotekModerationConfigurationServiceInterface $service, $id, $priority) { + $this->config[$priority] = $service; + krsort($this->config); + } + + /** + * {@inheritdoc} + */ + public function addModerationForm(LingotekModerationSettingsFormInterface $service, $id, $priority) { + $this->forms[$priority] = $service; + krsort($this->forms); + } + + /** + * {@inheritdoc} + */ + public function addModerationHandler(LingotekModerationHandlerInterface $service, $id, $priority) { + $this->handlers[$priority] = $service; + krsort($this->handlers); + } + + /** + * {@inheritdoc} + */ + public function getModerationConfigurationService() { + foreach ($this->config as $service) { + if ($service->applies()) { + return $service; + } + } + } + + /** + * {@inheritdoc} + */ + public function getModerationSettingsForm() { + foreach ($this->forms as $service) { + if ($service->applies()) { + return $service; + } + } + } + + /** + * {@inheritdoc} + */ + public function getModerationHandler() { + foreach ($this->handlers as $service) { + if ($service->applies()) { + return $service; + } + } + } + +} diff --git a/src/Moderation/LingotekModerationFactoryInterface.php b/src/Moderation/LingotekModerationFactoryInterface.php new file mode 100644 index 0000000..23aea8c --- /dev/null +++ b/src/Moderation/LingotekModerationFactoryInterface.php @@ -0,0 +1,73 @@ +moduleHandler = $module_handler; + return $this; + } + +} diff --git a/src/Moderation/LingotekNoModerationConfigurationService.php b/src/Moderation/LingotekNoModerationConfigurationService.php new file mode 100644 index 0000000..2ab9b3c --- /dev/null +++ b/src/Moderation/LingotekNoModerationConfigurationService.php @@ -0,0 +1,42 @@ +moduleHandler->moduleExists('workbench_moderation'); + } + + /** + * {@inheritdoc} + */ + public function setModuleHandler(ModuleHandlerInterface $module_handler) { + $this->moduleHandler = $module_handler; + return $this; + } + +} diff --git a/src/Moderation/LingotekWorkbenchModerationConfigurationService.php b/src/Moderation/LingotekWorkbenchModerationConfigurationService.php new file mode 100644 index 0000000..8ae446c --- /dev/null +++ b/src/Moderation/LingotekWorkbenchModerationConfigurationService.php @@ -0,0 +1,75 @@ +setModuleHandler($module_handler); + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public function getUploadStatus($entity_type_id, $bundle) { + $config = $this->configFactory->get('lingotek.settings'); + $upload_status = $config->get('translate.entity.' . $entity_type_id . '.' . $bundle . '.workbench_moderation.upload_status'); + return $upload_status; + } + + /** + * {@inheritdoc} + */ + public function getDownloadTransition($entity_type_id, $bundle) { + $config = $this->configFactory->get('lingotek.settings'); + $download_transition = $config->get('translate.entity.' . $entity_type_id . '.' . $bundle . '.workbench_moderation.download_transition'); + return $download_transition; + } + + /** + * {@inheritdoc} + */ + public function setUploadStatus($entity_type_id, $bundle, $status) { + $config = $this->configFactory->getEditable('lingotek.settings'); + $config->set('translate.entity.' . $entity_type_id . '.' . $bundle . '.workbench_moderation.upload_status', $status); + $config->save(); + return $status; + } + + /** + * {@inheritdoc} + */ + public function setDownloadTransition($entity_type_id, $bundle, $transition) { + $config = $this->configFactory->getEditable('lingotek.settings'); + $config->set('translate.entity.' . $entity_type_id . '.' . $bundle . '.workbench_moderation.download_transition', $transition); + $config->save(); + return $transition; + } + +} diff --git a/src/Moderation/LingotekWorkbenchModerationHandler.php b/src/Moderation/LingotekWorkbenchModerationHandler.php new file mode 100644 index 0000000..3dc410d --- /dev/null +++ b/src/Moderation/LingotekWorkbenchModerationHandler.php @@ -0,0 +1,126 @@ +setModuleHandler($module_handler); + $this->entityTypeManager = $entity_type_manager; + $this->moderationConfiguration = $moderation_configuration; + // We need a service we cannot depend on, as it may not exist if the module + // is not present. Ignore the error. + if ($container->has('workbench_moderation.moderation_information')) { + $this->moderationInfo = $container->get('workbench_moderation.moderation_information'); + } + } + + /** + * {@inheritdoc} + */ + public function shouldModerationPreventUpload(EntityInterface $entity) { + $prevent = FALSE; + if ($this->moduleHandler->moduleExists('workbench_moderation')) { + $moderationEnabled = $this->isModerationEnabled($entity); + if ($moderationEnabled) { + $uploadStatus = $this->moderationConfiguration->getUploadStatus($entity->getEntityTypeId(), $entity->bundle()); + $state = $this->getModerationState($entity); + if ($state !== $uploadStatus) { + $prevent = TRUE; + } + } + } + return $prevent; + } + + /** + * {@inheritdoc} + */ + public function performModerationTransitionIfNeeded(ContentEntityInterface &$entity) { + if ($this->moderationInfo->isModeratableBundle($entity->getEntityType(), $entity->bundle())) { + $transition = $this->moderationConfiguration->getDownloadTransition($entity->getEntityTypeId(), $entity->bundle()); + if ($transition !== NULL) { + $transition = $this->entityTypeManager->getStorage('moderation_state_transition')->load($transition); + if ($transition !== NULL) { + // Ensure we can execute this transition. + if ($this->getModerationState($entity) === $transition->getFromState()) { + $this->setModerationState($entity, $transition->getToState()); + } + } + } + } + } + + /** + * {@inheritdoc} + */ + public function getModerationState(ContentEntityInterface $entity) { + return $entity->get('moderation_state')->target_id; + } + + /** + * {@inheritdoc} + */ + public function setModerationState(ContentEntityInterface $entity, $state) { + $entity->set('moderation_state', $state); + $entity->save(); + } + + /** + * {@inheritdoc} + */ + public function isModerationEnabled(EntityInterface $entity) { + $bundleEntityType = $entity->getEntityType()->getBundleEntityType(); + $bundleType = $this->entityTypeManager->getStorage($bundleEntityType) + ->load($entity->bundle()); + $moderationEnabled = $bundleType->getThirdPartySetting('workbench_moderation', 'enabled', FALSE); + return $moderationEnabled; + } + +} diff --git a/src/Moderation/LingotekWorkbenchModerationSettingsForm.php b/src/Moderation/LingotekWorkbenchModerationSettingsForm.php new file mode 100644 index 0000000..af19b37 --- /dev/null +++ b/src/Moderation/LingotekWorkbenchModerationSettingsForm.php @@ -0,0 +1,200 @@ +setModuleHandler($module_handler); + $this->entityTypeManager = $entity_type_manager; + $this->moderationConfiguration = $moderation_configuration; + // We need a service we cannot depend on, as it may not exist if the module + // is not present. Ignore the error. + if ($container->has('workbench_moderation.moderation_information')) { + $this->moderationInfo = $container->get('workbench_moderation.moderation_information'); + } + } + + /** + * {@inheritdoc} + */ + public function getColumnHeader() { + return $this->t('Workbench Moderation'); + } + + /** + * {@inheritdoc} + */ + public function needsColumn($entity_type_id) { + $entity_type_definition = $this->entityTypeManager->getDefinition($entity_type_id); + return ($this->moduleHandler->moduleExists('workbench_moderation') && + ($this->moderationInfo !== NULL && $this->moderationInfo->isModeratableEntityType($entity_type_definition))); + } + + /** + * {@inheritdoc} + */ + public function getModerationUploadStatuses($entity_type_id, $bundle) { + $states = $this->entityTypeManager->getStorage('moderation_state')->loadMultiple(); + $values = []; + foreach ($states as $state_id => $state) { + $values[$state_id] = $state->label(); + } + return $values; + } + + /** + * {@inheritdoc} + */ + public function getDefaultModerationUploadStatus($entity_type_id, $bundle) { + $status = $this->moderationConfiguration->getUploadStatus($entity_type_id, $bundle); + + if (!$status) { + $published_statuses = $this->entityTypeManager->getStorage('moderation_state')->getQuery()->condition('published', TRUE)->execute(); + if (count($published_statuses) > 0) { + $status = reset($published_statuses); + } + } + return $status; + } + + /** + * {@inheritdoc} + */ + public function getModerationDownloadTransitions($entity_type_id, $bundle) { + $transitions = $this->entityTypeManager->getStorage('moderation_state_transition')->loadMultiple(); + $values = []; + foreach ($transitions as $transition_id => $transition) { + $values[$transition_id] = $transition->label(); + } + return $values; + } + + /** + * {@inheritdoc} + */ + public function getDefaultModerationDownloadTransition($entity_type_id, $bundle) { + $transition = $this->moderationConfiguration->getDownloadTransition($entity_type_id, $bundle); + + if (!$transition) { + $transitions = $this->entityTypeManager->getStorage('moderation_state_transition')->getQuery() + ->condition('stateFrom', $this->getDefaultWorkbenchModerationUploadStatus($entity_type_id, $bundle)) + ->execute(); + if (count($transitions) > 0) { + $transitions = $this->entityTypeManager->getStorage('moderation_state_transition')->loadMultiple($transitions); + /** @var \Drupal\workbench_moderation\ModerationStateTransitionInterface $potential_transition */ + foreach ($transitions as $transition_id => $potential_transition) { + /** @var \Drupal\workbench_moderation\ModerationStateInterface $toState */ + $toState = $this->entityTypeManager->getStorage('moderation_state')->load($potential_transition->getToState()); + if ($toState->isPublishedState()) { + $transition = $transition_id; + break; + } + } + } + } + return $transition; + } + + /** + * {@inheritdoc} + */ + public function form($entity_type_id, $bundle) { + // We only add this option if the workbench moderation is enabled. + $entity_type_definition = $this->entityTypeManager->getDefinition($entity_type_id); + $form = []; + + if ($this->moderationInfo->isModeratableBundle($entity_type_definition, $bundle)) { + $statuses = $this->getModerationUploadStatuses($entity_type_id, $bundle); + $default_status = $this->getDefaultModerationUploadStatus($entity_type_id, $bundle); + + $transitions = $this->getModerationDownloadTransitions($entity_type_id, $bundle); + $default_transition = $this->getDefaultModerationDownloadTransition($entity_type_id, $bundle); + $form['upload_status'] = [ + '#type' => 'select', + '#options' => $statuses, + '#default_value' => $default_status, + '#title' => $this->t('In which status needs to be uploaded?'), + ]; + $form['download_transition'] = [ + '#type' => 'select', + '#options' => $transitions, + '#default_value' => $default_transition, + '#title' => $this->t('Which transition should be executed after download?'), + ]; + } + elseif ($this->moderationInfo->isModeratableEntityType($entity_type_definition)) { + $bundle_type_id = $entity_type_definition->getBundleEntityType(); + $form = [ + '#markup' => $this->t('This entity bundle is not enabled for moderation with workbench_moderation. You can change its settings here.', [':moderation' => \Drupal::url("entity.$bundle_type_id.moderation", [$bundle_type_id => $bundle])]) + ]; + } + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitHandler($entity_type_id, $bundle, $form_values) { + $upload_status = $form_values['moderation']['upload_status']; + $download_transition = $form_values['moderation']['download_transition']; + + $this->moderationConfiguration->setUploadStatus($entity_type_id, $bundle, $upload_status); + $this->moderationConfiguration->setDownloadTransition($entity_type_id, $bundle, $download_transition); + } + +} diff --git a/src/Tests/LingotekWorkbenchModerationSettingsTest.php b/src/Tests/LingotekWorkbenchModerationSettingsTest.php new file mode 100644 index 0000000..3a53a6d --- /dev/null +++ b/src/Tests/LingotekWorkbenchModerationSettingsTest.php @@ -0,0 +1,173 @@ +drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + $this->drupalCreateContentType(['type' => 'page', 'name' => 'Page']); + + $this->vocabulary = $this->createVocabulary(); + + // Add a language. + ConfigurableLanguage::createFromLangcode('es') + ->setThirdPartySetting('lingotek', 'locale', 'es_MX') + ->save(); + + // Enable translation for the current entity type and ensure the change is + // picked up. + ContentLanguageSettings::loadByEntityTypeBundle('node', 'article')->setLanguageAlterable(TRUE)->save(); + \Drupal::service('content_translation.manager')->setEnabled('node', 'article', TRUE); + + ContentLanguageSettings::loadByEntityTypeBundle('node', 'page')->setLanguageAlterable(TRUE)->save(); + \Drupal::service('content_translation.manager')->setEnabled('node', 'page', TRUE); + + ContentLanguageSettings::loadByEntityTypeBundle('taxonomy_term', $this->vocabulary->id())->setLanguageAlterable(TRUE)->save(); + \Drupal::service('content_translation.manager')->setEnabled('taxonomy_term', $this->vocabulary->id(), TRUE); + + drupal_static_reset(); + \Drupal::entityManager()->clearCachedDefinitions(); + \Drupal::service('entity.definition_update_manager')->applyUpdates(); + // Rebuild the container so that the new languages are picked up by services + // that hold a list of languages. + $this->rebuildContainer(); + } + + /** + * Tests that the workbench moderation settings are stored correctly. + */ + protected function testWorkbenchModerationSettings() { + $this->drupalGet('admin/lingotek/settings'); + + // We don't have any fields for configuring workbench moderation until it's + // enabled. + $this->assertNoField('node[article][moderation][upload_status]', + 'The field for setting the state when a content should be uploaded does not exist as workbench moderation is not enabled for this bundle.'); + $this->assertNoField('node[article][moderation][download_transition]', + 'The field for setting the transition that must happen after download does not exist as workbench moderation is not enabled for this bundle.'); + + $this->assertNoField('node[page][moderation][upload_status]', + 'The field for setting the state when a content should be uploaded does not exist as workbench moderation is not enabled for this bundle.'); + $this->assertNoField('node[page][moderation][download_transition]', + 'The field for setting the transition that must happen after download does not exist as workbench moderation is not enabled for this bundle.'); + + // We show a message and link for enabling it. + $this->assertText('This entity bundle is not enabled for moderation with workbench_moderation. You can change its settings here.'); + $this->assertLinkByHref('/admin/structure/types/manage/article/moderation'); + $this->assertLinkByHref('/admin/structure/types/manage/page/moderation'); + + // Let's enable it for articles. + $this->enableModerationThroughUI('article', ['draft', 'needs_review', 'published'], 'draft'); + + $this->drupalGet('admin/lingotek/settings'); + + // Assert the fields for setting up the integration exist and they have + // sensible defaults. + $this->assertField('node[article][moderation][upload_status]', + 'The field for setting the state when a content should be uploaded exists.'); + $this->assertField('node[article][moderation][download_transition]', + 'The field for setting the transition that must happen after download exists.'); + $this->assertOptionSelected('edit-node-article-moderation-upload-status', 'published', + 'The default value is a published one.'); + $this->assertOptionSelected('edit-node-article-moderation-download-transition', 'published_published', + 'The default transition is from published to published.'); + + // But not for the other content types. There is still a message for configuring. + $this->assertNoField('node[page][moderation][upload_status]', + 'The field for setting the state when a content should be uploaded does not exist as workbench moderation is not enabled for this bundle.'); + $this->assertNoField('node[page][moderation][download_transition]', + 'The field for setting the transition that must happen after download does not exist as workbench moderation is not enabled for this bundle.'); + $this->assertText('This entity bundle is not enabled for moderation with workbench_moderation. You can change its settings here.'); + $this->assertNoLinkByHref('/admin/structure/types/manage/article/moderation'); + $this->assertLinkByHref('/admin/structure/types/manage/page/moderation'); + + // Let's save the settings for articles. + $edit = [ + 'node[article][enabled]' => 1, + 'node[article][profiles]' => 'automatic', + 'node[article][fields][title]' => 1, + 'node[article][moderation][upload_status]' => 'draft', + 'node[article][moderation][download_transition]' => 'draft_needs_review', + ]; + $this->drupalPostForm(NULL, $edit, 'Save', [], [], 'lingoteksettings-tab-content-form'); + + // Assert the values are saved. + $this->assertOptionSelected('edit-node-article-moderation-upload-status', 'draft', + 'The desired status for upload is stored correctly.'); + $this->assertOptionSelected('edit-node-article-moderation-download-transition', 'draft_needs_review', + 'The desired transition after download is stored correctly.'); + + // It never existed for taxonomies. + $this->assertNoField("taxonomy_term[{$this->vocabulary->id()}][moderation][upload_status]", + 'The field for setting the state when a content should be uploaded does not exist as workbench moderation is not available for this entity type.'); + $this->assertNoField("taxonomy_term[{$this->vocabulary->id()}][moderation][download_transition]", + 'The field for setting the transition that must happen after download does not exist as workbench moderation is not available for this entity type.'); + $this->assertNoLinkByHref("/admin/structure/taxonomy/manage/{$this->vocabulary->id()}/moderation", 'There is no link to moderation settings in taxonomies as they cannot be moderated.'); + + $header = $this->xpath("//details[@id='edit-entity-node']//th[text()='Workbench Moderation']"); + $this->assertEqual(count($header), 1, 'There is a Workbench Moderation column for content.'); + $header = $this->xpath("//details[@id='edit-entity-taxonomy-term']//th[text()='Workbench Moderation']"); + $this->assertEqual(count($header), 0, 'There is no Workbench Moderation column for taxonomies.'); + + } + + /** + * Enable moderation for a specified content type, using the UI. + * + * @param string $content_type_id + * Machine name. + * @param string[] $allowed_states + * Array of allowed state IDs. + * @param string $default_state + * Default state. + */ + protected function enableModerationThroughUI($content_type_id, array $allowed_states, $default_state) { + $this->drupalGet('admin/structure/types/manage/' . $content_type_id . '/moderation'); + $this->assertFieldByName('enable_moderation_state'); + $this->assertNoFieldChecked('edit-enable-moderation-state'); + + $edit['enable_moderation_state'] = 1; + + /** @var ModerationState $state */ + foreach (ModerationState::loadMultiple() as $id => $state) { + $key = $state->isPublishedState() ? 'allowed_moderation_states_published[' . $state->id() . ']' : 'allowed_moderation_states_unpublished[' . $state->id() . ']'; + $edit[$key] = (int)in_array($id, $allowed_states); + } + + $edit['default_moderation_state'] = $default_state; + + $this->drupalPostForm(NULL, $edit, t('Save')); + } + +} diff --git a/src/Tests/LingotekWorkbenchModerationTest.php b/src/Tests/LingotekWorkbenchModerationTest.php new file mode 100644 index 0000000..0445a0a --- /dev/null +++ b/src/Tests/LingotekWorkbenchModerationTest.php @@ -0,0 +1,384 @@ +drupalPlaceBlock('page_title_block', ['region' => 'content', 'weight' => -5]); + $this->drupalPlaceBlock('local_tasks_block', ['region' => 'content', 'weight' => -10]); + + $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']); + $this->drupalCreateContentType(['type' => 'page', 'name' => 'Page']); + + // Add a language. + ConfigurableLanguage::createFromLangcode('es') + ->setThirdPartySetting('lingotek', 'locale', 'es_MX') + ->save(); + + // Enable translation for the current entity type and ensure the change is + // picked up. + ContentLanguageSettings::loadByEntityTypeBundle('node', 'article') + ->setLanguageAlterable(TRUE) + ->save(); + \Drupal::service('content_translation.manager') + ->setEnabled('node', 'article', TRUE); + + drupal_static_reset(); + \Drupal::entityManager()->clearCachedDefinitions(); + \Drupal::service('entity.definition_update_manager')->applyUpdates(); + // Rebuild the container so that the new languages are picked up by services + // that hold a list of languages. + $this->rebuildContainer(); + + // Enable workbench moderation. + $this->enableModerationThroughUI('article', ['draft', 'needs_review', 'published'], 'draft'); + + $edit = [ + 'node[article][enabled]' => 1, + 'node[article][profiles]' => 'automatic', + 'node[article][fields][title]' => 1, + 'node[article][fields][body]' => 1, + 'node[article][moderation][upload_status]' => 'draft', + 'node[article][moderation][download_transition]' => 'draft_needs_review', + ]; + $this->drupalPostForm('admin/lingotek/settings', $edit, 'Save', [], [], 'lingoteksettings-tab-content-form'); + } + + /** + * Tests creating an entity with automatic profile but not in upload state is not uploaded. + */ + public function testCreateEntityWithAutomaticProfileButNotInUploadState() { + $edit = []; + $edit['title[0][value]'] = 'Llamas are cool'; + $edit['body[0][value]'] = 'Llamas are very cool'; + $edit['langcode[0][value]'] = 'en'; + $edit['lingotek_translation_profile'] = 'automatic'; + $this->drupalPostForm('/node/add/article', $edit, t('Save and Request Review')); + + $this->assertText('Article Llamas are cool has been created.'); + $this->assertNoText('Llamas are cool sent to Lingotek successfully.'); + } + + /** + * Tests creating an entity with manual profile but not in upload state is not uploaded. + */ + public function testCreateEntityWithManualProfileButNotInUploadState() { + $edit = []; + $edit['title[0][value]'] = 'Llamas are cool'; + $edit['body[0][value]'] = 'Llamas are very cool'; + $edit['langcode[0][value]'] = 'en'; + $edit['lingotek_translation_profile'] = 'manual'; + $this->drupalPostForm('/node/add/article', $edit, t('Save and Request Review')); + + $this->assertText('Article Llamas are cool has been created.'); + $this->assertNoText('Llamas are cool sent to Lingotek successfully.'); + } + + /** + * Tests creating an entity with automatic profile and in upload state is uploaded. + */ + public function testCreateEntityWithAutomaticProfileAndInUploadState() { + $edit = []; + $edit['title[0][value]'] = 'Llamas are cool'; + $edit['body[0][value]'] = 'Llamas are very cool'; + $edit['langcode[0][value]'] = 'en'; + $edit['lingotek_translation_profile'] = 'automatic'; + $this->drupalPostForm('/node/add/article', $edit, t('Save and Create New Draft')); + + $this->assertText('Article Llamas are cool has been created.'); + $this->assertText('Llamas are cool sent to Lingotek successfully.'); + } + + /** + * Tests creating an entity with manual profile and in upload state is not uploaded. + */ + public function testCreateEntityWithManualProfileAndInUploadState() { + $edit = []; + $edit['title[0][value]'] = 'Llamas are cool'; + $edit['body[0][value]'] = 'Llamas are very cool'; + $edit['langcode[0][value]'] = 'en'; + $edit['lingotek_translation_profile'] = 'manual'; + $this->drupalPostForm('/node/add/article', $edit, t('Save and Create New Draft')); + + $this->assertText('Article Llamas are cool has been created.'); + $this->assertNoText('Llamas are cool sent to Lingotek successfully.'); + } + + /** + * Tests updating an entity with automatic profile but not in upload state is not uploaded. + */ + public function testUpdateEntityWithAutomaticProfileButNotInUploadState() { + $edit = []; + $edit['title[0][value]'] = 'Llamas are cool'; + $edit['body[0][value]'] = 'Llamas are very cool'; + $edit['langcode[0][value]'] = 'en'; + $edit['lingotek_translation_profile'] = 'automatic'; + $this->drupalPostForm('/node/add/article', $edit, t('Save and Create New Draft')); + + $this->assertText('Article Llamas are cool has been created.'); + $this->drupalPostForm('/node/1/edit', $edit, t('Save and Request Review (this translation)')); + + $this->assertText('Article Llamas are cool has been updated.'); + $this->assertNoText('Llamas are cool was updated and sent to Lingotek successfully.'); + } + + /** + * Tests updating an entity with manual profile but not in upload state is not uploaded. + */ + public function testUpdateEntityWithManualProfileButNotInUploadState() { + $edit = []; + $edit['title[0][value]'] = 'Llamas are cool'; + $edit['body[0][value]'] = 'Llamas are very cool'; + $edit['langcode[0][value]'] = 'en'; + $edit['lingotek_translation_profile'] = 'manual'; + $this->drupalPostForm('/node/add/article', $edit, t('Save and Create New Draft')); + + $this->assertText('Article Llamas are cool has been created.'); + $this->drupalPostForm('/node/1/edit', $edit, t('Save and Request Review (this translation)')); + + $this->assertText('Article Llamas are cool has been updated.'); + $this->assertNoText('Llamas are cool was updated and sent to Lingotek successfully.'); + } + + /** + * Tests updating an entity with automatic profile and in upload state is uploaded. + */ + public function testUpdateEntityWithAutomaticProfileAndInUploadState() { + $edit = []; + $edit['title[0][value]'] = 'Llamas are cool'; + $edit['body[0][value]'] = 'Llamas are very cool'; + $edit['langcode[0][value]'] = 'en'; + $edit['lingotek_translation_profile'] = 'automatic'; + $this->drupalPostForm('/node/add/article', $edit, t('Save and Create New Draft')); + + $this->assertText('Article Llamas are cool has been created.'); + $this->drupalPostForm('/node/1/edit', $edit, t('Save and Create New Draft (this translation)')); + + $this->assertText('Article Llamas are cool has been updated.'); + $this->assertText('Llamas are cool was updated and sent to Lingotek successfully.'); + } + + /** + * Tests updating an entity with manual profile and in upload state is not uploaded. + */ + public function testUpdateEntityWithManualProfileAndInUploadState() { + $edit = []; + $edit['title[0][value]'] = 'Llamas are cool'; + $edit['body[0][value]'] = 'Llamas are very cool'; + $edit['langcode[0][value]'] = 'en'; + $edit['lingotek_translation_profile'] = 'manual'; + $this->drupalPostForm('/node/add/article', $edit, t('Save and Create New Draft')); + + $this->assertText('Article Llamas are cool has been created.'); + $this->drupalPostForm('/node/1/edit', $edit, t('Save and Create New Draft (this translation)')); + + $this->assertText('Article Llamas are cool has been updated.'); + $this->assertNoText('Llamas are cool was updated and sent to Lingotek successfully.'); + } + + protected function configureNeedsReviewAsUploadState() { + $edit = [ + 'node[article][enabled]' => 1, + 'node[article][profiles]' => 'automatic', + 'node[article][fields][title]' => 1, + 'node[article][fields][body]' => 1, + 'node[article][moderation][upload_status]' => 'needs_review', + 'node[article][moderation][download_transition]' => 'needs_review_published', + ]; + $this->drupalPostForm('admin/lingotek/settings', $edit, 'Save', [], [], 'lingoteksettings-tab-content-form'); + } + + public function testModerationToUploadStateWithAutomaticProfileTriggersUpload() { + $this->configureNeedsReviewAsUploadState(); + + $edit = []; + $edit['title[0][value]'] = 'Llamas are cool'; + $edit['body[0][value]'] = 'Llamas are very cool'; + $edit['langcode[0][value]'] = 'en'; + $edit['lingotek_translation_profile'] = 'automatic'; + $this->drupalPostForm('/node/add/article', $edit, t('Save and Create New Draft')); + + // Moderate. + $edit = ['new_state' => 'needs_review']; + $this->drupalPostForm(NULL, $edit, 'Apply'); + $this->assertText('The moderation state has been updated.'); + $this->assertText('Llamas are cool sent to Lingotek successfully.'); + } + + public function testModerationToNonUploadStateWithAutomaticProfileDoesntTriggerUpload() { + $this->configureNeedsReviewAsUploadState(); + + $edit = []; + $edit['title[0][value]'] = 'Llamas are cool'; + $edit['body[0][value]'] = 'Llamas are very cool'; + $edit['langcode[0][value]'] = 'en'; + $edit['lingotek_translation_profile'] = 'automatic'; + $this->drupalPostForm('/node/add/article', $edit, t('Save and Create New Draft')); + + // Moderate. + $edit = ['new_state' => 'published']; + $this->drupalPostForm(NULL, $edit, 'Apply'); + $this->assertText('The moderation state has been updated.'); + $this->assertNoText('Llamas are cool sent to Lingotek successfully.'); + } + + public function testModerationToUploadStateWithManualProfileDoesntTriggerUpload() { + $this->configureNeedsReviewAsUploadState(); + + $edit = []; + $edit['title[0][value]'] = 'Llamas are cool'; + $edit['body[0][value]'] = 'Llamas are very cool'; + $edit['langcode[0][value]'] = 'en'; + $edit['lingotek_translation_profile'] = 'manual'; + $this->drupalPostForm('/node/add/article', $edit, t('Save and Create New Draft')); + + // Moderate. + $edit = ['new_state' => 'needs_review']; + $this->drupalPostForm(NULL, $edit, 'Apply'); + $this->assertText('The moderation state has been updated.'); + $this->assertNoText('Llamas are cool sent to Lingotek successfully.'); + } + + public function testModerationToNonUploadStateWithManualProfileDoesntTriggerUpload() { + $this->configureNeedsReviewAsUploadState(); + + $edit = []; + $edit['title[0][value]'] = 'Llamas are cool'; + $edit['body[0][value]'] = 'Llamas are very cool'; + $edit['langcode[0][value]'] = 'en'; + $edit['lingotek_translation_profile'] = 'manual'; + $this->drupalPostForm('/node/add/article', $edit, t('Save and Create New Draft')); + + // Moderate. + $edit = ['new_state' => 'published']; + $this->drupalPostForm(NULL, $edit, 'Apply'); + $this->assertText('The moderation state has been updated.'); + $this->assertNoText('Llamas are cool sent to Lingotek successfully.'); + } + + public function testDownloadFromUploadStateTriggersATransition() { + $this->configureNeedsReviewAsUploadState(); + + $edit = []; + $edit['title[0][value]'] = 'Llamas are cool'; + $edit['body[0][value]'] = 'Llamas are very cool'; + $edit['langcode[0][value]'] = 'en'; + $edit['lingotek_translation_profile'] = 'automatic'; + $this->drupalPostForm('/node/add/article', $edit, t('Save and Create New Draft')); + + // The status is draft. + $value = $this->xpath('//div[@id="edit-current"]/text()'); + $value = trim((string)$value[1]); + $this->assertEqual($value, 'Draft', 'Workbench current status is draft'); + + // Moderate to Needs review, so it's uploaded. + $edit = ['new_state' => 'needs_review']; + $this->drupalPostForm(NULL, $edit, 'Apply'); + + // The status is needs review. + $value = $this->xpath('//div[@id="edit-current"]/text()'); + $value = trim((string)$value[1]); + $this->assertEqual($value, 'Needs Review', 'Workbench current status is Needs Review'); + + $this->goToContentBulkManagementForm(); + // Request translation. + $this->clickLink('ES'); + // Check translation. + $this->clickLink('ES'); + // Download translation. + $this->clickLink('ES'); + + // Let's see the current status is modified. + $this->clickLink('Llamas are cool'); + $this->assertNoFieldByName('new_state', 'The transition to a new workbench status happened (so no moderation form is shown).'); + } + + public function testDownloadFromNotUploadStateDoesntTriggerATransition() { + $this->configureNeedsReviewAsUploadState(); + + $edit = []; + $edit['title[0][value]'] = 'Llamas are cool'; + $edit['body[0][value]'] = 'Llamas are very cool'; + $edit['langcode[0][value]'] = 'en'; + $edit['lingotek_translation_profile'] = 'automatic'; + $this->drupalPostForm('/node/add/article', $edit, t('Save and Create New Draft')); + + // The status is draft. + $value = $this->xpath('//div[@id="edit-current"]/text()'); + $value = trim((string)$value[1]); + $this->assertEqual($value, 'Draft', 'Workbench current status is draft'); + + // Moderate to Needs review, so it's uploaded. + $edit = ['new_state' => 'needs_review']; + $this->drupalPostForm(NULL, $edit, 'Apply'); + + // Moderate back to draft, so the transition won't happen on download. + $edit = ['new_state' => 'draft']; + $this->drupalPostForm(NULL, $edit, 'Apply'); + + $this->goToContentBulkManagementForm(); + // Request translation. + $this->clickLink('ES'); + // Check translation. + $this->clickLink('ES'); + // Download translation. + $this->clickLink('ES'); + + // Let's see the current status is unmodified. + $this->clickLink('Llamas are cool'); + $value = $this->xpath('//div[@id="edit-current"]/text()'); + $value = trim((string)$value[1]); + $this->assertEqual($value, 'Draft', 'The transition to a new workbench status didn\'t happen because the source wasn\'t the expected.'); + } + + /** + * Enable moderation for a specified content type, using the UI. + * + * @param string $content_type_id + * Machine name. + * @param string[] $allowed_states + * Array of allowed state IDs. + * @param string $default_state + * Default state. + */ + protected function enableModerationThroughUI($content_type_id, array $allowed_states, $default_state) { + $this->drupalGet('admin/structure/types/manage/' . $content_type_id . '/moderation'); + $this->assertFieldByName('enable_moderation_state'); + $this->assertNoFieldChecked('edit-enable-moderation-state'); + + $edit['enable_moderation_state'] = 1; + + /** @var ModerationState $state */ + foreach (ModerationState::loadMultiple() as $id => $state) { + $key = $state->isPublishedState() ? 'allowed_moderation_states_published[' . $state->id() . ']' : 'allowed_moderation_states_unpublished[' . $state->id() . ']'; + $edit[$key] = (int)in_array($id, $allowed_states); + } + + $edit['default_moderation_state'] = $default_state; + + $this->drupalPostForm(NULL, $edit, t('Save')); + } + +} diff --git a/tests/src/Unit/Moderation/LingotekModerationFactoryTest.php b/tests/src/Unit/Moderation/LingotekModerationFactoryTest.php new file mode 100644 index 0000000..753f8f3 --- /dev/null +++ b/tests/src/Unit/Moderation/LingotekModerationFactoryTest.php @@ -0,0 +1,157 @@ +factory = new LingotekModerationFactory(); + } + + /** + * @covers ::addModerationConfiguration + * @covers ::getModerationConfigurationService + */ + public function testAddModerationConfiguration() { + $configServiceLast = $this->getMock(LingotekModerationConfigurationServiceInterface::class); + $configServiceLast->expects($this->any()) + ->method('applies') + ->willReturn(TRUE); + $configServiceFirst = $this->getMock(LingotekModerationConfigurationServiceInterface::class); + $configServiceFirst->expects($this->any()) + ->method('applies') + ->willReturn(TRUE); + + $this->factory->addModerationConfiguration($configServiceLast, 'last', 10); + $this->factory->addModerationConfiguration($configServiceFirst, 'first', 100); + + $configService = $this->factory->getModerationConfigurationService(); + $this->assertEquals($configService, $configServiceFirst, 'Priority is respected if all services apply.'); + } + + /** + * @covers ::addModerationConfiguration + * @covers ::getModerationConfigurationService + */ + public function testAddModerationConfigurationWithANonApplyingService() { + $configServiceLast = $this->getMock(LingotekModerationConfigurationServiceInterface::class); + $configServiceLast->expects($this->any()) + ->method('applies') + ->willReturn(TRUE); + $configServiceFirst = $this->getMock(LingotekModerationConfigurationServiceInterface::class); + $configServiceFirst->expects($this->any()) + ->method('applies') + ->willReturn(FALSE); + + $this->factory->addModerationConfiguration($configServiceLast, 'last', 10); + $this->factory->addModerationConfiguration($configServiceFirst, 'first', 100); + + $configService = $this->factory->getModerationConfigurationService(); + $this->assertEquals($configService, $configServiceLast, 'Priority is respected, but we return a services that applies.'); + } + + /** + * @covers ::addModerationForm + * @covers ::getModerationSettingsForm + */ + public function testAddModerationSettingsForm() { + $configServiceLast = $this->getMock(LingotekModerationSettingsFormInterface::class); + $configServiceLast->expects($this->any()) + ->method('applies') + ->willReturn(TRUE); + $configServiceFirst = $this->getMock(LingotekModerationSettingsFormInterface::class); + $configServiceFirst->expects($this->any()) + ->method('applies') + ->willReturn(TRUE); + + $this->factory->addModerationForm($configServiceLast, 'last', 10); + $this->factory->addModerationForm($configServiceFirst, 'first', 100); + + $configService = $this->factory->getModerationSettingsForm(); + $this->assertEquals($configService, $configServiceFirst, 'Priority is respected if all services apply.'); + } + + /** + * @covers ::addModerationForm + * @covers ::getModerationSettingsForm + */ + public function testAddModerationSettingsFormWithANonApplyingService() { + $configServiceLast = $this->getMock(LingotekModerationSettingsFormInterface::class); + $configServiceLast->expects($this->any()) + ->method('applies') + ->willReturn(TRUE); + $configServiceFirst = $this->getMock(LingotekModerationSettingsFormInterface::class); + $configServiceFirst->expects($this->any()) + ->method('applies') + ->willReturn(FALSE); + + $this->factory->addModerationForm($configServiceLast, 'last', 10); + $this->factory->addModerationForm($configServiceFirst, 'first', 100); + + $configService = $this->factory->getModerationSettingsForm(); + $this->assertEquals($configService, $configServiceLast, 'Priority is respected, but we return a services that applies.'); + } + + /** + * @covers ::addModerationHandler + * @covers ::getModerationHandler + */ + public function testAddModerationHandler() { + $configServiceLast = $this->getMock(LingotekModerationHandlerInterface::class); + $configServiceLast->expects($this->any()) + ->method('applies') + ->willReturn(TRUE); + $configServiceFirst = $this->getMock(LingotekModerationHandlerInterface::class); + $configServiceFirst->expects($this->any()) + ->method('applies') + ->willReturn(TRUE); + + $this->factory->addModerationHandler($configServiceLast, 'last', 10); + $this->factory->addModerationHandler($configServiceFirst, 'first', 100); + + $configService = $this->factory->getModerationHandler(); + $this->assertEquals($configService, $configServiceFirst, 'Priority is respected if all services apply.'); + } + + /** + * @covers ::addModerationHandler + * @covers ::getModerationHandler + */ + public function testAddModerationHandlerWithANonApplyingService() { + $configServiceLast = $this->getMock(LingotekModerationHandlerInterface::class); + $configServiceLast->expects($this->any()) + ->method('applies') + ->willReturn(TRUE); + $configServiceFirst = $this->getMock(LingotekModerationHandlerInterface::class); + $configServiceFirst->expects($this->any()) + ->method('applies') + ->willReturn(FALSE); + + $this->factory->addModerationHandler($configServiceLast, 'last', 10); + $this->factory->addModerationHandler($configServiceFirst, 'first', 100); + + $configService = $this->factory->getModerationHandler(); + $this->assertEquals($configService, $configServiceLast, 'Priority is respected, but we return a services that applies.'); + } + +}