diff --git a/actions/class.Creator.php b/actions/class.Creator.php index 5e8d9b359..6d4da7a3e 100755 --- a/actions/class.Creator.php +++ b/actions/class.Creator.php @@ -1,22 +1,22 @@ getRequestParameter('uri'); $testModel = $this->getServiceManager()->get(TestModelService::SERVICE_ID); + // Add support for translation and side-by-side view + $this->setData('translation', $this->getRequestParameter('translation') ?? "false"); + $this->setData('originResourceUri', json_encode($this->getRequestParameter('originResourceUri'))); + $items = $testModel->getItems(new core_kernel_classes_Resource(tao_helpers_Uri::decode($testUri))); foreach ($items as $item) { $labels[$item->getUri()] = $item->getLabel(); @@ -91,9 +95,9 @@ public function index() $this->setView('creator.tpl'); } - /** - * Get json's test content, the uri of the test must be provided in parameter - */ + /** + * Get json's test content, the uri of the test must be provided in parameter + */ public function getTest() { $test = $this->getCurrentTest(); @@ -104,11 +108,11 @@ public function getTest() $this->response = $this->getPsrResponse()->withBody(stream_for($qtiTestService->getJsonTest($test))); } - /** - * Save a test, test uri and - * The request must use the POST method and contains - * the test uri and a json string that represents the QTI Test in the model parameter. - */ + /** + * Save a test, test uri and + * The request must use the POST method and contains + * the test uri and a json string that represents the QTI Test in the model parameter. + */ public function saveTest() { $saved = false; diff --git a/composer.json b/composer.json index ffdad3dfc..7ac770aa0 100644 --- a/composer.json +++ b/composer.json @@ -64,10 +64,10 @@ "oat-sa/oatbox-extension-installer": "~1.1||dev-master", "qtism/qtism": ">=0.28.3", "oat-sa/generis": ">=15.36.4", - "oat-sa/tao-core": ">=54.21.0", - "oat-sa/extension-tao-item" : ">=12.1.0", - "oat-sa/extension-tao-itemqti" : ">=30.12.0", - "oat-sa/extension-tao-test" : ">=16.0.0", + "oat-sa/tao-core": "dev-feat/HKD-6/integration as 99", + "oat-sa/extension-tao-item" : "dev-feat/HKD-6/integration as 99", + "oat-sa/extension-tao-itemqti" : "dev-feat/HKD-6/integration as 99", + "oat-sa/extension-tao-test" : "dev-feat/HKD-6/integration as 99", "oat-sa/extension-tao-delivery" : ">=15.0.0", "oat-sa/extension-tao-outcome" : ">=13.0.0", "league/flysystem": "~1.0", diff --git a/manifest.php b/manifest.php index d43052c4d..9689ac325 100755 --- a/manifest.php +++ b/manifest.php @@ -23,14 +23,13 @@ use oat\tao\model\user\TaoRoles; use oat\taoQtiTest\model\Container\TestQtiServiceProvider; use oat\taoQtiTest\models\classes\metadata\MetadataServiceProvider; -// phpcs:disable Generic.Files.LineLength +// phpcs:ignore Generic.Files.LineLength use oat\taoQtiTest\models\classes\render\CustomInteraction\ServiceProvider\CustomInteractionPostProcessingServiceProvider; -// phpcs:enable Generic.Files.LineLength use oat\taoQtiTest\models\render\ItemsReferencesServiceProvider; use oat\taoQtiTest\models\TestSessionState\Container\TestSessionStateServiceProvider; +use oat\taoQtiTest\models\Translation\ServiceProvider\TranslationServiceProvider; +use oat\taoQtiTest\models\UniqueId\ServiceProvider\UniqueIdServiceProvider; use oat\taoQtiTest\models\xmlEditor\XmlEditorInterface; -use oat\taoQtiTest\scripts\install\RegisterResultTransmissionEventHandlers; -use oat\taoQtiTest\scripts\install\SetupProvider; use oat\taoQtiTest\scripts\install\CreateTestSessionFilesystem; use oat\taoQtiTest\scripts\install\DisableBRSinTestAuthoring; use oat\taoQtiTest\scripts\install\RegisterCreatorServices; @@ -38,6 +37,7 @@ use oat\taoQtiTest\scripts\install\RegisterQtiCategoryPresetProviders; use oat\taoQtiTest\scripts\install\RegisterQtiFlysystemManager; use oat\taoQtiTest\scripts\install\RegisterQtiPackageExporter; +use oat\taoQtiTest\scripts\install\RegisterResultTransmissionEventHandlers; use oat\taoQtiTest\scripts\install\RegisterSectionPauseService; use oat\taoQtiTest\scripts\install\RegisterTestCategoryPresetProviderService; use oat\taoQtiTest\scripts\install\RegisterTestContainer; @@ -50,6 +50,7 @@ use oat\taoQtiTest\scripts\install\SetSynchronisationService; use oat\taoQtiTest\scripts\install\SetupDefaultTemplateConfiguration; use oat\taoQtiTest\scripts\install\SetupEventListeners; +use oat\taoQtiTest\scripts\install\SetupProvider; use oat\taoQtiTest\scripts\install\SetUpQueueTasks; use oat\taoQtiTest\scripts\install\SetupStateOffloadQueue; use oat\taoQtiTest\scripts\install\SyncChannelInstaller; @@ -185,6 +186,8 @@ ItemsReferencesServiceProvider::class, TestQtiServiceProvider::class, TestSessionStateServiceProvider::class, - MetadataServiceProvider::class + MetadataServiceProvider::class, + TranslationServiceProvider::class, + UniqueIdServiceProvider::class, ], ]; diff --git a/migrations/Version202409111328132260_taoQtiTest.php b/migrations/Version202409111328132260_taoQtiTest.php new file mode 100644 index 000000000..e27392816 --- /dev/null +++ b/migrations/Version202409111328132260_taoQtiTest.php @@ -0,0 +1,46 @@ +getServiceManager()->get(EventManager::SERVICE_ID); + $eventManager->attach( + TestCreatedEvent::class, + [TestCreatedEventListener::class, 'populateUniqueId'] + ); + $this->getServiceManager()->register(EventManager::SERVICE_ID, $eventManager); + } + + public function down(Schema $schema): void + { + /** @var EventManager $eventManager */ + $eventManager = $this->getServiceManager()->get(EventManager::SERVICE_ID); + $eventManager->detach( + TestCreatedEvent::class, + [TestCreatedEventListener::class, 'populateUniqueId'] + ); + $this->getServiceManager()->register(EventManager::SERVICE_ID, $eventManager); + } +} diff --git a/models/classes/Translation/Service/TestTranslator.php b/models/classes/Translation/Service/TestTranslator.php new file mode 100644 index 000000000..cf0bd8394 --- /dev/null +++ b/models/classes/Translation/Service/TestTranslator.php @@ -0,0 +1,160 @@ +testQtiService = $testQtiService; + $this->ontology = $ontology; + $this->resourceTranslationRepository = $resourceTranslationRepository; + } + + /** + * @throws taoQtiTest_models_classes_QtiTestConverterException + * @throws taoQtiTest_models_classes_QtiTestServiceException + * @throws core_kernel_persistence_Exception + * @throws ResourceTranslationException + */ + public function translate(core_kernel_classes_Resource $translationTest): core_kernel_classes_Resource + { + $this->assertIsTest($translationTest); + + $originalTestUri = $translationTest->getOnePropertyValue( + $this->ontology->getProperty(TaoOntology::PROPERTY_TRANSLATION_ORIGINAL_RESOURCE_URI) + ); + $originalTest = $this->ontology->getResource($originalTestUri->getUri()); + + $jsonTest = $this->testQtiService->getJsonTest($originalTest); + $originalTestData = json_decode($jsonTest, true, 512, JSON_THROW_ON_ERROR); + + $originalItemUris = $this->collectItemUris($originalTestData); + $translationUris = $this->getItemTranslationUris($translationTest, $originalItemUris); + + $translatedTestData = $this->doTranslation($originalTestData, $translationUris); + + $this->testQtiService->saveJsonTest($translationTest, json_encode($translatedTestData)); + $this->updateTranslationCompletionStatus($translationTest, $originalItemUris, $translationUris); + + return $translationTest; + } + + private function assertIsTest(core_kernel_classes_Resource $resource): void + { + if (!$resource->isInstanceOf($this->ontology->getClass(TaoOntology::CLASS_URI_TEST))) { + throw new ResourceTranslationException('Provided resources is not a Test'); + } + } + + private function doTranslation(array $testData, array $translationUris): array + { + foreach ($testData['testParts'] as &$testPart) { + foreach ($testPart['assessmentSections'] as &$assessmentSection) { + foreach ($assessmentSection['sectionParts'] as &$sectionPart) { + $sectionPart['href'] = $translationUris[$sectionPart['href']] ?? $sectionPart['href']; + } + } + } + + return $testData; + } + + private function collectItemUris(array $testData): array + { + $uris = []; + + foreach ($testData['testParts'] as $testPart) { + foreach ($testPart['assessmentSections'] as $assessmentSection) { + foreach ($assessmentSection['sectionParts'] as $sectionPart) { + if (in_array($sectionPart['href'], $uris, true)) { + continue; + } + + $uris[] = $sectionPart['href']; + } + } + } + + return $uris; + } + + /** + * @param string[] $originalItemUris + * @return array + */ + private function getItemTranslationUris(core_kernel_classes_Resource $test, array $originalItemUris): array + { + $language = $test->getOnePropertyValue($this->ontology->getProperty(TaoOntology::PROPERTY_LANGUAGE)); + $translations = $this->resourceTranslationRepository->find( + new ResourceTranslationQuery( + $originalItemUris, + $language->getUri() + ) + ); + + $translationUris = []; + + /** @var ResourceTranslation $translation */ + foreach ($translations->jsonSerialize()['resources'] as $translation) { + $translationUris[$translation->getOriginResourceUri()] = $translation->getResourceUri(); + } + + return $translationUris; + } + + private function updateTranslationCompletionStatus( + core_kernel_classes_Resource $test, + array $uniqueIds, + array $translationUris + ): void { + $status = count($uniqueIds) > count($translationUris) + ? TaoTestOntology::PROPERTY_VALUE_TRANSLATION_COMPLETION_MISSING_TRANSLATIONS + : TaoTestOntology::PROPERTY_VALUE_TRANSLATION_COMPLETION_TRANSLATED; + + $test->editPropertyValues( + $this->ontology->getProperty(TaoTestOntology::PROPERTY_TRANSLATION_COMPLETION), + $status + ); + } +} diff --git a/models/classes/Translation/Service/TranslationPostCreationService.php b/models/classes/Translation/Service/TranslationPostCreationService.php new file mode 100644 index 000000000..122f6f08e --- /dev/null +++ b/models/classes/Translation/Service/TranslationPostCreationService.php @@ -0,0 +1,59 @@ +testTranslator = $testTranslator; + $this->logger = $logger; + } + + public function __invoke(core_kernel_classes_Resource $test): core_kernel_classes_Resource + { + try { + return $this->testTranslator->translate($test); + } catch (Throwable $exception) { + $message = sprintf('An error occurred while trying to translate the test %s.', $test->getUri()); + + $this->logger->error( + sprintf( + '%s. Error: (%s) %s', + $message, + get_class($exception), + $exception->getMessage()) + ); + + throw new ResourceTranslationException($message); + } + } +} diff --git a/models/classes/Translation/Service/TranslationSyncService.php b/models/classes/Translation/Service/TranslationSyncService.php new file mode 100644 index 000000000..bd5e9543c --- /dev/null +++ b/models/classes/Translation/Service/TranslationSyncService.php @@ -0,0 +1,62 @@ +testTranslator = $testTranslator; + $this->logger = $logger; + } + + public function __invoke(core_kernel_classes_Resource $test): core_kernel_classes_Resource + { + try { + return $this->testTranslator->translate($test); + } catch (Throwable $exception) { + $message = sprintf( + 'An error occurred while trying to synchronize the translation for test %s.', + $test->getUri() + ); + + $this->logger->error( + sprintf( + '%s. Error: (%s) %s', + $message, + get_class($exception), + $exception->getMessage()) + ); + + throw new ResourceTranslationException($message); + } + } +} diff --git a/models/classes/Translation/ServiceProvider/TranslationServiceProvider.php b/models/classes/Translation/ServiceProvider/TranslationServiceProvider.php new file mode 100644 index 000000000..254e2565c --- /dev/null +++ b/models/classes/Translation/ServiceProvider/TranslationServiceProvider.php @@ -0,0 +1,89 @@ +services(); + + $services + ->set(TestTranslator::class, TestTranslator::class) + ->args([ + service(taoQtiTest_models_classes_QtiTestService::class), + service(Ontology::SERVICE_ID), + service(ResourceTranslationRepository::class), + service(LoggerService::SERVICE_ID), + ]); + + $services + ->set(TranslationSyncService::class, TranslationSyncService::class) + ->args([ + service(TestTranslator::class), + service(LoggerService::SERVICE_ID), + ]); + + $services + ->get(TaoTranslationSyncService::class) + ->call( + 'addSynchronizer', + [ + TaoOntology::CLASS_URI_TEST, + service(TranslationSyncService::class), + ] + ); + + $services + ->set(TranslationPostCreationService::class, TranslationPostCreationService::class) + ->args([ + service(TestTranslator::class), + service(LoggerService::SERVICE_ID), + ]); + + $services + ->get(TranslationCreationService::class) + ->call( + 'addPostCreation', + [ + TaoOntology::CLASS_URI_TEST, + service(TranslationPostCreationService::class) + ] + ); + } +} diff --git a/models/classes/UniqueId/Form/Modifier/UniqueIdFormModifier.php b/models/classes/UniqueId/Form/Modifier/UniqueIdFormModifier.php new file mode 100644 index 000000000..3a8e6997c --- /dev/null +++ b/models/classes/UniqueId/Form/Modifier/UniqueIdFormModifier.php @@ -0,0 +1,69 @@ +ontology = $ontology; + $this->featureFlagChecker = $featureFlagChecker; + $this->qtiIdentifierRetriever = $qtiIdentifierRetriever; + } + + public function modify(tao_helpers_form_Form $form, array $options = []): void + { + if (!$this->featureFlagChecker->isEnabled('FEATURE_FLAG_UNIQUE_NUMERIC_QTI_IDENTIFIER')) { + return; + } + + $encodedProperty = tao_helpers_Uri::encode(TaoOntology::PROPERTY_UNIQUE_IDENTIFIER); + $uniqueIdValue = $form->getValue($encodedProperty); + + if (!empty($uniqueIdValue)) { + return; + } + + $instance = $this->ontology->getResource($form->getValue('uri')); + $identifier = $this->qtiIdentifierRetriever->retrieve($instance); + + if ($identifier) { + $form->setValue($encodedProperty, $identifier); + } + } +} diff --git a/models/classes/UniqueId/Listener/TestCreatedEventListener.php b/models/classes/UniqueId/Listener/TestCreatedEventListener.php new file mode 100644 index 000000000..68d54827f --- /dev/null +++ b/models/classes/UniqueId/Listener/TestCreatedEventListener.php @@ -0,0 +1,75 @@ +featureFlagChecker = $featureFlagChecker; + $this->ontology = $ontology; + $this->qtiIdentifierRetriever = $qtiIdentifierRetriever; + $this->logger = $logger; + } + + public function populateUniqueId(TestCreatedEvent $event): void + { + if (!$this->featureFlagChecker->isEnabled('FEATURE_FLAG_UNIQUE_NUMERIC_QTI_IDENTIFIER')) { + return; + } + + $test = $this->ontology->getResource($event->getTestUri()); + $uniqueIdProperty = $this->ontology->getProperty(TaoOntology::PROPERTY_UNIQUE_IDENTIFIER); + + if (!empty((string) $test->getOnePropertyValue($uniqueIdProperty))) { + $this->logger->info( + sprintf( + 'The property "%s" for the test "%s" has already been set.', + $uniqueIdProperty->getUri(), + $test->getUri() + ) + ); + + return; + } + + $identifier = $this->qtiIdentifierRetriever->retrieve($test); + $test->editPropertyValues($uniqueIdProperty, $identifier); + } +} diff --git a/models/classes/UniqueId/Service/QtiIdentifierRetriever.php b/models/classes/UniqueId/Service/QtiIdentifierRetriever.php new file mode 100644 index 000000000..f67a3e71e --- /dev/null +++ b/models/classes/UniqueId/Service/QtiIdentifierRetriever.php @@ -0,0 +1,54 @@ +qtiTestService = $qtiTestService; + $this->logger = $logger; + } + + public function retrieve(core_kernel_classes_Resource $test): ?string + { + try { + $jsonTest = $this->qtiTestService->getJsonTest($test); + $decodedTest = json_decode($jsonTest, true, 512, JSON_THROW_ON_ERROR); + + return $decodedTest['identifier'] ?? null; + } catch (Throwable $exception) { + $this->logger->error('An error occurred while retrieving test data: ' . $exception->getMessage()); + + throw $exception; + } + } +} diff --git a/models/classes/UniqueId/ServiceProvider/UniqueIdServiceProvider.php b/models/classes/UniqueId/ServiceProvider/UniqueIdServiceProvider.php new file mode 100644 index 000000000..578345ffe --- /dev/null +++ b/models/classes/UniqueId/ServiceProvider/UniqueIdServiceProvider.php @@ -0,0 +1,78 @@ +services(); + + $services + ->set(QtiIdentifierRetriever::class, QtiIdentifierRetriever::class) + ->args([ + service(taoQtiTest_models_classes_QtiTestService::class), + service(LoggerService::SERVICE_ID), + ]); + + $services + ->set(UniqueIdFormModifier::class, UniqueIdFormModifier::class) + ->args([ + service(Ontology::SERVICE_ID), + service(QtiIdentifierRetriever::class), + service(FeatureFlagChecker::class), + ]); + + $services + ->get(FormModifierProxy::class) + ->call( + 'addModifier', + [ + service(UniqueIdFormModifier::class), + ] + ); + + $services + ->set(TestCreatedEventListener::class, TestCreatedEventListener::class) + ->public() + ->args([ + service(FeatureFlagChecker::class), + service(Ontology::SERVICE_ID), + service(QtiIdentifierRetriever::class), + service(LoggerService::SERVICE_ID), + ]); + } +} diff --git a/scripts/install/SetupEventListeners.php b/scripts/install/SetupEventListeners.php index 7b0b3eda1..ddfedc814 100644 --- a/scripts/install/SetupEventListeners.php +++ b/scripts/install/SetupEventListeners.php @@ -25,6 +25,8 @@ use oat\taoQtiTest\models\event\AfterAssessmentTestSessionClosedEvent; use oat\taoQtiTest\models\event\QtiTestStateChangeEvent; use oat\taoQtiTest\models\QtiTestListenerService; +use oat\taoQtiTest\models\UniqueId\Listener\TestCreatedEventListener; +use oat\taoTests\models\event\TestCreatedEvent; /** * Register a listener for state changes @@ -48,5 +50,9 @@ public function __invoke($params) AfterAssessmentTestSessionClosedEvent::class, [QtiTestListenerService::SERVICE_ID, 'archiveState'] ); + $this->registerEvent( + TestCreatedEvent::class, + [TestCreatedEventListener::class, 'populateUniqueId'] + ); } } diff --git a/test/unit/models/classes/Translation/Service/TestTranslatorTest.php b/test/unit/models/classes/Translation/Service/TestTranslatorTest.php new file mode 100644 index 000000000..61e3f626a --- /dev/null +++ b/test/unit/models/classes/Translation/Service/TestTranslatorTest.php @@ -0,0 +1,172 @@ +translationTest = $this->createMock(core_kernel_classes_Resource::class); + + $this->testQtiService = $this->createMock(taoQtiTest_models_classes_QtiTestService::class); + $this->ontology = $this->createMock(Ontology::class); + $this->resourceTranslationRepository = $this->createMock(ResourceTranslationRepository::class); + + $this->sut = new TestTranslator($this->testQtiService, $this->ontology, $this->resourceTranslationRepository); + } + + public function testTranslate(): void + { + $rootClass = $this->createMock(core_kernel_classes_Class::class); + + $this->ontology + ->expects($this->once()) + ->method('getClass') + ->with(TaoOntology::CLASS_URI_TEST) + ->willReturn($rootClass); + + $this->translationTest + ->expects($this->once()) + ->method('isInstanceOf') + ->with($rootClass) + ->willReturn(true); + + $translationOriginalResourceUriProperty = $this->createMock(core_kernel_classes_Property::class); + $languageProperty = $this->createMock(core_kernel_classes_Property::class); + $translationCompletionProperty = $this->createMock(core_kernel_classes_Property::class); + + $this->ontology + ->expects($this->exactly(3)) + ->method('getProperty') + ->withConsecutive( + [TaoOntology::PROPERTY_TRANSLATION_ORIGINAL_RESOURCE_URI], + [TaoOntology::PROPERTY_LANGUAGE], + [TaoTestOntology::PROPERTY_TRANSLATION_COMPLETION], + ) + ->willReturnOnConsecutiveCalls( + $translationOriginalResourceUriProperty, + $languageProperty, + $translationCompletionProperty + ); + + $originalTestUri = $this->createMock(core_kernel_classes_Resource::class); + $originalTestUri + ->expects($this->once()) + ->method('getUri') + ->willReturn('originalTestUri'); + + $translationLanguage = $this->createMock(core_kernel_classes_Resource::class); + $translationLanguage + ->expects($this->once()) + ->method('getUri') + ->willReturn('translationLanguageUri'); + + $this->translationTest + ->expects($this->exactly(2)) + ->method('getOnePropertyValue') + ->withConsecutive( + [$translationOriginalResourceUriProperty], + [$languageProperty] + ) + ->willReturnOnConsecutiveCalls($originalTestUri, $translationLanguage); + + $originalTest = $this->createMock(core_kernel_classes_Resource::class); + + $this->ontology + ->expects($this->once()) + ->method('getResource') + ->with('originalTestUri') + ->willReturn($originalTest); + + $this->testQtiService + ->expects($this->once()) + ->method('getJsonTest') + ->with($originalTest) + ->willReturn('{"testParts":[{"assessmentSections":[{"sectionParts":[{"href":"originalItemUri"}]}]}]}'); + + $translationResource = $this->createMock(ResourceTranslation::class); + + $this->resourceTranslationRepository + ->expects($this->once()) + ->method('find') + ->with(new ResourceTranslationQuery(['originalItemUri'], 'translationLanguageUri')) + ->willReturn(new ResourceCollection($translationResource)); + + $translationResource + ->expects($this->once()) + ->method('getOriginResourceUri') + ->willReturn('originalItemUri'); + + $translationResource + ->expects($this->once()) + ->method('getResourceUri') + ->willReturn('translationItemUri'); + + $this->testQtiService + ->expects($this->once()) + ->method('saveJsonTest') + ->with( + $this->translationTest, + '{"testParts":[{"assessmentSections":[{"sectionParts":[{"href":"translationItemUri"}]}]}]}' + ); + + $this->translationTest + ->expects($this->once()) + ->method('editPropertyValues') + ->with( + $translationCompletionProperty, + TaoTestOntology::PROPERTY_VALUE_TRANSLATION_COMPLETION_TRANSLATED + ); + + $this->assertEquals($this->translationTest, $this->sut->translate($this->translationTest)); + } +} diff --git a/test/unit/models/classes/Translation/Service/TranslationPostCreationServiceTest.php b/test/unit/models/classes/Translation/Service/TranslationPostCreationServiceTest.php new file mode 100644 index 000000000..ff5edf359 --- /dev/null +++ b/test/unit/models/classes/Translation/Service/TranslationPostCreationServiceTest.php @@ -0,0 +1,94 @@ +resource = $this->createMock(core_kernel_classes_Resource::class); + + $this->testTranslator = $this->createMock(TestTranslator::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->sut = new TranslationPostCreationService($this->testTranslator, $this->logger); + } + + public function testService(): void + { + $this->testTranslator + ->expects($this->once()) + ->method('translate') + ->with($this->resource) + ->willReturn($this->resource); + + $this->logger + ->expects($this->never()) + ->method('error'); + + $this->assertEquals($this->resource, $this->sut->__invoke($this->resource)); + } + + public function testServiceException(): void + { + $this->testTranslator + ->expects($this->once()) + ->method('translate') + ->with($this->resource) + ->willThrowException($this->createMock(Throwable::class)); + + $this->logger + ->expects($this->once()) + ->method('error'); + + $this->resource + ->expects($this->once()) + ->method('getUri') + ->willReturn('resourceUri'); + + $this->expectException(ResourceTranslationException::class); + $this->expectExceptionMessage('An error occurred while trying to translate the test resourceUri.'); + + $this->sut->__invoke($this->resource); + } +} diff --git a/test/unit/models/classes/Translation/Service/TranslationSyncServiceTest.php b/test/unit/models/classes/Translation/Service/TranslationSyncServiceTest.php new file mode 100644 index 000000000..591880297 --- /dev/null +++ b/test/unit/models/classes/Translation/Service/TranslationSyncServiceTest.php @@ -0,0 +1,95 @@ +resource = $this->createMock(core_kernel_classes_Resource::class); + + $this->testTranslator = $this->createMock(TestTranslator::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->sut = new TranslationSyncService($this->testTranslator, $this->logger); + } + + public function testService(): void + { + $this->testTranslator + ->expects($this->once()) + ->method('translate') + ->with($this->resource) + ->willReturn($this->resource); + + $this->logger + ->expects($this->never()) + ->method('error'); + + $this->assertEquals($this->resource, $this->sut->__invoke($this->resource)); + } + + public function testServiceException(): void + { + $this->testTranslator + ->expects($this->once()) + ->method('translate') + ->with($this->resource) + ->willThrowException($this->createMock(Throwable::class)); + + $this->logger + ->expects($this->once()) + ->method('error'); + + $this->resource + ->expects($this->once()) + ->method('getUri') + ->willReturn('resourceUri'); + + $this->expectException(ResourceTranslationException::class); + $this->expectExceptionMessage( + 'An error occurred while trying to synchronize the translation for test resourceUri.' + ); + + $this->sut->__invoke($this->resource); + } +} diff --git a/test/unit/models/classes/UniqueId/Form/Modifier/UniqueIdFormModifierTest.php b/test/unit/models/classes/UniqueId/Form/Modifier/UniqueIdFormModifierTest.php new file mode 100644 index 000000000..7a094ddff --- /dev/null +++ b/test/unit/models/classes/UniqueId/Form/Modifier/UniqueIdFormModifierTest.php @@ -0,0 +1,202 @@ +form = $this->createMock(tao_helpers_form_Form::class); + $this->encodedProperty = tao_helpers_Uri::encode(TaoOntology::PROPERTY_UNIQUE_IDENTIFIER); + + $this->ontology = $this->createMock(Ontology::class); + $this->qtiIdentifierRetriever = $this->createMock(QtiIdentifierRetriever::class); + $this->featureFlagChecker = $this->createMock(FeatureFlagCheckerInterface::class); + + $this->sut = new UniqueIdFormModifier( + $this->ontology, + $this->qtiIdentifierRetriever, + $this->featureFlagChecker + ); + } + + public function testModifyFeatureDisabled(): void + { + $this->featureFlagChecker + ->expects($this->once()) + ->method('isEnabled') + ->with('FEATURE_FLAG_UNIQUE_NUMERIC_QTI_IDENTIFIER') + ->willReturn(false); + + $this->form + ->expects($this->never()) + ->method($this->anything()); + + $this->ontology + ->expects($this->never()) + ->method($this->anything()); + + $this->qtiIdentifierRetriever + ->expects($this->never()) + ->method($this->anything()); + + $this->form + ->expects($this->never()) + ->method('setValue'); + + $this->sut->modify($this->form); + } + + public function testModifyFeatureEnabledButValueSet(): void + { + $this->featureFlagChecker + ->expects($this->once()) + ->method('isEnabled') + ->with('FEATURE_FLAG_UNIQUE_NUMERIC_QTI_IDENTIFIER') + ->willReturn(true); + + $this->form + ->expects($this->once()) + ->method('getValue') + ->with($this->encodedProperty) + ->willReturn('value'); + + $this->ontology + ->expects($this->never()) + ->method($this->anything()); + + $this->qtiIdentifierRetriever + ->expects($this->never()) + ->method($this->anything()); + + $this->form + ->expects($this->never()) + ->method('setValue'); + + $this->sut->modify($this->form); + } + + public function testModifyFeatureEnabledButNoIdentifier(): void + { + $this->featureFlagChecker + ->expects($this->once()) + ->method('isEnabled') + ->with('FEATURE_FLAG_UNIQUE_NUMERIC_QTI_IDENTIFIER') + ->willReturn(true); + + $this->form + ->expects($this->exactly(2)) + ->method('getValue') + ->withConsecutive( + [$this->encodedProperty], + ['uri'] + ) + ->willReturnOnConsecutiveCalls(null, 'instanceUri'); + + $instance = $this->createMock(core_kernel_classes_Resource::class); + + $this->ontology + ->expects($this->once()) + ->method('getResource') + ->with('instanceUri') + ->willReturn($instance); + + $this->qtiIdentifierRetriever + ->expects($this->once()) + ->method('retrieve') + ->with($instance) + ->willReturn(null); + + $this->form + ->expects($this->never()) + ->method('setValue'); + + $this->sut->modify($this->form); + } + + public function testModify(): void + { + $this->featureFlagChecker + ->expects($this->once()) + ->method('isEnabled') + ->with('FEATURE_FLAG_UNIQUE_NUMERIC_QTI_IDENTIFIER') + ->willReturn(true); + + $this->form + ->expects($this->exactly(2)) + ->method('getValue') + ->withConsecutive( + [$this->encodedProperty], + ['uri'] + ) + ->willReturnOnConsecutiveCalls(null, 'instanceUri'); + + $instance = $this->createMock(core_kernel_classes_Resource::class); + + $this->ontology + ->expects($this->once()) + ->method('getResource') + ->with('instanceUri') + ->willReturn($instance); + + $this->qtiIdentifierRetriever + ->expects($this->once()) + ->method('retrieve') + ->with($instance) + ->willReturn('qtiIdentifier'); + + $this->form + ->expects($this->once()) + ->method('setValue') + ->with($this->encodedProperty, 'qtiIdentifier'); + + $this->sut->modify($this->form); + } +} diff --git a/test/unit/models/classes/UniqueId/Listener/TestCreatedEventListenerTest.php b/test/unit/models/classes/UniqueId/Listener/TestCreatedEventListenerTest.php new file mode 100644 index 000000000..3fca6232d --- /dev/null +++ b/test/unit/models/classes/UniqueId/Listener/TestCreatedEventListenerTest.php @@ -0,0 +1,202 @@ +testCreatedEvent = $this->createMock(TestCreatedEvent::class); + $this->test = $this->createMock(core_kernel_classes_Resource::class); + $this->property = $this->createMock(core_kernel_classes_Property::class); + + $this->featureFlagChecker = $this->createMock(FeatureFlagCheckerInterface::class); + $this->ontology = $this->createMock(Ontology::class); + $this->qtiIdentifierRetriever = $this->createMock(QtiIdentifierRetriever::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->sut = new TestCreatedEventListener( + $this->featureFlagChecker, + $this->ontology, + $this->qtiIdentifierRetriever, + $this->logger + ); + } + + public function testPopulateUniqueIdFeatureDisabled(): void + { + $this->featureFlagChecker + ->expects($this->once()) + ->method('isEnabled') + ->with('FEATURE_FLAG_UNIQUE_NUMERIC_QTI_IDENTIFIER') + ->willReturn(false); + + $this->ontology + ->expects($this->never()) + ->method($this->anything()); + $this->logger + ->expects($this->never()) + ->method($this->anything()); + $this->testCreatedEvent + ->expects($this->never()) + ->method($this->anything()); + $this->test + ->expects($this->never()) + ->method($this->anything()); + $this->qtiIdentifierRetriever + ->expects($this->never()) + ->method($this->anything()); + + $this->sut->populateUniqueId($this->testCreatedEvent); + } + + public function testPopulateUniqueId(): void + { + $this->featureFlagChecker + ->expects($this->once()) + ->method('isEnabled') + ->with('FEATURE_FLAG_UNIQUE_NUMERIC_QTI_IDENTIFIER') + ->willReturn(true); + + $this->ontology + ->expects($this->once()) + ->method('getProperty') + ->with(TaoOntology::PROPERTY_UNIQUE_IDENTIFIER) + ->willReturn($this->property); + + $this->testCreatedEvent + ->expects($this->once()) + ->method('getTestUri') + ->willReturn('testUri'); + + $this->ontology + ->expects($this->once()) + ->method('getResource') + ->with('testUri') + ->willReturn($this->test); + + $this->test + ->expects($this->once()) + ->method('getOnePropertyValue') + ->with($this->property) + ->willReturn(null); + + $this->logger + ->expects($this->never()) + ->method('info'); + + $this->qtiIdentifierRetriever + ->expects($this->once()) + ->method('retrieve') + ->with($this->test) + ->willReturn('qtiIdentifier'); + + $this->test + ->expects($this->once()) + ->method('setPropertyValue') + ->with($this->property, 'qtiIdentifier'); + + $this->sut->populateUniqueId($this->testCreatedEvent); + } + + public function testPopulateUniqueIdValueSet(): void + { + $this->featureFlagChecker + ->expects($this->once()) + ->method('isEnabled') + ->with('FEATURE_FLAG_UNIQUE_NUMERIC_QTI_IDENTIFIER') + ->willReturn(true); + + $this->ontology + ->expects($this->once()) + ->method('getProperty') + ->with(TaoOntology::PROPERTY_UNIQUE_IDENTIFIER) + ->willReturn($this->property); + + $this->testCreatedEvent + ->expects($this->once()) + ->method('getTestUri') + ->willReturn('testUri'); + + $this->ontology + ->expects($this->once()) + ->method('getResource') + ->with('testUri') + ->willReturn($this->test); + + $this->test + ->expects($this->once()) + ->method('getOnePropertyValue') + ->with($this->property) + ->willReturn('propertyValue'); + + $this->logger + ->expects($this->once()) + ->method('info'); + + $this->qtiIdentifierRetriever + ->expects($this->never()) + ->method('retrieve'); + + $this->test + ->expects($this->never()) + ->method('setPropertyValue'); + + $this->sut->populateUniqueId($this->testCreatedEvent); + } +} diff --git a/test/unit/models/classes/UniqueId/Service/QtiIdentifierRetrieverTest.php b/test/unit/models/classes/UniqueId/Service/QtiIdentifierRetrieverTest.php new file mode 100644 index 000000000..d0f81e47b --- /dev/null +++ b/test/unit/models/classes/UniqueId/Service/QtiIdentifierRetrieverTest.php @@ -0,0 +1,96 @@ +test = $this->createMock(core_kernel_classes_Resource::class); + + $this->qtiTestService = $this->createMock(taoQtiTest_models_classes_QtiTestService::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->sut = new QtiIdentifierRetriever($this->qtiTestService, $this->logger); + } + + public function testRetrieve(): void + { + $this->qtiTestService + ->expects($this->once()) + ->method('getJsonTest') + ->with($this->test) + ->willReturn('{"identifier":"qtiIdentifier"}'); + + $this->assertEquals('qtiIdentifier', $this->sut->retrieve($this->test)); + } + + public function testRetrieveNoIdentifier(): void + { + $this->qtiTestService + ->expects($this->once()) + ->method('getJsonTest') + ->with($this->test) + ->willReturn('[]'); + + $this->assertEquals(null, $this->sut->retrieve($this->test)); + } + + public function testRetrieveException(): void + { + $this->qtiTestService + ->expects($this->once()) + ->method('getJsonTest') + ->with($this->test) + ->willThrowException(new Exception('error')); + + $this->logger + ->expects($this->once()) + ->method('error') + ->with('An error occurred while retrieving test data: error'); + + $this->expectException(Throwable::class); + + $this->sut->retrieve($this->test); + } +} diff --git a/views/js/controller/creator/creator.js b/views/js/controller/creator/creator.js index e04c1ccf1..2bf9a5ebb 100644 --- a/views/js/controller/creator/creator.js +++ b/views/js/controller/creator/creator.js @@ -126,6 +126,7 @@ define([ options.labels = options.labels || {}; options.categoriesPresets = featureVisibility.filterVisiblePresets(options.categoriesPresets) || {}; options.guidedNavigation = options.guidedNavigation === true; + options.translation = options.translation === true; categorySelector.setPresets(options.categoriesPresets); @@ -135,9 +136,8 @@ define([ creatorContext.trigger('creatorclose'); }); - //preview button let previewId = 0; - const createPreviewButton = ({ id, label } = {}) => { + const createPreviewButton = ({ id, label, uri = '' } = {}) => { // configured labels will need to to be registered elsewhere for the translations const translate = text => text && __(text); @@ -152,7 +152,7 @@ define([ ).on('click', e => { e.preventDefault(); if (!$(e.currentTarget).hasClass('disabled')) { - creatorContext.trigger('preview', id, previewId); + creatorContext.trigger('preview', id, uri); } }); if (!Object.keys(options.labels).length) { @@ -162,9 +162,19 @@ define([ previewId++; return $button; }; - const previewButtons = options.providers - ? options.providers.map(createPreviewButton) - : [createPreviewButton()]; + + let previewButtons; + + if (options.translation) { + previewButtons = [ + createPreviewButton({ label: 'Preview original', uri: options.originResourceUri }), + createPreviewButton({ label: 'Preview translation' }) + ]; + } else { + previewButtons = options.providers + ? options.providers.map(createPreviewButton) + : [createPreviewButton()]; + } const isTestContainsItems = () => { if ($container.find('.test-content').find('.itemref').length) { @@ -219,6 +229,8 @@ define([ binder = DataBindController.takeControl($container, binderOptions).get(model => { creatorContext = qtiTestCreatorFactory($container, { uri: options.uri, + translation: options.translation, + originResourceUri: options.originResourceUri, labels: options.labels, routes: options.routes, guidedNavigation: options.guidedNavigation @@ -260,10 +272,10 @@ define([ } }); - creatorContext.on('preview', provider => { + creatorContext.on('preview', (provider, uri) => { if (isTestContainsItems() && !creatorContext.isTestHasErrors()) { const saveUrl = options.routes.save; - const testUri = saveUrl.slice(saveUrl.indexOf('uri=') + 4); + const testUri = uri || saveUrl.slice(saveUrl.indexOf('uri=') + 4); const config = module.config(); const type = provider || config.provider || 'qtiTest'; return previewerFactory(type, decodeURIComponent(testUri), { diff --git a/views/templates/creator.tpl b/views/templates/creator.tpl index acd9f2f62..fcc2ef17f 100755 --- a/views/templates/creator.tpl +++ b/views/templates/creator.tpl @@ -76,6 +76,8 @@ requirejs.config({ blueprintByTestSection : '', identifier : '' }, + translation : , + originResourceUri : , categoriesPresets : , labels : , guidedNavigation :