diff --git a/README.md b/README.md
index 4424ca77eb..d7dd1ee4a4 100644
--- a/README.md
+++ b/README.md
@@ -106,6 +106,7 @@ Here you can find the environment variables including feature flags
| DATA_STORE_STATISTIC_PUB_SUB_TOPIC | Topic name for statistic metadata Pub/Sub | - |
| REDIRECT_AFTER_LOGOUT_URL | Allows to configure the redirect after logout via environment variable. The fallback is the configured redirect on urlroute.conf.php | - |
| PORTAL_URL | The Portal url used on the back button of Portal theme | - |
+| FEATURE_TRANSLATION_ENABLED | Enable access to items/tests translations feature | - |
# Routing
diff --git a/actions/class.Main.php b/actions/class.Main.php
index f17b3a72fb..f2d693b0a3 100644
--- a/actions/class.Main.php
+++ b/actions/class.Main.php
@@ -521,11 +521,13 @@ private function getSections($shownExtension, $shownStructure)
$sections = [];
$user = $this->getSession()->getUser();
$structure = MenuService::getPerspective($shownExtension, $shownStructure);
+ $sectionVisibilityFilter = $this->getSectionVisibilityFilter();
+
if (!is_null($structure)) {
foreach ($structure->getChildren() as $section) {
$resolver = new ActionResolver($section->getUrl());
- if (!$this->getSectionVisibilityFilter()->isVisible($section->getId())) {
+ if (!$sectionVisibilityFilter->isVisible($section->getId())) {
continue;
}
@@ -540,6 +542,19 @@ private function getSections($shownExtension, $shownStructure)
if (FuncProxy::accessPossible($user, $resolver->getController(), $resolver->getAction())) {
foreach ($section->getActions() as $action) {
+ $sectionPath = $sectionVisibilityFilter->createSectionPath(
+ [
+ $section->getId(),
+ $action->getId()
+ ]
+ );
+
+ if (!$sectionVisibilityFilter->isVisible($sectionPath)) {
+ $section->removeAction($action);
+
+ continue;
+ }
+
$this->propagate($action);
$resolver = new ActionResolver($action->getUrl());
if (
@@ -580,7 +595,7 @@ protected function getUserService()
return $this->getServiceLocator()->get(tao_models_classes_UserService::SERVICE_ID);
}
- private function getSectionVisibilityFilter(): SectionVisibilityFilterInterface
+ private function getSectionVisibilityFilter(): SectionVisibilityFilter
{
if (empty($this->sectionVisibilityFilter)) {
$this->sectionVisibilityFilter = $this->getServiceLocator()->get(SectionVisibilityFilter::SERVICE_ID);
diff --git a/actions/class.Translation.php b/actions/class.Translation.php
new file mode 100644
index 0000000000..b7840c6ded
--- /dev/null
+++ b/actions/class.Translation.php
@@ -0,0 +1,122 @@
+getTranslationUpdateService()->update(
+ new UpdateTranslationCommand(
+ $this->getRequestParameter('id'),
+ $this->getRequestParameter('progress'),
+ )
+ );
+
+ $this->setSuccessJsonResponse(
+ [
+ 'resourceUri' => $resource->getUri()
+ ]
+ );
+ } catch (Throwable $exception) {
+ $this->setErrorJsonResponse($exception->getMessage());
+ }
+ }
+
+ /**
+ * @requiresRight id WRITE
+ */
+ public function translate(): void
+ {
+ try {
+ $newResource = $this->getTranslationCreationService()->createByRequest($this->getPsrRequest());
+
+ $this->setSuccessJsonResponse(
+ [
+ 'resourceUri' => $newResource->getUri()
+ ]
+ );
+ } catch (Throwable $exception) {
+ $this->setErrorJsonResponse($exception->getMessage());
+ }
+ }
+
+ /**
+ * @requiresRight id READ
+ */
+ public function translations(): void
+ {
+ try {
+ $this->setSuccessJsonResponse(
+ $this->getResourceTranslationRetriever()->getByRequest($this->getPsrRequest())
+ );
+ } catch (Throwable $exception) {
+ $this->setErrorJsonResponse($exception->getMessage());
+ }
+ }
+
+ /**
+ * @requiresRight id READ
+ */
+ public function translatable(): void
+ {
+ try {
+ $this->setSuccessJsonResponse(
+ $this->getResourceTranslatableRetriever()->getByRequest($this->getPsrRequest())
+ );
+ } catch (Throwable $exception) {
+ $this->setErrorJsonResponse($exception->getMessage());
+ }
+ }
+
+ private function getResourceTranslationRetriever(): ResourceTranslationRetriever
+ {
+ return $this->getServiceManager()->getContainer()->get(ResourceTranslationRetriever::class);
+ }
+
+ private function getResourceTranslatableRetriever(): ResourceTranslatableRetriever
+ {
+ return $this->getServiceManager()->getContainer()->get(ResourceTranslatableRetriever::class);
+ }
+
+ private function getTranslationCreationService(): TranslationCreationService
+ {
+ return $this->getServiceManager()->getContainer()->get(TranslationCreationService::class);
+ }
+
+ private function getTranslationUpdateService(): TranslationUpdateService
+ {
+ return $this->getServiceManager()->getContainer()->get(TranslationUpdateService::class);
+ }
+}
diff --git a/doc/taoApi.yml b/doc/taoApi.yml
index d02290b0c6..c317cb667d 100644
--- a/doc/taoApi.yml
+++ b/doc/taoApi.yml
@@ -5,6 +5,89 @@ info:
version: v1
paths:
+ /tao/Translation/update:
+ post:
+ summary: Return a list of translations for a given translatable resource
+ requestBody:
+ required: true
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ properties:
+ id:
+ type: string
+ description: The resource URI
+ progress:
+ type: string
+ description: The progress URI
+ responses:
+ 200:
+ $ref: '#/components/responses/TranslateResponse'
+ 400:
+ $ref: '#/components/responses/BadRequestResponse'
+ 500:
+ $ref: '#/components/responses/InternalServerErrorResponse'
+ /tao/Translation/translations:
+ get:
+ summary: Return a list of translations for a given translatable resource
+ parameters:
+ - in: query
+ name: id
+ required: true
+ schema:
+ type: string
+ description: The RDF resource id
+ - in: query
+ name: languageUri
+ required: false
+ schema:
+ type: string
+ description: The RDF language URI
+ responses:
+ 200:
+ $ref: '#/components/responses/TranslationsResponse'
+ 400:
+ $ref: '#/components/responses/BadRequestResponse'
+ 500:
+ $ref: '#/components/responses/InternalServerErrorResponse'
+ /tao/Translation/translatable:
+ get:
+ summary: Return translatable resources
+ parameters:
+ - in: query
+ name: id
+ required: true
+ schema:
+ type: string
+ description: The RDF resource id
+ responses:
+ 200:
+ $ref: '#/components/responses/TranslatableResponse'
+ 400:
+ $ref: '#/components/responses/BadRequestResponse'
+ 500:
+ $ref: '#/components/responses/InternalServerErrorResponse'
+ /tao/Translation/translate:
+ post:
+ summary: translate a resources
+ requestBody:
+ required: true
+ content:
+ multipart/form-data:
+ schema:
+ type: object
+ properties:
+ id:
+ type: string
+ description: The resource URI
+ responses:
+ 200:
+ $ref: '#/components/responses/TranslateResponse'
+ 400:
+ $ref: '#/components/responses/BadRequestResponse'
+ 500:
+ $ref: '#/components/responses/InternalServerErrorResponse'
/tao/Languages/index:
get:
summary: Get a list of available languages in the TAO platform
@@ -46,6 +129,70 @@ paths:
$ref: '#/components/responses/InternalServerErrorResponse'
components:
schemas:
+ Translations:
+ properties:
+ success:
+ type: boolean
+ example: true
+ data:
+ type: object
+ properties:
+ resources:
+ type: array
+ items:
+ properties:
+ originResourceUri:
+ type: string
+ resourceUri:
+ type: string
+ resourceLabel:
+ type: string
+ metadata:
+ type: object
+ properties:
+ key:
+ type: object
+ properties:
+ value:
+ type: string
+ literal:
+ type: string
+ Translatable:
+ properties:
+ success:
+ type: boolean
+ example: true
+ data:
+ type: object
+ properties:
+ resources:
+ type: array
+ items:
+ properties:
+ resourceUri:
+ type: string
+ resourceLabel:
+ type: string
+ metadata:
+ type: object
+ properties:
+ key:
+ type: object
+ properties:
+ value:
+ type: string
+ literal:
+ type: string
+ Translated:
+ properties:
+ success:
+ type: boolean
+ example: true
+ data:
+ type: object
+ properties:
+ resourceUri:
+ type: string
ResourceRelationResource:
description: 'A resource related to another resources'
type: object
@@ -104,6 +251,24 @@ components:
message:
type: string
responses:
+ TranslationsResponse:
+ description: The list of translations
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Translations'
+ TranslatableResponse:
+ description: The list of translatable
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Translatable'
+ TranslateResponse:
+ description: The resource is translated
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Translated'
ResourceRelationsResponse:
description: Bad request
content:
diff --git a/helpers/form/class.Form.php b/helpers/form/class.Form.php
index 8caac01ad8..cd3724a0e3 100644
--- a/helpers/form/class.Form.php
+++ b/helpers/form/class.Form.php
@@ -666,20 +666,37 @@ public function setValues($values)
foreach ($values as $key => $value) {
foreach ($this->elements as $element) {
if ($element->getName() === $key) {
- if (
- $element instanceof tao_helpers_form_elements_Checkbox ||
- (method_exists($element, 'setValues') && is_array($value))
- ) {
- $element->setValues($value);
- } else {
- $element->setValue($value);
- }
+ $this->setElementValue($element, $value);
+
break;
}
}
}
}
+ public function setElementValue(tao_helpers_form_FormElement $element, $value): void
+ {
+ if (
+ $element instanceof tao_helpers_form_elements_Checkbox
+ || (method_exists($element, 'setValues') && is_array($value))
+ ) {
+ $element->setValues($value);
+
+ return;
+ }
+
+ $element->setValue($value);
+ }
+
+ public function setValue(string $name, $value): void
+ {
+ $element = $this->getElement($name);
+
+ if ($element) {
+ $this->setElementValue($element, $value);
+ }
+ }
+
/**
* Disables the whole form
*/
diff --git a/helpers/form/class.FormContainer.php b/helpers/form/class.FormContainer.php
index 7fe405faa3..bd5437a8ad 100644
--- a/helpers/form/class.FormContainer.php
+++ b/helpers/form/class.FormContainer.php
@@ -26,7 +26,9 @@
use oat\oatbox\service\ServiceManager;
use oat\oatbox\validator\ValidatorInterface;
+use oat\tao\model\form\Modifier\AbstractFormModifier;
use oat\tao\model\security\xsrf\TokenService;
+use Psr\Container\ContainerInterface;
use tao_helpers_form_FormFactory as FormFactory;
use oat\tao\helpers\form\elements\xhtml\CsrfToken;
use oat\tao\helpers\form\elements\FormElementAware;
@@ -46,6 +48,7 @@ abstract class tao_helpers_form_FormContainer
public const IS_DISABLED = 'is_disabled';
public const ADDITIONAL_VALIDATORS = 'extraValidators';
public const ATTRIBUTE_VALIDATORS = 'attributeValidators';
+ public const FORM_MODIFIERS = 'formModifiers';
public const WITH_SERVICE_MANAGER = 'withServiceManager';
@@ -118,6 +121,14 @@ public function __construct(array $data = [], array $options = [])
// initialize the elements of the form
$this->initElements();
+ foreach ($options[self::FORM_MODIFIERS] ?? [] as $modifierClass) {
+ $modifier = $this->getContainer()->get($modifierClass);
+
+ if ($modifier instanceof AbstractFormModifier) {
+ $modifier->modify($this->form, $options);
+ }
+ }
+
if ($this->form !== null) {
$this->form->evaluateInputValues();
@@ -317,8 +328,7 @@ private function configureFormValidators(iterable $validators, tao_helpers_form_
private function getSanitizerValidationFeeder(): SanitizerValidationFeederInterface
{
if (!isset($this->sanitizerValidationFeeder)) {
- $serviceManager = $this->serviceManager ?? ServiceManager::getServiceManager();
- $this->sanitizerValidationFeeder = $serviceManager->getContainer()->get(SanitizerValidationFeeder::class);
+ $this->sanitizerValidationFeeder = $this->getContainer()->get(SanitizerValidationFeeder::class);
}
return $this->sanitizerValidationFeeder;
@@ -335,4 +345,11 @@ private function withServiceManager(array &$options): void
unset($options[self::WITH_SERVICE_MANAGER]);
}
+
+ private function getContainer(): ContainerInterface
+ {
+ $serviceManager = $this->serviceManager ?? ServiceManager::getServiceManager();
+
+ return $serviceManager->getContainer();
+ }
}
diff --git a/manifest.php b/manifest.php
index a94598883d..00d6625fe1 100755
--- a/manifest.php
+++ b/manifest.php
@@ -57,6 +57,7 @@
use oat\tao\model\routing\ServiceProvider\RoutingServiceProvider;
use oat\tao\model\search\ServiceProvider\SearchServiceProvider;
use oat\tao\model\StatisticalMetadata\StatisticalMetadataServiceProvider;
+use oat\tao\model\Translation\ServiceProvider\TranslationServiceProvider;
use oat\tao\model\user\TaoRoles;
use oat\tao\model\user\UserSettingsServiceProvider;
use oat\tao\model\webhooks\WebhookServiceProvider;
@@ -418,7 +419,8 @@
MenuServiceProvider::class,
FormDataProviderServiceProvider::class,
PropertyServiceProvider::class,
- DynamicConfigServiceProvider::class
+ DynamicConfigServiceProvider::class,
+ TranslationServiceProvider::class
],
'middlewares' => [
MiddlewareConfig::class,
diff --git a/migrations/Version202409040743452141_tao.php b/migrations/Version202409040743452141_tao.php
new file mode 100644
index 0000000000..73c630b358
--- /dev/null
+++ b/migrations/Version202409040743452141_tao.php
@@ -0,0 +1,29 @@
+addReport(Report::createSuccess('Ontology models successfully synchronized'));
+ }
+
+ public function down(Schema $schema): void
+ {
+ }
+}
diff --git a/models/classes/TaoOntology.php b/models/classes/TaoOntology.php
index a25702de43..73a2ae426f 100644
--- a/models/classes/TaoOntology.php
+++ b/models/classes/TaoOntology.php
@@ -117,4 +117,28 @@ interface TaoOntology
public const PROPERTY_STANCE_LANGUAGE_USAGE_DATA = 'http://www.tao.lu/Ontologies/TAO.rdf#LanguageUsageData';
/** @deprecated use tao_models_classes_LanguageService::CLASS_URI_LANGUAGES */
public const LANGUAGES_CLASS_URI = 'http://www.tao.lu/Ontologies/TAO.rdf#Languages';
+
+ public const PROPERTY_TRANSLATION_TYPE = 'http://www.tao.lu/Ontologies/TAO.rdf#TranslationType';
+ public const PROPERTY_VALUE_TRANSLATION_TYPE_ORIGINAL =
+ 'http://www.tao.lu/Ontologies/TAO.rdf#TranslationTypeOriginal';
+ public const PROPERTY_VALUE_TRANSLATION_TYPE_TRANSLATION =
+ 'http://www.tao.lu/Ontologies/TAO.rdf#TranslationTypeTranslation';
+
+ public const PROPERTY_TRANSLATION_PROGRESS = 'http://www.tao.lu/Ontologies/TAO.rdf#TranslationProgress';
+ public const PROPERTY_VALUE_TRANSLATION_PROGRESS_PENDING =
+ 'http://www.tao.lu/Ontologies/TAO.rdf#TranslationProgressStatusPending';
+ public const PROPERTY_VALUE_TRANSLATION_PROGRESS_TRANSLATING =
+ 'http://www.tao.lu/Ontologies/TAO.rdf#TranslationProgressStatusTranslating';
+ public const PROPERTY_VALUE_TRANSLATION_PROGRESS_TRANSLATED =
+ 'http://www.tao.lu/Ontologies/TAO.rdf#TranslationProgressStatusTranslated';
+
+ public const PROPERTY_TRANSLATION_STATUS = 'http://www.tao.lu/Ontologies/TAO.rdf#TranslationStatus';
+ public const PROPERTY_VALUE_TRANSLATION_STATUS_READY =
+ 'http://www.tao.lu/Ontologies/TAO.rdf#TranslationStatusReadyForTranslation';
+ public const PROPERTY_VALUE_TRANSLATION_STATUS_NOT_READY =
+ 'http://www.tao.lu/Ontologies/TAO.rdf#TranslationStatusNotReadyForTranslation';
+
+ public const PROPERTY_UNIQUE_IDENTIFIER = 'http://www.tao.lu/Ontologies/TAO.rdf#UniqueIdentifier';
+ public const PROPERTY_LANGUAGE = 'http://www.tao.lu/Ontologies/TAO.rdf#Language';
+ public const LANGUAGE_PREFIX = 'http://www.tao.lu/Ontologies/TAO.rdf#Lang';
}
diff --git a/models/classes/Translation/Command/CreateTranslationCommand.php b/models/classes/Translation/Command/CreateTranslationCommand.php
new file mode 100644
index 0000000000..01bb70a7e8
--- /dev/null
+++ b/models/classes/Translation/Command/CreateTranslationCommand.php
@@ -0,0 +1,52 @@
+resourceType = $resourceType;
+ $this->uniqueId = $uniqueId;
+ $this->languageUri = $languageUri;
+ }
+
+ public function getResourceType(): string
+ {
+ return $this->resourceType;
+ }
+
+ public function getUniqueId(): string
+ {
+ return $this->uniqueId;
+ }
+
+ public function getLanguageUri(): string
+ {
+ return $this->languageUri;
+ }
+}
diff --git a/models/classes/Translation/Command/UpdateTranslationCommand.php b/models/classes/Translation/Command/UpdateTranslationCommand.php
new file mode 100644
index 0000000000..48a4fd0cd6
--- /dev/null
+++ b/models/classes/Translation/Command/UpdateTranslationCommand.php
@@ -0,0 +1,62 @@
+resourceUri = $resourceUri;
+ $this->progressUri = $progressUri;
+ }
+
+ public function getResourceUri(): string
+ {
+ return $this->resourceUri;
+ }
+
+ public function getProgressUri(): string
+ {
+ return $this->progressUri;
+ }
+}
diff --git a/models/classes/Translation/Entity/AbstractResource.php b/models/classes/Translation/Entity/AbstractResource.php
new file mode 100644
index 0000000000..ee0d83bfe5
--- /dev/null
+++ b/models/classes/Translation/Entity/AbstractResource.php
@@ -0,0 +1,122 @@
+resourceUri = $resourceUri;
+ $this->resourceLabel = $resourceLabel;
+ }
+
+ public function getResourceUri(): string
+ {
+ return $this->resourceUri;
+ }
+
+ public function getResourceLabel(): string
+ {
+ return $this->resourceLabel;
+ }
+
+ public function getUniqueId(): ?string
+ {
+ return $this->getMetadataValue(TaoOntology::PROPERTY_UNIQUE_IDENTIFIER);
+ }
+
+ public function getLanguageCode(): ?string
+ {
+ return $this->getMetadataLiteralValue(TaoOntology::PROPERTY_LANGUAGE);
+ }
+
+ public function getLanguageUri(): ?string
+ {
+ return $this->getMetadataValue(TaoOntology::PROPERTY_LANGUAGE);
+ }
+
+ /**
+ * @param string|array|null $value
+ * @param string|null $literal
+ */
+ public function addMetadata(string $uri, $value, $literal): void
+ {
+ $this->metadata[$uri] = [
+ 'value' => $value,
+ 'literal' => $literal
+ ];
+ }
+
+ public function getMetadataValue(string $metadata): ?string
+ {
+ if (!empty($this->metadata[$metadata])) {
+ return $this->metadata[$metadata]['value'];
+ }
+
+ return null;
+ }
+
+ public function getMetadataLiteralValue(string $metadata): ?string
+ {
+ if (!empty($this->metadata[$metadata])) {
+ return $this->metadata[$metadata]['literal'];
+ }
+
+ return null;
+ }
+
+ public function getMetadata(): array
+ {
+ return $this->metadata;
+ }
+
+ public function addMetadataUri(string $uri): void
+ {
+ $this->metadataUris = array_unique(array_merge($this->metadataUris, [$uri]));
+ }
+
+ public function getMetadataUris(): array
+ {
+ return $this->metadataUris;
+ }
+
+ public function jsonSerialize(): array
+ {
+ return [
+ 'resourceUri' => $this->getResourceUri(),
+ 'resourceLabel' => $this->getResourceLabel(),
+ 'metadata' => $this->metadata,
+ ];
+ }
+}
diff --git a/models/classes/Translation/Entity/ResourceCollection.php b/models/classes/Translation/Entity/ResourceCollection.php
new file mode 100644
index 0000000000..a9b368a0bd
--- /dev/null
+++ b/models/classes/Translation/Entity/ResourceCollection.php
@@ -0,0 +1,41 @@
+ $this->getArrayCopy()
+ ];
+ }
+}
diff --git a/models/classes/Translation/Entity/ResourceTranslatable.php b/models/classes/Translation/Entity/ResourceTranslatable.php
new file mode 100644
index 0000000000..c66619aaca
--- /dev/null
+++ b/models/classes/Translation/Entity/ResourceTranslatable.php
@@ -0,0 +1,38 @@
+getMetadataValue(TaoOntology::PROPERTY_TRANSLATION_STATUS);
+ }
+
+ public function isReadyForTranslation(): bool
+ {
+ return $this->getStatusUri() === TaoOntology::PROPERTY_VALUE_TRANSLATION_STATUS_READY;
+ }
+}
diff --git a/models/classes/Translation/Entity/ResourceTranslation.php b/models/classes/Translation/Entity/ResourceTranslation.php
new file mode 100644
index 0000000000..0b9d0c0b11
--- /dev/null
+++ b/models/classes/Translation/Entity/ResourceTranslation.php
@@ -0,0 +1,55 @@
+originResourceUri = $originResourceUri;
+ }
+
+ public function getOriginResourceUri(): string
+ {
+ return $this->originResourceUri;
+ }
+
+ public function getProgressUri(): string
+ {
+ return $this->getMetadataValue(TaoOntology::PROPERTY_TRANSLATION_PROGRESS);
+ }
+
+ public function jsonSerialize(): array
+ {
+ return array_merge(
+ [
+ 'originResourceUri' => $this->getOriginResourceUri()
+ ],
+ parent::jsonSerialize(),
+ );
+ }
+}
diff --git a/models/classes/Translation/Exception/ResourceTranslationException.php b/models/classes/Translation/Exception/ResourceTranslationException.php
new file mode 100644
index 0000000000..c341028182
--- /dev/null
+++ b/models/classes/Translation/Exception/ResourceTranslationException.php
@@ -0,0 +1,29 @@
+metadataPopulateService = $metadataPopulateService;
+ }
+
+ public function create(core_kernel_classes_Resource $originResource): ResourceTranslatable
+ {
+ $resource = new ResourceTranslatable($originResource->getUri(), $originResource->getLabel());
+ $resource->addMetadataUri(TaoOntology::PROPERTY_TRANSLATION_STATUS);
+
+ $this->metadataPopulateService->populate($resource, $originResource);
+
+ return $resource;
+ }
+}
diff --git a/models/classes/Translation/Factory/ResourceTranslationFactory.php b/models/classes/Translation/Factory/ResourceTranslationFactory.php
new file mode 100644
index 0000000000..70f3ee2045
--- /dev/null
+++ b/models/classes/Translation/Factory/ResourceTranslationFactory.php
@@ -0,0 +1,52 @@
+metadataPopulateService = $metadataPopulateService;
+ }
+
+ public function create(
+ ResourceTranslatable $originResource,
+ core_kernel_classes_Resource $translationResource
+ ): ResourceTranslation {
+ $resource = new ResourceTranslation($translationResource->getUri(), $translationResource->getLabel());
+ $resource->setOriginResourceUri($originResource->getResourceUri());
+ $resource->addMetadataUri(TaoOntology::PROPERTY_TRANSLATION_PROGRESS);
+
+ $this->metadataPopulateService->populate($resource, $translationResource);
+
+ return $resource;
+ }
+}
diff --git a/models/classes/Translation/Form/Modifier/TranslationFormModifier.php b/models/classes/Translation/Form/Modifier/TranslationFormModifier.php
new file mode 100644
index 0000000000..b9af7af51d
--- /dev/null
+++ b/models/classes/Translation/Form/Modifier/TranslationFormModifier.php
@@ -0,0 +1,84 @@
+featureFlagChecker = $featureFlagChecker;
+ }
+
+ public function modify(tao_helpers_form_Form $form, array $options = []): void
+ {
+ foreach ($this->getTranslationElementsToRemove($form) as $elementUri) {
+ $form->removeElement(tao_helpers_Uri::encode($elementUri));
+ }
+ }
+
+ private function getTranslationElementsToRemove(tao_helpers_form_Form $form): array
+ {
+ if (!$this->featureFlagChecker->isEnabled('FEATURE_TRANSLATION_ENABLED')) {
+ return [
+ TaoOntology::PROPERTY_UNIQUE_IDENTIFIER,
+ TaoOntology::PROPERTY_LANGUAGE,
+ TaoOntology::PROPERTY_TRANSLATION_TYPE,
+ TaoOntology::PROPERTY_TRANSLATION_STATUS,
+ TaoOntology::PROPERTY_TRANSLATION_PROGRESS,
+ ];
+ }
+
+ $elementsToRemove = [];
+
+ if (!$this->featureFlagChecker->isEnabled('FEATURE_TRANSLATION_DEVELOPER_MODE')) {
+ $elementsToRemove[] = TaoOntology::PROPERTY_TRANSLATION_TYPE;
+ }
+
+ $translationTypeValue = $form->getValue(tao_helpers_Uri::encode(TaoOntology::PROPERTY_TRANSLATION_TYPE));
+ $isTranslationTypeValueEmpty = empty($translationTypeValue);
+
+ if (
+ $isTranslationTypeValueEmpty
+ || $translationTypeValue === TaoOntology::PROPERTY_VALUE_TRANSLATION_TYPE_ORIGINAL
+ ) {
+ $elementsToRemove[] = TaoOntology::PROPERTY_TRANSLATION_PROGRESS;
+ }
+
+ if (
+ $isTranslationTypeValueEmpty
+ || $translationTypeValue === TaoOntology::PROPERTY_VALUE_TRANSLATION_TYPE_TRANSLATION
+ ) {
+ $elementsToRemove[] = TaoOntology::PROPERTY_TRANSLATION_STATUS;
+ }
+
+ return $elementsToRemove;
+ }
+}
diff --git a/models/classes/Translation/Query/ResourceTranslatableQuery.php b/models/classes/Translation/Query/ResourceTranslatableQuery.php
new file mode 100644
index 0000000000..6af00a9d1e
--- /dev/null
+++ b/models/classes/Translation/Query/ResourceTranslatableQuery.php
@@ -0,0 +1,45 @@
+resourceType = $resourceType;
+ $this->uniqueIds = $uniqueIds;
+ }
+
+ public function getResourceType(): string
+ {
+ return $this->resourceType;
+ }
+
+ public function getUniqueIds(): array
+ {
+ return $this->uniqueIds;
+ }
+}
diff --git a/models/classes/Translation/Query/ResourceTranslationQuery.php b/models/classes/Translation/Query/ResourceTranslationQuery.php
new file mode 100644
index 0000000000..a10c516876
--- /dev/null
+++ b/models/classes/Translation/Query/ResourceTranslationQuery.php
@@ -0,0 +1,52 @@
+resourceType = $resourceType;
+ $this->uniqueIds = $uniqueIds;
+ $this->languageUri = $languageUri;
+ }
+
+ public function getResourceType(): string
+ {
+ return $this->resourceType;
+ }
+
+ public function getUniqueIds(): array
+ {
+ return $this->uniqueIds;
+ }
+
+ public function getLanguageUri(): ?string
+ {
+ return $this->languageUri;
+ }
+}
diff --git a/models/classes/Translation/Repository/ResourceTranslatableRepository.php b/models/classes/Translation/Repository/ResourceTranslatableRepository.php
new file mode 100644
index 0000000000..3e12ae523d
--- /dev/null
+++ b/models/classes/Translation/Repository/ResourceTranslatableRepository.php
@@ -0,0 +1,88 @@
+complexSearch = $complexSearch;
+ $this->factory = $factory;
+ $this->ontology = $ontology;
+ }
+
+ public function find(ResourceTranslatableQuery $query): ResourceCollection
+ {
+ $queryBuilder = $this->complexSearch->query();
+ $searchQuery = $this->complexSearch->searchType(
+ $queryBuilder,
+ 'http://www.tao.lu/Ontologies/TAO.rdf#AssessmentContentObject',
+ true
+ );
+ $searchQuery->addCriterion(
+ TaoOntology::PROPERTY_TRANSLATION_TYPE,
+ SupportedOperatorHelper::EQUAL,
+ TaoOntology::PROPERTY_VALUE_TRANSLATION_TYPE_ORIGINAL
+ );
+
+ if (!empty($query->getUniqueIds())) {
+ $searchQuery->addCriterion(
+ TaoOntology::PROPERTY_UNIQUE_IDENTIFIER,
+ SupportedOperatorHelper::IN,
+ $query->getUniqueIds()
+ );
+ }
+
+ $queryBuilder->setCriteria($searchQuery);
+
+ $result = $this->complexSearch->getGateway()->search($queryBuilder);
+ $output = [];
+
+ $resourceTypeClass = $this->ontology->getClass($query->getResourceType());
+
+ /** @var core_kernel_classes_Resource $resource */
+ foreach ($result as $resource) {
+ if ($resource->isInstanceOf($resourceTypeClass)) {
+ $output[] = $this->factory->create($resource);
+ }
+ }
+
+ return new ResourceCollection(...$output);
+ }
+}
diff --git a/models/classes/Translation/Repository/ResourceTranslationRepository.php b/models/classes/Translation/Repository/ResourceTranslationRepository.php
new file mode 100644
index 0000000000..f5c48a32a8
--- /dev/null
+++ b/models/classes/Translation/Repository/ResourceTranslationRepository.php
@@ -0,0 +1,137 @@
+ontology = $ontology;
+ $this->complexSearch = $complexSearch;
+ $this->resourceTranslatableRepository = $resourceTranslatableRepository;
+ $this->factory = $resourceTranslationFactory;
+ $this->logger = $logger;
+ }
+
+ public function find(ResourceTranslationQuery $query): ResourceCollection
+ {
+ $uniqueIds = $query->getUniqueIds();
+ $resources = $this->resourceTranslatableRepository->find(
+ new ResourceTranslatableQuery(
+ $query->getResourceType(),
+ $uniqueIds
+ )
+ );
+
+ if ($resources->count() === 0) {
+ throw new Exception(
+ sprintf(
+ 'Translation Origin Resource [%s] does not exist',
+ implode(',', $uniqueIds)
+ )
+ );
+ }
+
+ /** @var ResourceTranslatable $originResource */
+ $originResource = $resources->current();
+ $output = [];
+
+ $queryBuilder = $this->complexSearch->query();
+ $searchQuery = $this->complexSearch->searchType(
+ $queryBuilder,
+ 'http://www.tao.lu/Ontologies/TAO.rdf#AssessmentContentObject',
+ true
+ );
+ $searchQuery->addCriterion(
+ TaoOntology::PROPERTY_TRANSLATION_TYPE,
+ SupportedOperatorHelper::EQUAL,
+ TaoOntology::PROPERTY_VALUE_TRANSLATION_TYPE_TRANSLATION
+ );
+ $searchQuery->addCriterion(
+ TaoOntology::PROPERTY_UNIQUE_IDENTIFIER,
+ SupportedOperatorHelper::IN,
+ $uniqueIds
+ );
+
+ if ($query->getLanguageUri()) {
+ $searchQuery->addCriterion(
+ TaoOntology::PROPERTY_LANGUAGE,
+ SupportedOperatorHelper::EQUAL,
+ $query->getLanguageUri()
+ );
+ }
+
+ $queryBuilder->setCriteria($searchQuery);
+
+ $result = $this->complexSearch->getGateway()->search($queryBuilder);
+ $resourceTypeClass = $this->ontology->getClass($query->getResourceType());
+
+ /** @var core_kernel_classes_Resource $translationResource */
+ foreach ($result as $translationResource) {
+ try {
+ if (!$translationResource->isInstanceOf($resourceTypeClass)) {
+ continue;
+ }
+
+ $output[] = $this->factory->create($originResource, $translationResource);
+ } catch (Throwable $exception) {
+ $this->logger->warning(
+ sprintf(
+ 'Cannot read translation status for [uniqueIds=%s, translationResourceId=%s]: %s - %s',
+ $uniqueIds,
+ $translationResource->getUri(),
+ $exception->getMessage(),
+ $exception->getTraceAsString()
+ )
+ );
+ }
+ }
+
+ return new ResourceCollection(...$output);
+ }
+}
diff --git a/models/classes/Translation/Service/ResourceMetadataPopulateService.php b/models/classes/Translation/Service/ResourceMetadataPopulateService.php
new file mode 100644
index 0000000000..2165126f35
--- /dev/null
+++ b/models/classes/Translation/Service/ResourceMetadataPopulateService.php
@@ -0,0 +1,80 @@
+ontology = $ontology;
+ }
+
+ public function addMetadata(string $resourceType, string $metadataUri): void
+ {
+ $this->metadata[$resourceType] ??= [];
+ $this->metadata[$resourceType] = array_unique(array_merge($this->metadata[$resourceType], [$metadataUri]));
+ }
+
+ public function populate(AbstractResource $resource, core_kernel_classes_Resource $originResource): void
+ {
+ $valueProperty = $this->ontology->getProperty(OntologyRdf::RDF_VALUE);
+
+ $parentClasses = $originResource->getParentClassesIds();
+ $resourceType = array_pop($parentClasses);
+
+ foreach ($this->metadata[$resourceType] ?? [] as $metadataUri) {
+ $resource->addMetadataUri($metadataUri);
+ }
+
+ foreach ($resource->getMetadataUris() as $metadataUri) {
+ $values = $originResource->getPropertyValues($this->ontology->getProperty($metadataUri));
+
+ if (empty($values)) {
+ continue;
+ }
+
+ $literal = null;
+ $value = empty($values) ? null : current($values);
+
+ if ($value) {
+ $literalProperty = $this->ontology->getProperty($value);
+ $oneValue = $literalProperty->getOnePropertyValue($valueProperty);
+
+ if ($oneValue instanceof core_kernel_classes_Literal) {
+ $literal = $oneValue->literal;
+ }
+ }
+
+ $resource->addMetadata($metadataUri, $value, $literal);
+ }
+ }
+}
diff --git a/models/classes/Translation/Service/ResourceTranslatableRetriever.php b/models/classes/Translation/Service/ResourceTranslatableRetriever.php
new file mode 100644
index 0000000000..db39b69a1d
--- /dev/null
+++ b/models/classes/Translation/Service/ResourceTranslatableRetriever.php
@@ -0,0 +1,81 @@
+resourceTranslatableRepository = $resourceTranslationRepository;
+ $this->ontology = $ontology;
+ }
+
+ public function getByRequest(ServerRequestInterface $request): ResourceCollection
+ {
+ $queryParams = $request->getQueryParams();
+ $id = $queryParams['id'] ?? null;
+
+ if (empty($id)) {
+ throw new ResourceTranslationException('Resource id is required');
+ }
+
+ $resource = $this->ontology->getResource($id);
+
+ $parentClassIds = $resource->getParentClassesIds();
+ $resourceType = array_pop($parentClassIds);
+
+ if (empty($resourceType)) {
+ throw new ResourceTranslationException(sprintf('Resource %s must have a resource type', $id));
+ }
+
+ $uniqueId = $resource->getUniquePropertyValue(
+ $this->ontology->getProperty(TaoOntology::PROPERTY_UNIQUE_IDENTIFIER)
+ );
+
+ if (empty($uniqueId)) {
+ throw new ResourceTranslationException(sprintf('Resource %s must have a unique identifier', $id));
+ }
+
+ return $this->resourceTranslatableRepository->find(
+ new ResourceTranslatableQuery(
+ $resourceType,
+ [
+ (string)$uniqueId
+ ]
+ )
+ );
+ }
+}
diff --git a/models/classes/Translation/Service/ResourceTranslationRetriever.php b/models/classes/Translation/Service/ResourceTranslationRetriever.php
new file mode 100644
index 0000000000..c64d951e22
--- /dev/null
+++ b/models/classes/Translation/Service/ResourceTranslationRetriever.php
@@ -0,0 +1,81 @@
+ontology = $ontology;
+ $this->resourceTranslationRepository = $resourceTranslationRepository;
+ }
+
+ public function getByRequest(ServerRequestInterface $request): ResourceCollection
+ {
+ $queryParams = $request->getQueryParams();
+ $languageUri = $queryParams['languageUri'] ?? null;
+ $id = $queryParams['id'] ?? null;
+
+ if (empty($id)) {
+ throw new ResourceTranslationException('Resource id is required');
+ }
+
+ $resource = $this->ontology->getResource($id);
+
+ $parentClassIds = $resource->getParentClassesIds();
+ $resourceType = array_pop($parentClassIds);
+
+ if (empty($resourceType)) {
+ throw new ResourceTranslationException(sprintf('Resource %s must have a resource type', $id));
+ }
+
+ $uniqueId = $resource->getUniquePropertyValue(
+ $this->ontology->getProperty(TaoOntology::PROPERTY_UNIQUE_IDENTIFIER)
+ );
+
+ if (empty($uniqueId)) {
+ throw new ResourceTranslationException(sprintf('Resource %s must have a unique identifier', $id));
+ }
+
+ return $this->resourceTranslationRepository->find(
+ new ResourceTranslationQuery(
+ $resourceType,
+ [(string)$uniqueId],
+ $languageUri
+ )
+ );
+ }
+}
diff --git a/models/classes/Translation/Service/TranslationCreationService.php b/models/classes/Translation/Service/TranslationCreationService.php
new file mode 100644
index 0000000000..add93e8f83
--- /dev/null
+++ b/models/classes/Translation/Service/TranslationCreationService.php
@@ -0,0 +1,280 @@
+ontology = $ontology;
+ $this->resourceTranslatableRepository = $resourceTranslatableRepository;
+ $this->resourceTranslationRepository = $resourceTranslationRepository;
+ $this->languageRepository = $languageRepository;
+ $this->logger = $logger;
+ }
+
+ public function setOntologyClassService(string $resourceType, OntologyClassService $ontologyClassService): void
+ {
+ $this->ontologyClassServices[$resourceType] = $ontologyClassService;
+ }
+
+ public function addPostCreation(string $resourceType, callable $callable): void
+ {
+ $this->callables[$resourceType] = $this->callables[$resourceType] ?? [];
+ $this->callables[$resourceType][] = $callable;
+ }
+
+ public function createByRequest(ServerRequestInterface $request): core_kernel_classes_Resource
+ {
+ $requestParams = $request->getParsedBody();
+ $id = $requestParams['id'] ?? null;
+
+ if (empty($id)) {
+ throw new ResourceTranslationException('Resource id is required');
+ }
+
+ $resource = $this->ontology->getResource($id);
+
+ if (!$resource->exists()) {
+ throw new ResourceTranslationException(
+ sprintf(
+ 'Resource %s does not exist',
+ $id
+ )
+ );
+ }
+
+ /** @var core_kernel_classes_Resource $translationType */
+ $translationType = $resource->getUniquePropertyValue(
+ $this->ontology->getProperty(TaoOntology::PROPERTY_TRANSLATION_TYPE)
+ );
+
+ if ($translationType->getUri() !== TaoOntology::PROPERTY_VALUE_TRANSLATION_TYPE_ORIGINAL) {
+ throw new ResourceTranslationException(
+ sprintf(
+ 'Resource %s is not the original',
+ $id
+ )
+ );
+ }
+
+ $parentClassIds = $resource->getParentClassesIds();
+ $resourceType = array_pop($parentClassIds);
+
+ if (empty($resourceType)) {
+ throw new ResourceTranslationException(sprintf('Resource %s must have a resource type', $id));
+ }
+
+ $uniqueId = $resource->getUniquePropertyValue(
+ $this->ontology->getProperty(TaoOntology::PROPERTY_UNIQUE_IDENTIFIER)
+ );
+
+ if (empty($uniqueId)) {
+ throw new ResourceTranslationException(sprintf('Resource %s must have a unique identifier', $id));
+ }
+
+ if (empty($requestParams['languageUri'])) {
+ throw new ResourceTranslationException('Parameter languageUri is mandatory');
+ }
+
+ return $this->create(
+ new CreateTranslationCommand(
+ $resourceType,
+ (string)$uniqueId,
+ $requestParams['languageUri']
+ )
+ );
+ }
+
+ public function create(CreateTranslationCommand $command): core_kernel_classes_Resource
+ {
+ try {
+ return $this->doCreate($command);
+ } catch (Throwable $exception) {
+ $this->logger->error(
+ sprintf(
+ 'Could not translate [uniqueId=%s, resourceType=%s, language=%s] (%s): %s',
+ $command->getUniqueId(),
+ $command->getResourceType(),
+ $command->getLanguageUri(),
+ get_class($exception),
+ $exception->getMessage()
+ )
+ );
+
+ throw $exception;
+ }
+ }
+
+ private function doCreate(CreateTranslationCommand $command): core_kernel_classes_Resource
+ {
+ $translations = $this->resourceTranslationRepository->find(
+ new ResourceTranslationQuery(
+ $command->getResourceType(),
+ [$command->getUniqueId()],
+ $command->getLanguageUri()
+ )
+ );
+
+ if ($translations->count() > 0) {
+ throw new ResourceTranslationException(
+ sprintf(
+ 'Translation already exists for [uniqueId=%s, locale=%s]',
+ $command->getUniqueId(),
+ $command->getLanguageUri()
+ )
+ );
+ }
+
+ $resources = $this->resourceTranslatableRepository->find(
+ new ResourceTranslatableQuery(
+ $command->getResourceType(),
+ [$command->getUniqueId()]
+ )
+ );
+
+ if ($resources->count() === 0) {
+ throw new ResourceTranslationException(
+ sprintf(
+ 'There is not translatable resource for [uniqueId=%s]',
+ $command->getUniqueId()
+ )
+ );
+ }
+
+ /** @var ResourceTranslatable $resource */
+ $resource = $resources->current();
+
+ if (!$resource->isReadyForTranslation()) {
+ throw new ResourceTranslationException(
+ sprintf(
+ 'Resource [uniqueId=%s] is not ready for translation',
+ $command->getUniqueId()
+ )
+ );
+ }
+
+ $existingLanguages = $this->languageRepository->findAvailableLanguagesByUsage();
+ $language = null;
+
+ /** @var Language $language */
+ foreach ($existingLanguages as $existingLanguage) {
+ if ($existingLanguage->getUri() === $command->getLanguageUri()) {
+ $language = $existingLanguage;
+ }
+ }
+
+ if (!$language) {
+ throw new ResourceTranslationException(
+ sprintf(
+ 'Language %s does not exist',
+ $command->getLanguageUri()
+ )
+ );
+ }
+
+ if ($resource->getLanguageUri() === $language->getUri()) {
+ throw new ResourceTranslationException(
+ sprintf(
+ 'Cannot translate to original language %s',
+ $command->getLanguageUri()
+ )
+ );
+ }
+
+ $instance = $this->ontology->getResource($resource->getResourceUri());
+ $types = $instance->getTypes();
+ $type = array_pop($types);
+
+ $clonedInstance = $this->getOntologyService($command->getResourceType())->cloneInstance($instance, $type);
+
+ $clonedInstance->setLabel(sprintf('%s (%s)', $instance->getLabel(), $language->getCode()));
+
+ $clonedInstance->editPropertyValues(
+ $this->ontology->getProperty(TaoOntology::PROPERTY_LANGUAGE),
+ $language->getUri()
+ );
+
+ $clonedInstance->editPropertyValues(
+ $this->ontology->getProperty(TaoOntology::PROPERTY_TRANSLATION_TYPE),
+ TaoOntology::PROPERTY_VALUE_TRANSLATION_TYPE_TRANSLATION
+ );
+
+ $clonedInstance->editPropertyValues(
+ $this->ontology->getProperty(TaoOntology::PROPERTY_TRANSLATION_PROGRESS),
+ TaoOntology::PROPERTY_VALUE_TRANSLATION_PROGRESS_PENDING
+ );
+
+ foreach ($this->callables[$command->getResourceType()] ?? [] as $callable) {
+ $clonedInstance = $callable($clonedInstance);
+ }
+
+ return $clonedInstance;
+ }
+
+ private function getOntologyService(string $resourceType): OntologyClassService
+ {
+ $service = $this->ontologyClassServices[$resourceType] ?? null;
+
+ if ($service) {
+ return $service;
+ }
+
+ throw new ResourceTranslationException(
+ sprintf(
+ 'There is no OntologyClassService for resource type %s',
+ $resourceType
+ )
+ );
+ }
+}
diff --git a/models/classes/Translation/Service/TranslationUpdateService.php b/models/classes/Translation/Service/TranslationUpdateService.php
new file mode 100644
index 0000000000..7508fabdad
--- /dev/null
+++ b/models/classes/Translation/Service/TranslationUpdateService.php
@@ -0,0 +1,90 @@
+ontology = $ontology;
+ $this->logger = $logger;
+ }
+
+ public function update(UpdateTranslationCommand $command): core_kernel_classes_Resource
+ {
+ try {
+ $instance = $this->ontology->getResource($command->getResourceUri());
+
+ if (!$instance->exists()) {
+ throw new ResourceTranslationException(
+ sprintf(
+ 'Resource %s does not exist',
+ $command->getResourceUri()
+ )
+ );
+ }
+
+ $translationType = $instance->getOnePropertyValue(
+ $this->ontology->getProperty(TaoOntology::PROPERTY_TRANSLATION_TYPE)
+ );
+
+ if ($translationType->getUri() !== TaoOntology::PROPERTY_VALUE_TRANSLATION_TYPE_TRANSLATION) {
+ throw new ResourceTranslationException(
+ sprintf(
+ 'Resource %s is not a translation',
+ $command->getResourceUri()
+ )
+ );
+ }
+
+ $instance->editPropertyValues(
+ $this->ontology->getProperty(TaoOntology::PROPERTY_TRANSLATION_PROGRESS),
+ $command->getProgressUri()
+ );
+
+ return $instance;
+ } catch (Throwable $exception) {
+ $this->logger->error(
+ sprintf(
+ 'Could not update translation status of [resourceUri=%s] (%s): %s',
+ $command->getResourceUri(),
+ get_class($exception),
+ $exception->getMessage()
+ )
+ );
+
+ throw $exception;
+ }
+ }
+}
diff --git a/models/classes/Translation/ServiceProvider/TranslationServiceProvider.php b/models/classes/Translation/ServiceProvider/TranslationServiceProvider.php
new file mode 100644
index 0000000000..dcafea5a52
--- /dev/null
+++ b/models/classes/Translation/ServiceProvider/TranslationServiceProvider.php
@@ -0,0 +1,141 @@
+services();
+ $services->set(ResourceMetadataPopulateService::class, ResourceMetadataPopulateService::class)
+ ->args(
+ [
+ service(Ontology::SERVICE_ID),
+ ]
+ );
+
+ $services->set(ResourceTranslationRepository::class, ResourceTranslationRepository::class)
+ ->args(
+ [
+ service(Ontology::SERVICE_ID),
+ service(ComplexSearchService::SERVICE_ID),
+ service(ResourceTranslatableRepository::class),
+ service(ResourceTranslationFactory::class),
+ service(LoggerService::SERVICE_ID),
+ ]
+ );
+
+ $services->set(ResourceTranslatableRepository::class, ResourceTranslatableRepository::class)
+ ->args(
+ [
+ service(Ontology::SERVICE_ID),
+ service(ComplexSearchService::SERVICE_ID),
+ service(ResourceTranslatableFactory::class)
+ ]
+ );
+
+ $services->set(ResourceTranslationFactory::class, ResourceTranslationFactory::class)
+ ->args(
+ [
+ service(ResourceMetadataPopulateService::class)
+ ]
+ );
+
+ $services->set(ResourceTranslatableFactory::class, ResourceTranslatableFactory::class)
+ ->args(
+ [
+ service(ResourceMetadataPopulateService::class)
+ ]
+ );
+
+ $services->set(ResourceTranslationRetriever::class, ResourceTranslationRetriever::class)
+ ->args(
+ [
+ service(Ontology::SERVICE_ID),
+ service(ResourceTranslationRepository::class)
+ ]
+ )
+ ->public();
+
+ $services->set(ResourceTranslatableRetriever::class, ResourceTranslatableRetriever::class)
+ ->args(
+ [
+ service(Ontology::SERVICE_ID),
+ service(ResourceTranslatableRepository::class)
+ ]
+ )
+ ->public();
+
+ $services
+ ->set(TranslationFormModifier::class, TranslationFormModifier::class)
+ ->args([
+ service(FeatureFlagChecker::class),
+ ]);
+
+ $services
+ ->set(TranslationCreationService::class, TranslationCreationService::class)
+ ->args(
+ [
+ service(Ontology::SERVICE_ID),
+ service(ResourceTranslatableRepository::class),
+ service(ResourceTranslationRepository::class),
+ service(LanguageRepositoryInterface::class),
+ service(LoggerService::SERVICE_ID),
+ ]
+ )
+ ->public();
+
+ $services
+ ->set(TranslationUpdateService::class, TranslationUpdateService::class)
+ ->args(
+ [
+ service(Ontology::SERVICE_ID),
+ service(LoggerService::SERVICE_ID),
+ ]
+ )
+ ->public();
+ }
+}
diff --git a/models/classes/featureFlag/README.md b/models/classes/featureFlag/README.md
index 1f0877c874..3936e2ddf6 100644
--- a/models/classes/featureFlag/README.md
+++ b/models/classes/featureFlag/README.md
@@ -71,20 +71,28 @@ sections that have to be disabled/enabled based on feature flag.
return new oat\tao\model\menu\SectionVisibilityFilter(array(
'featureFlagSections' => [
- 'sectionName' => [
+ 'sectionName1' => [
+ 'FEATURE_FLAG_01'
+ ],
+ 'sectionName2/actionName' => [
'FEATURE_FLAG_01'
]
],
'featureFlagSectionsToHide' => [
- 'sectionNameToHide' => [
+ 'sectionNameToHide1' => [
+ 'FEATURE_FLAG_02'
+ ],
+ 'sectionNameToHide2/actionName' => [
'FEATURE_FLAG_02'
]
]
));
```
-This configuration will display `sectionName` when `FEATURE_FLAG_01` is enabled.
+This configuration will display `sectionName1` when `FEATURE_FLAG_01` is enabled.
+This configuration will display `actionName` from `sectionName2` when `FEATURE_FLAG_01` is enabled.
This configuration will hide `sectionNameToHide` when `FEATURE_FLAG_02` is enabled.
+This configuration will display `actionName` from `sectionNameToHide` when `FEATURE_FLAG_02` is enabled.
### Override configs on execution time
diff --git a/models/classes/form/Modifier/AbstractFormModifier.php b/models/classes/form/Modifier/AbstractFormModifier.php
new file mode 100644
index 0000000000..752c39bf5e
--- /dev/null
+++ b/models/classes/form/Modifier/AbstractFormModifier.php
@@ -0,0 +1,45 @@
+modifiers, true)) {
+ $this->modifiers[] = $modifier;
+ }
+ }
+
+ public function modify(tao_helpers_form_Form $form, array $options = []): void
+ {
+ foreach ($this->modifiers as $modifier) {
+ $modifier->modify($form, $options);
+ }
+ }
+}
diff --git a/models/classes/menu/SectionVisibilityFilter.php b/models/classes/menu/SectionVisibilityFilter.php
index e236b6435e..4e19bc0cdd 100644
--- a/models/classes/menu/SectionVisibilityFilter.php
+++ b/models/classes/menu/SectionVisibilityFilter.php
@@ -22,7 +22,6 @@
namespace oat\tao\model\menu;
-use LogicException;
use oat\generis\model\GenerisRdf;
use oat\oatbox\service\ConfigurableService;
use oat\tao\model\featureFlag\FeatureFlagChecker;
@@ -33,6 +32,7 @@
class SectionVisibilityFilter extends ConfigurableService implements SectionVisibilityFilterInterface
{
+ private const SECTION_PATH_SEPARATOR = '/';
public const SERVICE_ID = 'tao/SectionVisibilityFilter';
public const OPTION_FEATURE_FLAG_SECTIONS = 'featureFlagSections';
public const OPTION_FEATURE_FLAG_SECTIONS_TO_HIDE = 'featureFlagSectionsToHide';
@@ -44,10 +44,7 @@ class SectionVisibilityFilter extends ConfigurableService implements SectionVisi
public const SIMPLE_INTERFACE_MODE_HIDDEN_SECTIONS = [];
- /**
- * @throws LogicException
- */
- public function isVisible(string $section): bool
+ public function isVisible(string $sectionPath): bool
{
$sections = $this->getOption(self::OPTION_FEATURE_FLAG_SECTIONS, []);
$sectionToHide = array_merge_recursive(
@@ -55,13 +52,13 @@ public function isVisible(string $section): bool
self::DEFAULT_FEATURE_FLAG_SECTIONS_TO_HIDE
);
- foreach ($sectionToHide[$section] ?? [] as $featureFlag) {
+ foreach ($sectionToHide[$sectionPath] ?? [] as $featureFlag) {
if ($this->getFeatureFlagChecker()->isEnabled($featureFlag)) {
return false;
}
}
- foreach ($sections[$section] ?? [] as $featureFlag) {
+ foreach ($sections[$sectionPath] ?? [] as $featureFlag) {
if (!$this->getFeatureFlagChecker()->isEnabled($featureFlag)) {
return false;
}
@@ -73,7 +70,7 @@ public function isVisible(string $section): bool
$userSettings->getSetting(
UserSettingsInterface::INTERFACE_MODE
) === GenerisRdf::PROPERTY_USER_INTERFACE_MODE_SIMPLE
- && in_array($section, self::SIMPLE_INTERFACE_MODE_HIDDEN_SECTIONS)
+ && in_array($sectionPath, self::SIMPLE_INTERFACE_MODE_HIDDEN_SECTIONS, true)
) {
return false;
}
@@ -81,11 +78,16 @@ public function isVisible(string $section): bool
return true;
}
- public function hideSectionByFeatureFlag(string $section, string $featureFlag): void
+ public function createSectionPath(array $segments): string
+ {
+ return implode(self::SECTION_PATH_SEPARATOR, $segments);
+ }
+
+ public function hideSectionByFeatureFlag(string $sectionPath, string $featureFlag): void
{
$options = $this->getOption(SectionVisibilityFilter::OPTION_FEATURE_FLAG_SECTIONS_TO_HIDE, []);
- $options[$section] = array_merge(
- $options[$section] ?? [],
+ $options[$sectionPath] = array_merge(
+ $options[$sectionPath] ?? [],
[
$featureFlag
]
@@ -93,6 +95,18 @@ public function hideSectionByFeatureFlag(string $section, string $featureFlag):
$this->setOption(SectionVisibilityFilter::OPTION_FEATURE_FLAG_SECTIONS_TO_HIDE, $options);
}
+ public function showSectionByFeatureFlag(string $sectionPath, string $featureFlag): void
+ {
+ $options = $this->getOption(SectionVisibilityFilter::OPTION_FEATURE_FLAG_SECTIONS, []);
+ $options[$sectionPath] = array_merge(
+ $options[$sectionPath] ?? [],
+ [
+ $featureFlag
+ ]
+ );
+ $this->setOption(SectionVisibilityFilter::OPTION_FEATURE_FLAG_SECTIONS, $options);
+ }
+
private function getFeatureFlagChecker(): FeatureFlagCheckerInterface
{
return $this->getServiceManager()->getContainer()->get(FeatureFlagChecker::class);
diff --git a/models/classes/menu/SectionVisibilityFilterInterface.php b/models/classes/menu/SectionVisibilityFilterInterface.php
index bf7c86b035..77e0f3357f 100644
--- a/models/classes/menu/SectionVisibilityFilterInterface.php
+++ b/models/classes/menu/SectionVisibilityFilterInterface.php
@@ -24,5 +24,5 @@
interface SectionVisibilityFilterInterface
{
- public function isVisible(string $section): bool;
+ public function isVisible(string $sectionPath): bool;
}
diff --git a/models/ontology/tao.rdf b/models/ontology/tao.rdf
index 912b1cba41..9b933bd752 100644
--- a/models/ontology/tao.rdf
+++ b/models/ontology/tao.rdf
@@ -17,6 +17,14 @@
+
+
+
+
+
+
+
+
@@ -340,4 +348,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/unit/models/classes/Translation/Command/CreateTranslationCommandTest.php b/test/unit/models/classes/Translation/Command/CreateTranslationCommandTest.php
new file mode 100644
index 0000000000..fcd44e4335
--- /dev/null
+++ b/test/unit/models/classes/Translation/Command/CreateTranslationCommandTest.php
@@ -0,0 +1,44 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace oat\tao\test\unit\model\Translation\Command;
+
+use oat\tao\model\Translation\Command\CreateTranslationCommand;
+use PHPUnit\Framework\TestCase;
+
+class CreateTranslationCommandTest extends TestCase
+{
+ public function testConstructorAndGetters(): void
+ {
+ $resourceType = 'http://www.tao.lu/Ontologies/TAO.rdf#SomeType';
+ $uniqueId = 'id1';
+ $languageUri = 'http://www.tao.lu/Ontologies/TAO.rdf#Langpt-BR';
+
+ $command = new CreateTranslationCommand($resourceType, $uniqueId, $languageUri);
+
+ $this->assertSame($resourceType, $command->getResourceType());
+ $this->assertSame($uniqueId, $command->getUniqueId());
+ $this->assertSame($languageUri, $command->getLanguageUri());
+ }
+}
diff --git a/test/unit/models/classes/Translation/Command/UpdateTranslationCommandTest.php b/test/unit/models/classes/Translation/Command/UpdateTranslationCommandTest.php
new file mode 100644
index 0000000000..42deec4a4f
--- /dev/null
+++ b/test/unit/models/classes/Translation/Command/UpdateTranslationCommandTest.php
@@ -0,0 +1,45 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace oat\tao\test\unit\model\Translation\Command;
+
+use oat\tao\model\TaoOntology;
+use oat\tao\model\Translation\Command\UpdateTranslationCommand;
+use PHPUnit\Framework\TestCase;
+
+class UpdateTranslationCommandTest extends TestCase
+{
+ public function testConstructorAndGetters(): void
+ {
+ $resourceUri = 'http://example.com/resource/1';
+
+ $command = new UpdateTranslationCommand(
+ $resourceUri,
+ TaoOntology::PROPERTY_VALUE_TRANSLATION_PROGRESS_TRANSLATED
+ );
+
+ $this->assertSame($resourceUri, $command->getResourceUri());
+ $this->assertSame(TaoOntology::PROPERTY_VALUE_TRANSLATION_PROGRESS_TRANSLATED, $command->getProgressUri());
+ }
+}
diff --git a/test/unit/models/classes/Translation/Entity/ResourceCollectionTest.php b/test/unit/models/classes/Translation/Entity/ResourceCollectionTest.php
new file mode 100644
index 0000000000..27fe187f78
--- /dev/null
+++ b/test/unit/models/classes/Translation/Entity/ResourceCollectionTest.php
@@ -0,0 +1,48 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace oat\tao\test\unit\model\Translation\Entity;
+
+use oat\tao\model\Translation\Entity\ResourceCollection;
+use PHPUnit\Framework\TestCase;
+
+class ResourceCollectionTest extends TestCase
+{
+ private ResourceCollection $sut;
+
+ public function setUp(): void
+ {
+ $this->sut = new ResourceCollection(...[]);
+ }
+
+ public function testGetters(): void
+ {
+ $this->assertSame(
+ [
+ 'resources' => []
+ ],
+ $this->sut->jsonSerialize()
+ );
+ }
+}
diff --git a/test/unit/models/classes/Translation/Entity/ResourceTranslatableTest.php b/test/unit/models/classes/Translation/Entity/ResourceTranslatableTest.php
new file mode 100644
index 0000000000..505559a6e6
--- /dev/null
+++ b/test/unit/models/classes/Translation/Entity/ResourceTranslatableTest.php
@@ -0,0 +1,70 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace oat\tao\test\unit\model\Translation\Entity;
+
+use oat\tao\model\TaoOntology;
+use oat\tao\model\Translation\Entity\ResourceTranslatable;
+use PHPUnit\Framework\TestCase;
+
+class ResourceTranslatableTest extends TestCase
+{
+ private ResourceTranslatable $sut;
+
+ public function setUp(): void
+ {
+ $this->sut = new ResourceTranslatable('resourceUri', 'resourceLabel');
+ $this->sut->addMetadata(TaoOntology::PROPERTY_LANGUAGE, 'languageUri', 'en-US');
+ $this->sut->addMetadata(TaoOntology::PROPERTY_UNIQUE_IDENTIFIER, 'abc123', null);
+ $this->sut->addMetadata(TaoOntology::PROPERTY_TRANSLATION_STATUS, 'statusUri', null);
+ }
+
+ public function testGetters(): void
+ {
+ $this->assertSame('en-US', $this->sut->getLanguageCode());
+ $this->assertSame('languageUri', $this->sut->getLanguageUri());
+ $this->assertSame('statusUri', $this->sut->getStatusUri());
+ $this->assertSame(
+ [
+ 'resourceUri' => 'resourceUri',
+ 'resourceLabel' => 'resourceLabel',
+ 'metadata' => [
+ TaoOntology::PROPERTY_LANGUAGE => [
+ 'value' => 'languageUri',
+ 'literal' => 'en-US',
+ ],
+ TaoOntology::PROPERTY_UNIQUE_IDENTIFIER => [
+ 'value' => 'abc123',
+ 'literal' => null,
+ ],
+ TaoOntology::PROPERTY_TRANSLATION_STATUS => [
+ 'value' => 'statusUri',
+ 'literal' => null,
+ ]
+ ],
+ ],
+ $this->sut->jsonSerialize()
+ );
+ }
+}
diff --git a/test/unit/models/classes/Translation/Entity/ResourceTranslationTest.php b/test/unit/models/classes/Translation/Entity/ResourceTranslationTest.php
new file mode 100644
index 0000000000..51cb86dec4
--- /dev/null
+++ b/test/unit/models/classes/Translation/Entity/ResourceTranslationTest.php
@@ -0,0 +1,72 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace oat\tao\test\unit\model\Translation\Entity;
+
+use oat\tao\model\TaoOntology;
+use oat\tao\model\Translation\Entity\ResourceTranslation;
+use PHPUnit\Framework\TestCase;
+
+class ResourceTranslationTest extends TestCase
+{
+ private ResourceTranslation $sut;
+
+ public function setUp(): void
+ {
+ $this->sut = new ResourceTranslation('resourceUri', 'resourceLabel');
+ $this->sut->setOriginResourceUri('originResourceUri');
+ $this->sut->addMetadata(TaoOntology::PROPERTY_LANGUAGE, 'languageUri', 'en-US');
+ $this->sut->addMetadata(TaoOntology::PROPERTY_UNIQUE_IDENTIFIER, 'abc123', null);
+ $this->sut->addMetadata(TaoOntology::PROPERTY_TRANSLATION_PROGRESS, 'progressUri', null);
+ }
+
+ public function testGetters(): void
+ {
+ $this->assertSame('en-US', $this->sut->getLanguageCode());
+ $this->assertSame('languageUri', $this->sut->getLanguageUri());
+ $this->assertSame('progressUri', $this->sut->getProgressUri());
+ $this->assertSame(
+ [
+ 'originResourceUri' => 'originResourceUri',
+ 'resourceUri' => 'resourceUri',
+ 'resourceLabel' => 'resourceLabel',
+ 'metadata' => [
+ TaoOntology::PROPERTY_LANGUAGE => [
+ 'value' => 'languageUri',
+ 'literal' => 'en-US',
+ ],
+ TaoOntology::PROPERTY_UNIQUE_IDENTIFIER => [
+ 'value' => 'abc123',
+ 'literal' => null,
+ ],
+ TaoOntology::PROPERTY_TRANSLATION_PROGRESS => [
+ 'value' => 'progressUri',
+ 'literal' => null,
+ ]
+ ],
+ ],
+ $this->sut->jsonSerialize()
+ );
+ }
+}
diff --git a/test/unit/models/classes/Translation/Factory/ResourceTranslatableFactoryTest.php b/test/unit/models/classes/Translation/Factory/ResourceTranslatableFactoryTest.php
new file mode 100644
index 0000000000..81e507fd2d
--- /dev/null
+++ b/test/unit/models/classes/Translation/Factory/ResourceTranslatableFactoryTest.php
@@ -0,0 +1,71 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace oat\tao\test\unit\model\Translation\Factory;
+
+use core_kernel_classes_Resource;
+use oat\tao\model\Translation\Entity\ResourceTranslatable;
+use oat\tao\model\Translation\Factory\ResourceTranslatableFactory;
+use oat\tao\model\Translation\Service\ResourceMetadataPopulateService;
+use oat\tao\model\TaoOntology;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class ResourceTranslatableFactoryTest extends TestCase
+{
+ private ResourceTranslatableFactory $sut;
+
+ /** @var ResourceMetadataPopulateService|MockObject */
+ private $metadataPopulateService;
+
+ /** @var core_kernel_classes_Resource|MockObject */
+ private $originResource;
+
+ protected function setUp(): void
+ {
+ $this->metadataPopulateService = $this->createMock(ResourceMetadataPopulateService::class);
+ $this->originResource = $this->createMock(core_kernel_classes_Resource::class);
+ $this->sut = new ResourceTranslatableFactory($this->metadataPopulateService);
+ }
+
+ public function testCreateReturnsResourceTranslatable(): void
+ {
+ $uri = 'http://example.com/resource';
+ $label = 'Test Resource';
+
+ $this->originResource->method('getUri')->willReturn($uri);
+ $this->originResource->method('getLabel')->willReturn($label);
+
+ $this->metadataPopulateService
+ ->expects($this->once())
+ ->method('populate');
+
+ $resource = $this->sut->create($this->originResource);
+
+ $this->assertInstanceOf(ResourceTranslatable::class, $resource);
+ $this->assertEquals($uri, $resource->getResourceUri());
+ $this->assertEquals($label, $resource->getResourceLabel());
+ $this->assertContains(TaoOntology::PROPERTY_TRANSLATION_STATUS, $resource->getMetadataUris());
+ }
+}
diff --git a/test/unit/models/classes/Translation/Factory/ResourceTranslationFactoryTest.php b/test/unit/models/classes/Translation/Factory/ResourceTranslationFactoryTest.php
new file mode 100644
index 0000000000..dfbc4123ad
--- /dev/null
+++ b/test/unit/models/classes/Translation/Factory/ResourceTranslationFactoryTest.php
@@ -0,0 +1,79 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace oat\tao\test\unit\model\Translation\Factory;
+
+use core_kernel_classes_Resource;
+use oat\tao\model\Translation\Entity\ResourceTranslation;
+use oat\tao\model\Translation\Entity\ResourceTranslatable;
+use oat\tao\model\Translation\Factory\ResourceTranslationFactory;
+use oat\tao\model\Translation\Service\ResourceMetadataPopulateService;
+use oat\tao\model\TaoOntology;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class ResourceTranslationFactoryTest extends TestCase
+{
+ private ResourceTranslationFactory $sut;
+
+ /** @var ResourceMetadataPopulateService|MockObject */
+ private $metadataPopulateService;
+
+ /** @var ResourceTranslatable|MockObject */
+ private $resourceTranslatable;
+
+ /** @var core_kernel_classes_Resource|MockObject */
+ private $translationResource;
+
+ protected function setUp(): void
+ {
+ $this->metadataPopulateService = $this->createMock(ResourceMetadataPopulateService::class);
+ $this->resourceTranslatable = $this->createMock(ResourceTranslatable::class);
+ $this->translationResource = $this->createMock(core_kernel_classes_Resource::class);
+ $this->sut = new ResourceTranslationFactory($this->metadataPopulateService);
+ }
+
+ public function testCreateReturnsResourceTranslation(): void
+ {
+ $originUri = 'http://example.com/origin';
+ $translationUri = 'http://example.com/translation';
+ $label = 'Test Resource';
+
+ $this->resourceTranslatable->method('getResourceUri')->willReturn($originUri);
+ $this->translationResource->method('getUri')->willReturn($translationUri);
+ $this->translationResource->method('getLabel')->willReturn($label);
+
+ $this->metadataPopulateService
+ ->expects($this->once())
+ ->method('populate');
+
+ $resource = $this->sut->create($this->resourceTranslatable, $this->translationResource);
+
+ $this->assertInstanceOf(ResourceTranslation::class, $resource);
+ $this->assertEquals($translationUri, $resource->getResourceUri());
+ $this->assertEquals($label, $resource->getResourceLabel());
+ $this->assertEquals($originUri, $resource->getOriginResourceUri());
+ $this->assertContains(TaoOntology::PROPERTY_TRANSLATION_PROGRESS, $resource->getMetadataUris());
+ }
+}
diff --git a/test/unit/models/classes/Translation/Form/Modifier/TranslationFormModifierTest.php b/test/unit/models/classes/Translation/Form/Modifier/TranslationFormModifierTest.php
new file mode 100644
index 0000000000..4ec2adbf80
--- /dev/null
+++ b/test/unit/models/classes/Translation/Form/Modifier/TranslationFormModifierTest.php
@@ -0,0 +1,157 @@
+form = $this->createMock(tao_helpers_form_Form::class);
+ $this->featureFlagChecker = $this->createMock(FeatureFlagCheckerInterface::class);
+ $this->sut = new TranslationFormModifier($this->featureFlagChecker);
+ }
+
+ public function testModifyTranslationDisabled(): void
+ {
+ $this->featureFlagChecker
+ ->expects($this->once())
+ ->method('isEnabled')
+ ->with('FEATURE_TRANSLATION_ENABLED')
+ ->willReturn(false);
+
+ $this->form
+ ->expects($this->exactly(5))
+ ->method('removeElement')
+ ->withConsecutive(
+ [tao_helpers_Uri::encode(TaoOntology::PROPERTY_UNIQUE_IDENTIFIER)],
+ [tao_helpers_Uri::encode(TaoOntology::PROPERTY_LANGUAGE)],
+ [tao_helpers_Uri::encode(TaoOntology::PROPERTY_TRANSLATION_TYPE)],
+ [tao_helpers_Uri::encode(TaoOntology::PROPERTY_TRANSLATION_STATUS)],
+ [tao_helpers_Uri::encode(TaoOntology::PROPERTY_TRANSLATION_PROGRESS)],
+ );
+
+ $this->sut->modify($this->form);
+ }
+
+ /**
+ * @dataProvider modifyTranslationEnabledDataProvider
+ */
+ public function testModifyTranslationEnabled(bool $developerMode, ?string $type, array $removeElements): void
+ {
+ $this->featureFlagChecker
+ ->expects($this->exactly(2))
+ ->method('isEnabled')
+ ->withConsecutive(
+ ['FEATURE_TRANSLATION_ENABLED'],
+ ['FEATURE_TRANSLATION_DEVELOPER_MODE'],
+ )
+ ->willReturnOnConsecutiveCalls(true, $developerMode);
+
+ $this->form
+ ->expects($this->once())
+ ->method('getValue')
+ ->with(tao_helpers_Uri::encode(TaoOntology::PROPERTY_TRANSLATION_TYPE))
+ ->willReturn($type);
+
+ $this->form
+ ->expects($this->exactly(count($removeElements)))
+ ->method('removeElement')
+ ->withConsecutive(
+ ...array_map(
+ static fn ($element) => [tao_helpers_Uri::encode($element)],
+ $removeElements
+ )
+ );
+
+ $this->sut->modify($this->form);
+ }
+
+ private function modifyTranslationEnabledDataProvider(): array
+ {
+ return [
+ 'Developer Mode enabled and no type provided' => [
+ 'developerMode' => true,
+ 'type' => null,
+ 'removeElements' => [
+ TaoOntology::PROPERTY_TRANSLATION_PROGRESS,
+ TaoOntology::PROPERTY_TRANSLATION_STATUS,
+ ],
+ ],
+ 'Developer Mode enabled and type original' => [
+ 'developerMode' => true,
+ 'type' => TaoOntology::PROPERTY_VALUE_TRANSLATION_TYPE_ORIGINAL,
+ 'removeElements' => [
+ TaoOntology::PROPERTY_TRANSLATION_PROGRESS,
+ ],
+ ],
+ 'Developer Mode enabled and type translation' => [
+ 'developerMode' => true,
+ 'type' => TaoOntology::PROPERTY_VALUE_TRANSLATION_TYPE_TRANSLATION,
+ 'removeElements' => [
+ TaoOntology::PROPERTY_TRANSLATION_STATUS,
+ ],
+ ],
+ 'Developer Mode disabled and no type provided' => [
+ 'developerMode' => false,
+ 'type' => null,
+ 'removeElements' => [
+ TaoOntology::PROPERTY_TRANSLATION_TYPE,
+ TaoOntology::PROPERTY_TRANSLATION_PROGRESS,
+ TaoOntology::PROPERTY_TRANSLATION_STATUS,
+ ],
+ ],
+ 'Developer Mode disabled and type original' => [
+ 'developerMode' => false,
+ 'type' => TaoOntology::PROPERTY_VALUE_TRANSLATION_TYPE_ORIGINAL,
+ 'removeElements' => [
+ TaoOntology::PROPERTY_TRANSLATION_TYPE,
+ TaoOntology::PROPERTY_TRANSLATION_PROGRESS,
+ ],
+ ],
+ 'Developer Mode disabled and type translation' => [
+ 'developerMode' => false,
+ 'type' => TaoOntology::PROPERTY_VALUE_TRANSLATION_TYPE_TRANSLATION,
+ 'removeElements' => [
+ TaoOntology::PROPERTY_TRANSLATION_TYPE,
+ TaoOntology::PROPERTY_TRANSLATION_STATUS,
+ ],
+ ],
+ ];
+ }
+}
diff --git a/test/unit/models/classes/Translation/Query/ResourceTranslatableQueryTest.php b/test/unit/models/classes/Translation/Query/ResourceTranslatableQueryTest.php
new file mode 100644
index 0000000000..c719536262
--- /dev/null
+++ b/test/unit/models/classes/Translation/Query/ResourceTranslatableQueryTest.php
@@ -0,0 +1,42 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace oat\tao\test\unit\model\Translation\Query;
+
+use oat\tao\model\Translation\Query\ResourceTranslatableQuery;
+use PHPUnit\Framework\TestCase;
+
+class ResourceTranslatableQueryTest extends TestCase
+{
+ public function testConstructorAndGetters(): void
+ {
+ $resourceType = 'http://www.tao.lu/Ontologies/TAO.rdf#AssessmentContentObject';
+ $uniqueIds = ['id1', 'id2'];
+
+ $query = new ResourceTranslatableQuery($resourceType, $uniqueIds);
+
+ $this->assertSame($resourceType, $query->getResourceType());
+ $this->assertSame($uniqueIds, $query->getUniqueIds());
+ }
+}
diff --git a/test/unit/models/classes/Translation/Query/ResourceTranslationQueryTest.php b/test/unit/models/classes/Translation/Query/ResourceTranslationQueryTest.php
new file mode 100644
index 0000000000..9df704613e
--- /dev/null
+++ b/test/unit/models/classes/Translation/Query/ResourceTranslationQueryTest.php
@@ -0,0 +1,54 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace oat\tao\test\unit\model\Translation\Query;
+
+use oat\tao\model\TaoOntology;
+use oat\tao\model\Translation\Query\ResourceTranslationQuery;
+use PHPUnit\Framework\TestCase;
+
+class ResourceTranslationQueryTest extends TestCase
+{
+ public function testConstructorAndGetters(): void
+ {
+ $resourceType = 'http://www.tao.lu/Ontologies/TAO.rdf#AssessmentContentObject';
+ $uniqueId = 'id1';
+ $languageUri = TaoOntology::PROPERTY_LANGUAGE;
+
+ $query = new ResourceTranslationQuery($resourceType, [$uniqueId], $languageUri);
+
+ $this->assertSame($resourceType, $query->getResourceType());
+ $this->assertSame([$uniqueId], $query->getUniqueIds());
+ $this->assertSame($languageUri, $query->getLanguageUri());
+ }
+
+ public function testConstructorWithoutLanguageUri(): void
+ {
+ $query = new ResourceTranslationQuery(
+ 'http://www.tao.lu/Ontologies/TAO.rdf#AssessmentContentObject',
+ ['id1']
+ );
+ $this->assertNull($query->getLanguageUri());
+ }
+}
diff --git a/test/unit/models/classes/Translation/Repository/ResourceTranslatableRepositoryTest.php b/test/unit/models/classes/Translation/Repository/ResourceTranslatableRepositoryTest.php
new file mode 100644
index 0000000000..a6ce9ba37e
--- /dev/null
+++ b/test/unit/models/classes/Translation/Repository/ResourceTranslatableRepositoryTest.php
@@ -0,0 +1,158 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace oat\tao\test\unit\model\Translation\Repository;
+
+use core_kernel_classes_Resource;
+use core_kernel_classes_Class;
+use oat\generis\model\data\Ontology;
+use oat\generis\model\kernel\persistence\smoothsql\search\ComplexSearchService;
+use oat\search\QueryBuilder;
+use oat\search\base\QueryInterface;
+use oat\search\base\SearchGateWayInterface;
+use oat\tao\model\Translation\Entity\ResourceCollection;
+use oat\tao\model\Translation\Entity\ResourceTranslatable;
+use oat\tao\model\Translation\Factory\ResourceTranslatableFactory;
+use oat\tao\model\Translation\Query\ResourceTranslatableQuery;
+use oat\tao\model\Translation\Repository\ResourceTranslatableRepository;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class ResourceTranslatableRepositoryTest extends TestCase
+{
+ private ResourceTranslatableRepository $sut;
+
+ /** @var Ontology|MockObject */
+ private $ontology;
+
+ /** @var ComplexSearchService|MockObject */
+ private $complexSearch;
+
+ /** @var ResourceTranslatableFactory|MockObject */
+ private $factory;
+
+ /** @var ResourceTranslatableQuery|MockObject */
+ private $query;
+
+ /** @var QueryBuilder|MockObject */
+ private $queryBuilder;
+
+ /** @var QueryInterface|MockObject */
+ private $searchQuery;
+
+ /** @var SearchGateWayInterface|MockObject */
+ private $gateway;
+
+ /** @var core_kernel_classes_Class|MockObject */
+ private $resourceTypeClass;
+
+ protected function setUp(): void
+ {
+ $this->ontology = $this->createMock(Ontology::class);
+ $this->complexSearch = $this->createMock(ComplexSearchService::class);
+ $this->factory = $this->createMock(ResourceTranslatableFactory::class);
+ $this->query = $this->createMock(ResourceTranslatableQuery::class);
+ $this->queryBuilder = $this->createMock(QueryBuilder::class);
+ $this->searchQuery = $this->createMock(QueryInterface::class);
+ $this->gateway = $this->createMock(SearchGateWayInterface::class);
+ $this->resourceTypeClass = $this->createMock(core_kernel_classes_Class::class);
+
+ $this->sut = new ResourceTranslatableRepository(
+ $this->ontology,
+ $this->complexSearch,
+ $this->factory
+ );
+ }
+
+ public function testFindReturnsResourceCollection(): void
+ {
+ $resourceType = 'http://www.tao.lu/Ontologies/TAO.rdf#AssessmentContentObject';
+ $uniqueIds = [
+ 'id1',
+ 'id2'
+ ];
+ $resource1 = $this->createMock(core_kernel_classes_Resource::class);
+ $resource2 = $this->createMock(core_kernel_classes_Resource::class);
+ $translatable1 = $this->createMock(ResourceTranslatable::class);
+ $translatable2 = $this->createMock(ResourceTranslatable::class);
+
+ $this->query
+ ->method('getResourceType')
+ ->willReturn($resourceType);
+ $this->query
+ ->method('getUniqueIds')
+ ->willReturn($uniqueIds);
+
+ $this->complexSearch
+ ->method('query')
+ ->willReturn($this->queryBuilder);
+ $this->complexSearch
+ ->method('searchType')
+ ->willReturn($this->searchQuery);
+ $this->complexSearch
+ ->method('getGateway')
+ ->willReturn($this->gateway);
+
+ $this->gateway
+ ->expects($this->once())
+ ->method('search')
+ ->with($this->queryBuilder)
+ ->willReturn(
+ [
+ $resource1,
+ $resource2
+ ]
+ );
+
+ $this->ontology
+ ->method('getClass')
+ ->with($resourceType)
+ ->willReturn($this->resourceTypeClass);
+
+ $resource1
+ ->method('isInstanceOf')
+ ->with($this->resourceTypeClass)
+ ->willReturn(true);
+ $resource2
+ ->method('isInstanceOf')
+ ->with($this->resourceTypeClass)
+ ->willReturn(true);
+
+ $this->factory
+ ->method('create')
+ ->willReturnMap(
+ [
+ [$resource1, $translatable1],
+ [$resource2, $translatable2]
+ ]
+ );
+
+ $result = $this->sut->find($this->query);
+
+ $this->assertInstanceOf(ResourceCollection::class, $result);
+ $this->assertCount(2, $result);
+ $this->assertContains($translatable1, $result);
+ $this->assertContains($translatable2, $result);
+ }
+}
diff --git a/test/unit/models/classes/Translation/Repository/ResourceTranslationRepositoryTest.php b/test/unit/models/classes/Translation/Repository/ResourceTranslationRepositoryTest.php
new file mode 100644
index 0000000000..37041b261c
--- /dev/null
+++ b/test/unit/models/classes/Translation/Repository/ResourceTranslationRepositoryTest.php
@@ -0,0 +1,163 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace oat\tao\test\unit\model\Translation\Repository;
+
+use core_kernel_classes_Class;
+use core_kernel_classes_Resource;
+use Exception;
+use oat\generis\model\data\Ontology;
+use oat\generis\model\kernel\persistence\smoothsql\search\ComplexSearchService;
+use oat\search\QueryBuilder;
+use oat\search\base\QueryInterface;
+use oat\search\base\SearchGateWayInterface;
+use oat\tao\model\Translation\Entity\ResourceCollection;
+use oat\tao\model\Translation\Entity\ResourceTranslatable;
+use oat\tao\model\Translation\Entity\ResourceTranslation;
+use oat\tao\model\Translation\Factory\ResourceTranslationFactory;
+use oat\tao\model\Translation\Query\ResourceTranslatableQuery;
+use oat\tao\model\Translation\Query\ResourceTranslationQuery;
+use oat\tao\model\Translation\Repository\ResourceTranslatableRepository;
+use oat\tao\model\Translation\Repository\ResourceTranslationRepository;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+
+class ResourceTranslationRepositoryTest extends TestCase
+{
+ private ResourceTranslationRepository $sut;
+
+ /** @var Ontology|MockObject */
+ private $ontology;
+
+ /** @var ComplexSearchService|MockObject */
+ private $complexSearch;
+
+ /** @var ResourceTranslatableRepository|MockObject */
+ private $resourceTranslatableRepository;
+
+ /** @var ResourceTranslationFactory|MockObject */
+ private $factory;
+
+ /** @var LoggerInterface|MockObject */
+ private $logger;
+
+ /** @var QueryBuilder|MockObject */
+ private $queryBuilder;
+
+ /** @var QueryInterface|MockObject */
+ private $searchQuery;
+
+ /** @var SearchGateWayInterface|MockObject */
+ private $gateway;
+
+ /** @var core_kernel_classes_Class|MockObject */
+ private $resourceTypeClass;
+
+ protected function setUp(): void
+ {
+ $this->ontology = $this->createMock(Ontology::class);
+ $this->complexSearch = $this->createMock(ComplexSearchService::class);
+ $this->resourceTranslatableRepository = $this->createMock(ResourceTranslatableRepository::class);
+ $this->factory = $this->createMock(ResourceTranslationFactory::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->queryBuilder = $this->createMock(QueryBuilder::class);
+ $this->searchQuery = $this->createMock(QueryInterface::class);
+ $this->gateway = $this->createMock(SearchGateWayInterface::class);
+ $this->resourceTypeClass = $this->createMock(core_kernel_classes_Class::class);
+
+ $this->sut = new ResourceTranslationRepository(
+ $this->ontology,
+ $this->complexSearch,
+ $this->resourceTranslatableRepository,
+ $this->factory,
+ $this->logger
+ );
+ }
+
+ public function testFindReturnsResourceCollection(): void
+ {
+ $resourceType = 'http://www.tao.lu/Ontologies/TAO.rdf#AssessmentContentObject';
+ $uniqueIds = ['id1'];
+ $translatable1 = $this->createMock(ResourceTranslatable::class);
+ $translationResource1 = $this->createMock(core_kernel_classes_Resource::class);
+ $translation1 = $this->createMock(ResourceTranslation::class);
+
+ $query = $this->createMock(ResourceTranslationQuery::class);
+ $query
+ ->method('getResourceType')
+ ->willReturn($resourceType);
+ $query
+ ->method('getUniqueIds')
+ ->willReturn($uniqueIds);
+
+ $this->resourceTranslatableRepository
+ ->method('find')
+ ->willReturn(new ResourceCollection($translatable1));
+
+ $this->complexSearch
+ ->method('query')
+ ->willReturn($this->queryBuilder);
+ $this->complexSearch
+ ->method('searchType')
+ ->willReturn($this->searchQuery);
+ $this->complexSearch
+ ->method('getGateway')
+ ->willReturn($this->gateway);
+
+ $this->gateway
+ ->expects($this->once())
+ ->method('search')
+ ->with($this->queryBuilder)
+ ->willReturn(
+ [
+ $translationResource1
+ ]
+ );
+
+ $this->ontology
+ ->method('getClass')
+ ->with($resourceType)
+ ->willReturn($this->resourceTypeClass);
+
+ $translationResource1
+ ->method('isInstanceOf')
+ ->with($this->resourceTypeClass)
+ ->willReturn(true);
+
+ $this->factory
+ ->method('create')
+ ->willReturnMap(
+ [
+ [$translatable1, $translationResource1, $translation1]
+ ]
+ );
+
+ $result = $this->sut->find($query);
+
+ $this->assertInstanceOf(ResourceCollection::class, $result);
+ $this->assertCount(1, $result);
+ $this->assertContains($translation1, $result);
+ }
+}
diff --git a/test/unit/models/classes/Translation/Service/ResourceMetadataPopulateServiceTest.php b/test/unit/models/classes/Translation/Service/ResourceMetadataPopulateServiceTest.php
new file mode 100644
index 0000000000..d90db1b6e8
--- /dev/null
+++ b/test/unit/models/classes/Translation/Service/ResourceMetadataPopulateServiceTest.php
@@ -0,0 +1,117 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace oat\tao\test\unit\model\Translation\Service;
+
+use core_kernel_classes_Literal;
+use core_kernel_classes_Property;
+use core_kernel_classes_Resource;
+use oat\generis\model\data\Ontology;
+use oat\generis\model\OntologyRdf;
+use oat\tao\model\Translation\Entity\AbstractResource;
+use oat\tao\model\Translation\Service\ResourceMetadataPopulateService;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+
+class ResourceMetadataPopulateServiceTest extends TestCase
+{
+ private ResourceMetadataPopulateService $sut;
+
+ /** @var Ontology|MockObject */
+ private $ontology;
+
+ /** @var AbstractResource|MockObject */
+ private $resource;
+
+ /** @var core_kernel_classes_Resource|MockObject */
+ private $originResource;
+
+ public function setUp(): void
+ {
+ $this->ontology = $this->createMock(Ontology::class);
+ $this->resource = $this->createMock(AbstractResource::class);
+ $this->originResource = $this->createMock(core_kernel_classes_Resource::class);
+ $this->sut = new ResourceMetadataPopulateService($this->ontology);
+ }
+
+ public function testPopulateAddsMetadataToResource(): void
+ {
+ $resourceType = 'testResourceType';
+ $metadataUri = 'http://example.com/metadata';
+ $valuePropertyUri = OntologyRdf::RDF_VALUE;
+ $value = 'testValue';
+ $literalValue = 'testLiteral';
+ $valueProperty = $this->createMock(core_kernel_classes_Property::class);
+ $literal = $this->createMock(core_kernel_classes_Literal::class);
+ $literal->literal = $literalValue;
+
+ $this->sut->addMetadata($resourceType, $metadataUri);
+
+ $this->originResource
+ ->method('getParentClassesIds')
+ ->willReturn([$resourceType]);
+
+ $this->originResource
+ ->method('getPropertyValues')
+ ->willReturn([$value]);
+
+ $this->resource
+ ->expects($this->once())
+ ->method('addMetadataUri')
+ ->with($metadataUri);
+
+ $this->ontology->method('getProperty')->willReturnMap(
+ [
+ [
+ $metadataUri,
+ $valueProperty
+ ],
+ [
+ $value,
+ $valueProperty
+ ],
+ [
+ $valuePropertyUri,
+ $valueProperty
+ ],
+ ]
+ );
+
+ $valueProperty
+ ->method('getOnePropertyValue')
+ ->willReturn($literal);
+
+ $this->resource
+ ->expects($this->once())
+ ->method('addMetadata')
+ ->with($metadataUri, $value, $literalValue);
+
+ $this->resource
+ ->expects($this->once())
+ ->method('getMetadataUris')
+ ->willReturn([$metadataUri]);
+
+ $this->sut->populate($this->resource, $this->originResource);
+ }
+}
diff --git a/test/unit/models/classes/Translation/Service/ResourceTranslatableRetrieverTest.php b/test/unit/models/classes/Translation/Service/ResourceTranslatableRetrieverTest.php
new file mode 100644
index 0000000000..cbf322b201
--- /dev/null
+++ b/test/unit/models/classes/Translation/Service/ResourceTranslatableRetrieverTest.php
@@ -0,0 +1,156 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace oat\tao\test\unit\model\Translation\Service;
+
+use core_kernel_classes_Property;
+use core_kernel_classes_Resource;
+use oat\generis\model\data\Ontology;
+use oat\tao\model\TaoOntology;
+use oat\tao\model\Translation\Entity\ResourceCollection;
+use oat\tao\model\Translation\Exception\ResourceTranslationException;
+use oat\tao\model\Translation\Query\ResourceTranslatableQuery;
+use oat\tao\model\Translation\Repository\ResourceTranslatableRepository;
+use oat\tao\model\Translation\Service\ResourceTranslatableRetriever;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Http\Message\ServerRequestInterface;
+
+class ResourceTranslatableRetrieverTest extends TestCase
+{
+ private ResourceTranslatableRetriever $sut;
+
+ /** @var ResourceTranslatableRepository|MockObject */
+ private $resourceTranslatableRepository;
+
+ /** @var Ontology|MockObject */
+ private $ontology;
+
+ /** @var MockObject|ServerRequestInterface */
+ private $request;
+
+ public function setUp(): void
+ {
+ $this->ontology = $this->createMock(Ontology::class);
+ $this->resourceTranslatableRepository = $this->createMock(ResourceTranslatableRepository::class);
+ $this->request = $this->createMock(ServerRequestInterface::class);
+ $this->sut = new ResourceTranslatableRetriever($this->ontology, $this->resourceTranslatableRepository);
+ }
+
+ public function testGetByRequest(): void
+ {
+ $this->request->expects($this->once())
+ ->method('getQueryParams')
+ ->willReturn(
+ [
+ 'id' => ['id'],
+ ]
+ );
+
+ $this->mockResource([TaoOntology::CLASS_URI_ITEM], 'uniqueId');
+
+ $result = new ResourceCollection();
+
+ $this->resourceTranslatableRepository
+ ->expects($this->once())
+ ->method('find')
+ ->with(
+ new ResourceTranslatableQuery(
+ TaoOntology::CLASS_URI_ITEM,
+ ['uniqueId']
+ )
+ )
+ ->willReturn($result);
+
+ $this->assertSame($result, $this->sut->getByRequest($this->request));
+ }
+
+ public function testGetByRequestRequiresResourceId(): void
+ {
+ $this->request->expects($this->once())
+ ->method('getQueryParams')
+ ->willReturn([]);
+
+ $this->expectException(ResourceTranslationException::class);
+ $this->expectExceptionMessage('Resource id is required');
+
+ $this->sut->getByRequest($this->request);
+ }
+
+ public function testGetByRequestRequiresResourceType(): void
+ {
+ $this->request->expects($this->once())
+ ->method('getQueryParams')
+ ->willReturn(['id' => 'id']);
+
+ $this->mockResource([], 'uniqueId');
+
+ $this->expectException(ResourceTranslationException::class);
+ $this->expectExceptionMessage('Resource id must have a resource type');
+
+ $this->sut->getByRequest($this->request);
+ }
+
+ public function testGetByRequestRequiresUniqueId(): void
+ {
+ $this->request->expects($this->once())
+ ->method('getQueryParams')
+ ->willReturn(
+ [
+ 'id' => 'id',
+ ]
+ );
+
+ $this->mockResource([TaoOntology::CLASS_URI_ITEM], '');
+
+ $this->expectException(ResourceTranslationException::class);
+ $this->expectExceptionMessage('Resource id must have a unique identifier');
+
+ $this->sut->getByRequest($this->request);
+ }
+
+ private function mockResource(array $classIds, string $uniqueId): core_kernel_classes_Resource
+ {
+ $resource = $this->createMock(core_kernel_classes_Resource::class);
+
+ $resource
+ ->method('getParentClassesIds')
+ ->willReturn($classIds);
+
+ $resource
+ ->method('getUniquePropertyValue')
+ ->willReturn($uniqueId);
+
+ $this->ontology
+ ->expects($this->once())
+ ->method('getResource')
+ ->willReturn($resource);
+
+ $this->ontology
+ ->method('getProperty')
+ ->willReturn($this->createMock(core_kernel_classes_Property::class));
+
+ return $resource;
+ }
+}
diff --git a/test/unit/models/classes/Translation/Service/ResourceTranslationRetrieverTest.php b/test/unit/models/classes/Translation/Service/ResourceTranslationRetrieverTest.php
new file mode 100644
index 0000000000..a86b716bd0
--- /dev/null
+++ b/test/unit/models/classes/Translation/Service/ResourceTranslationRetrieverTest.php
@@ -0,0 +1,158 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace oat\tao\test\unit\model\Translation\Service;
+
+use core_kernel_classes_Property;
+use core_kernel_classes_Resource;
+use InvalidArgumentException;
+use oat\generis\model\data\Ontology;
+use oat\tao\model\TaoOntology;
+use oat\tao\model\Translation\Entity\ResourceCollection;
+use oat\tao\model\Translation\Exception\ResourceTranslationException;
+use oat\tao\model\Translation\Query\ResourceTranslationQuery;
+use oat\tao\model\Translation\Repository\ResourceTranslationRepository;
+use oat\tao\model\Translation\Service\ResourceTranslationRetriever;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Http\Message\ServerRequestInterface;
+
+class ResourceTranslationStatusServiceTest extends TestCase
+{
+ private ResourceTranslationRetriever $sut;
+
+ /** @var Ontology|MockObject */
+ private $ontology;
+
+ /** @var ResourceTranslationRepository|MockObject */
+ private $resourceTranslationRepository;
+
+ /** @var MockObject|ServerRequestInterface */
+ private $request;
+
+ public function setUp(): void
+ {
+ $this->ontology = $this->createMock(Ontology::class);
+ $this->resourceTranslationRepository = $this->createMock(ResourceTranslationRepository::class);
+ $this->request = $this->createMock(ServerRequestInterface::class);
+ $this->sut = new ResourceTranslationRetriever($this->ontology, $this->resourceTranslationRepository);
+ }
+
+ public function testGetByRequest(): void
+ {
+ $this->request->expects($this->once())
+ ->method('getQueryParams')
+ ->willReturn(
+ [
+ 'id' => 'id',
+ ]
+ );
+
+ $this->mockResource([TaoOntology::CLASS_URI_ITEM], 'uniqueId');
+
+ $result = new ResourceCollection();
+
+ $this->resourceTranslationRepository
+ ->expects($this->once())
+ ->method('find')
+ ->with(
+ new ResourceTranslationQuery(
+ TaoOntology::CLASS_URI_ITEM,
+ ['uniqueId'],
+ null
+ )
+ )
+ ->willReturn($result);
+
+ $this->assertSame($result, $this->sut->getByRequest($this->request));
+ }
+
+ public function testGetByRequestRequiresResourceId(): void
+ {
+ $this->request->expects($this->once())
+ ->method('getQueryParams')
+ ->willReturn([]);
+
+ $this->expectException(ResourceTranslationException::class);
+ $this->expectExceptionMessage('Resource id is required');
+
+ $this->sut->getByRequest($this->request);
+ }
+
+ public function testGetByRequestRequiresResourceType(): void
+ {
+ $this->request->expects($this->once())
+ ->method('getQueryParams')
+ ->willReturn(['id' => 'id']);
+
+ $this->mockResource([], 'uniqueId');
+
+ $this->expectException(ResourceTranslationException::class);
+ $this->expectExceptionMessage('Resource id must have a resource type');
+
+ $this->sut->getByRequest($this->request);
+ }
+
+ public function testGetByRequestRequiresUniqueId(): void
+ {
+ $this->request->expects($this->once())
+ ->method('getQueryParams')
+ ->willReturn(
+ [
+ 'id' => 'id',
+ ]
+ );
+
+ $this->mockResource([TaoOntology::CLASS_URI_ITEM], '');
+
+ $this->expectException(ResourceTranslationException::class);
+ $this->expectExceptionMessage('Resource id must have a unique identifier');
+
+ $this->sut->getByRequest($this->request);
+ }
+
+ private function mockResource(array $classIds, string $uniqueId): core_kernel_classes_Resource
+ {
+ $resource = $this->createMock(core_kernel_classes_Resource::class);
+
+ $resource
+ ->method('getParentClassesIds')
+ ->willReturn($classIds);
+
+ $resource
+ ->method('getUniquePropertyValue')
+ ->willReturn($uniqueId);
+
+ $this->ontology
+ ->expects($this->once())
+ ->method('getResource')
+ ->willReturn($resource);
+
+ $this->ontology
+ ->method('getProperty')
+ ->willReturn($this->createMock(core_kernel_classes_Property::class));
+
+ return $resource;
+ }
+}
diff --git a/test/unit/models/classes/Translation/Service/TranslationCreationServiceTest.php b/test/unit/models/classes/Translation/Service/TranslationCreationServiceTest.php
new file mode 100644
index 0000000000..a759f9facc
--- /dev/null
+++ b/test/unit/models/classes/Translation/Service/TranslationCreationServiceTest.php
@@ -0,0 +1,320 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace oat\tao\test\unit\models\classes\Translation\Service;
+
+use core_kernel_classes_Property;
+use core_kernel_classes_Resource;
+use oat\tao\model\Language\Business\Contract\LanguageRepositoryInterface;
+use oat\tao\model\Language\Language;
+use oat\tao\model\Language\LanguageCollection;
+use oat\tao\model\OntologyClassService;
+use oat\tao\model\TaoOntology;
+use oat\tao\model\Translation\Entity\ResourceTranslation;
+use oat\tao\model\Translation\Exception\ResourceTranslationException;
+use oat\tao\model\Translation\Service\TranslationCreationService;
+use oat\tao\model\Translation\Command\CreateTranslationCommand;
+use oat\tao\model\Translation\Repository\ResourceTranslatableRepository;
+use oat\tao\model\Translation\Repository\ResourceTranslationRepository;
+use oat\tao\model\Translation\Entity\ResourceCollection;
+use oat\tao\model\Translation\Entity\ResourceTranslatable;
+use oat\generis\model\data\Ontology;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+
+class TranslationCreationServiceTest extends TestCase
+{
+ private TranslationCreationService $service;
+
+ /** @var Ontology|MockObject */
+ private $ontology;
+
+ /** @var ResourceTranslatableRepository|MockObject */
+ private $resourceTranslatableRepository;
+
+ /** @var ResourceTranslationRepository|MockObject */
+ private $resourceTranslationRepository;
+
+ /** @var LanguageRepositoryInterface|MockObject */
+ private $languageRepository;
+
+ /** @var OntologyClassService|MockObject */
+ private $ontologyClassService;
+
+ /** @var MockObject|LoggerInterface */
+ private $logger;
+
+ protected function setUp(): void
+ {
+ $this->ontology = $this->createMock(Ontology::class);
+ $this->resourceTranslatableRepository = $this->createMock(ResourceTranslatableRepository::class);
+ $this->resourceTranslationRepository = $this->createMock(ResourceTranslationRepository::class);
+ $this->languageRepository = $this->createMock(LanguageRepositoryInterface::class);
+ $this->ontologyClassService = $this->createMock(OntologyClassService::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+
+ $this->service = new TranslationCreationService(
+ $this->ontology,
+ $this->resourceTranslatableRepository,
+ $this->resourceTranslationRepository,
+ $this->languageRepository,
+ $this->logger,
+ );
+
+ $this->service->setOntologyClassService(
+ TaoOntology::CLASS_URI_ITEM,
+ $this->ontologyClassService
+ );
+ }
+
+ public function testCreate(): void
+ {
+ $resourceType = TaoOntology::CLASS_URI_ITEM;
+ $uniqueId = 'id1';
+ $languageUri = 'http://www.tao.lu/Ontologies/TAO.rdf#Langpt-BR';
+
+ $translatable = $this->createMock(ResourceTranslatable::class);
+
+ $languageProperty = $this->createMock(core_kernel_classes_Property::class);
+ $translationTypeProperty = $this->createMock(core_kernel_classes_Property::class);
+ $translationProgressProperty = $this->createMock(core_kernel_classes_Property::class);
+
+ $resourceCollection = new ResourceCollection();
+ $translatableCollection = new ResourceCollection($translatable);
+
+ $translatable
+ ->method('isReadyForTranslation')
+ ->willReturn(true);
+
+ $language = $this->createMock(Language::class);
+ $language
+ ->expects($this->exactly(3))
+ ->method('getUri')
+ ->willReturn($languageUri);
+ $language
+ ->expects($this->once())
+ ->method('getCode')
+ ->willReturn('pt-BR');
+
+ $instance = $this->createMock(core_kernel_classes_Resource::class);
+ $instance
+ ->expects($this->once())
+ ->method('getTypes')
+ ->willReturn([]);
+ $instance
+ ->expects($this->once())
+ ->method('getLabel')
+ ->willReturn('MyInstance');
+
+ $clonedInstance = $this->createMock(core_kernel_classes_Resource::class);
+ $clonedInstance
+ ->expects($this->once())
+ ->method('setLabel')
+ ->with('MyInstance (pt-BR)');
+ $clonedInstance
+ ->expects($this->exactly(3))
+ ->method('editPropertyValues')
+ ->withConsecutive(
+ [
+ $languageProperty,
+ $languageUri
+ ],
+ [
+ $translationTypeProperty,
+ TaoOntology::PROPERTY_VALUE_TRANSLATION_TYPE_TRANSLATION
+ ],
+ [
+ $translationProgressProperty,
+ TaoOntology::PROPERTY_VALUE_TRANSLATION_PROGRESS_PENDING
+ ]
+ );
+
+ $this->resourceTranslationRepository
+ ->expects($this->once())
+ ->method('find')
+ ->willReturn($resourceCollection);
+
+ $this->resourceTranslatableRepository
+ ->expects($this->once())
+ ->method('find')
+ ->willReturn($translatableCollection);
+
+ $this->languageRepository
+ ->expects($this->once())
+ ->method('findAvailableLanguagesByUsage')
+ ->willReturn(new LanguageCollection($language));
+
+ $this->ontology
+ ->expects($this->once())
+ ->method('getResource')
+ ->willReturn($instance);
+
+ $this->ontology
+ ->expects($this->exactly(3))
+ ->method('getProperty')
+ ->willReturnMap(
+ [
+ [
+ TaoOntology::PROPERTY_LANGUAGE,
+ $languageProperty
+ ],
+ [
+ TaoOntology::PROPERTY_TRANSLATION_TYPE,
+ $translationTypeProperty
+ ],
+ [
+ TaoOntology::PROPERTY_TRANSLATION_PROGRESS,
+ $translationProgressProperty
+ ]
+ ]
+ );
+
+ $this->ontologyClassService
+ ->expects($this->once())
+ ->method('cloneInstance')
+ ->willReturn($clonedInstance);
+
+ $this->assertInstanceOf(
+ core_kernel_classes_Resource::class,
+ $this->service->create(new CreateTranslationCommand($resourceType, $uniqueId, $languageUri))
+ );
+ }
+
+ public function testCreateWillFailIfTranslationAlreadyExists(): void
+ {
+ $translatable = $this->createMock(ResourceTranslatable::class);
+
+ $translatable
+ ->method('isReadyForTranslation')
+ ->willReturn(true);
+
+ $this->resourceTranslationRepository
+ ->expects($this->once())
+ ->method('find')
+ ->willReturn(new ResourceCollection($translatable));
+
+ $this->expectException(ResourceTranslationException::class);
+ $this->expectExceptionMessage(
+ 'Translation already exists for [uniqueId=id1, locale=http://www.tao.lu/Ontologies/TAO.rdf#Langpt-BR]'
+ );
+
+ $this->service->create(
+ new CreateTranslationCommand(
+ TaoOntology::CLASS_URI_ITEM,
+ 'id1',
+ 'http://www.tao.lu/Ontologies/TAO.rdf#Langpt-BR'
+ )
+ );
+ }
+
+ public function testCreateWillFailIfResourceDoesNotExist(): void
+ {
+ $this->resourceTranslationRepository
+ ->expects($this->once())
+ ->method('find')
+ ->willReturn(new ResourceCollection());
+
+ $this->resourceTranslatableRepository
+ ->expects($this->once())
+ ->method('find')
+ ->willReturn(new ResourceCollection());
+
+ $this->expectException(ResourceTranslationException::class);
+ $this->expectExceptionMessage('There is not translatable resource for [uniqueId=id1]');
+
+ $this->service->create(
+ new CreateTranslationCommand(
+ TaoOntology::CLASS_URI_ITEM,
+ 'id1',
+ 'http://www.tao.lu/Ontologies/TAO.rdf#Langpt-BR'
+ )
+ );
+ }
+
+ public function testCreateWillFailIfLanguageDoesNotExist(): void
+ {
+ $translatable = $this->createMock(ResourceTranslatable::class);
+
+ $translatable
+ ->method('isReadyForTranslation')
+ ->willReturn(true);
+
+ $this->resourceTranslationRepository
+ ->expects($this->once())
+ ->method('find')
+ ->willReturn(new ResourceCollection());
+
+ $this->resourceTranslatableRepository
+ ->expects($this->once())
+ ->method('find')
+ ->willReturn(new ResourceCollection($translatable));
+
+ $this->languageRepository
+ ->expects($this->once())
+ ->method('findAvailableLanguagesByUsage')
+ ->willReturn(new LanguageCollection());
+
+ $this->expectException(ResourceTranslationException::class);
+ $this->expectExceptionMessage('Language http://www.tao.lu/Ontologies/TAO.rdf#Langpt-BR does not exist');
+
+ $this->service->create(
+ new CreateTranslationCommand(
+ TaoOntology::CLASS_URI_ITEM,
+ 'id1',
+ 'http://www.tao.lu/Ontologies/TAO.rdf#Langpt-BR'
+ )
+ );
+ }
+
+ public function testCreateWillFailIfResourceIsNotReadyForTranslation(): void
+ {
+ $translatable = $this->createMock(ResourceTranslatable::class);
+
+ $translatable
+ ->method('isReadyForTranslation')
+ ->willReturn(false);
+
+ $this->resourceTranslationRepository
+ ->expects($this->once())
+ ->method('find')
+ ->willReturn(new ResourceCollection());
+
+ $this->resourceTranslatableRepository
+ ->expects($this->once())
+ ->method('find')
+ ->willReturn(new ResourceCollection($translatable));
+
+ $this->expectException(ResourceTranslationException::class);
+ $this->expectExceptionMessage('Resource [uniqueId=id1] is not ready for translation');
+
+ $this->service->create(
+ new CreateTranslationCommand(
+ TaoOntology::CLASS_URI_ITEM,
+ 'id1',
+ 'http://www.tao.lu/Ontologies/TAO.rdf#Langpt-BR'
+ )
+ );
+ }
+}
diff --git a/test/unit/models/classes/Translation/Service/TranslationUpdateServiceTest.php b/test/unit/models/classes/Translation/Service/TranslationUpdateServiceTest.php
new file mode 100644
index 0000000000..fecc72c129
--- /dev/null
+++ b/test/unit/models/classes/Translation/Service/TranslationUpdateServiceTest.php
@@ -0,0 +1,155 @@
+
+ */
+
+declare(strict_types=1);
+
+namespace oat\tao\test\unit\models\classes\Translation\Service;
+
+use core_kernel_classes_Property;
+use core_kernel_classes_Resource;
+use oat\generis\model\data\Ontology;
+use oat\tao\model\TaoOntology;
+use oat\tao\model\Translation\Command\UpdateTranslationCommand;
+use oat\tao\model\Translation\Exception\ResourceTranslationException;
+use oat\tao\model\Translation\Service\TranslationUpdateService;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+
+class TranslationUpdateServiceTest extends TestCase
+{
+ private TranslationUpdateService $service;
+
+ /** @var Ontology|MockObject */
+ private $ontology;
+
+ /** @var LoggerInterface|MockObject */
+ private $logger;
+
+ protected function setUp(): void
+ {
+ $this->ontology = $this->createMock(Ontology::class);
+ $this->logger = $this->createMock(LoggerInterface::class);
+ $this->service = new TranslationUpdateService($this->ontology, $this->logger);
+ }
+
+ public function testUpdateSuccess(): void
+ {
+ $typeProperty = $this->createMock(core_kernel_classes_Property::class);
+ $progressProperty = $this->createMock(core_kernel_classes_Property::class);
+
+ $translationType = $this->createMock(core_kernel_classes_Resource::class);
+ $translationType
+ ->method('getUri')
+ ->willReturn(TaoOntology::PROPERTY_VALUE_TRANSLATION_TYPE_TRANSLATION);
+
+ $resource = $this->createMock(core_kernel_classes_Resource::class);
+ $resource
+ ->method('getOnePropertyValue')
+ ->willReturn($translationType);
+
+ $resource
+ ->method('exists')
+ ->willReturn(true);
+
+ $resource
+ ->expects($this->once())
+ ->method('editPropertyValues')
+ ->with(
+ $progressProperty,
+ TaoOntology::PROPERTY_VALUE_TRANSLATION_PROGRESS_TRANSLATED
+ );
+
+ $this->ontology
+ ->method('getResource')
+ ->willReturn($resource);
+
+ $this->ontology
+ ->method('getProperty')
+ ->willReturnMap(
+ [
+ [
+ TaoOntology::PROPERTY_TRANSLATION_TYPE,
+ $typeProperty
+ ],
+ [
+ TaoOntology::PROPERTY_TRANSLATION_PROGRESS,
+ $progressProperty
+ ],
+ ]
+ );
+
+ $this->assertSame(
+ $resource,
+ $this->service->update(
+ new UpdateTranslationCommand(
+ 'http://example.com/resource/1',
+ TaoOntology::PROPERTY_VALUE_TRANSLATION_PROGRESS_TRANSLATED
+ )
+ )
+ );
+ }
+
+ public function testOriginalCannotBeUpdated(): void
+ {
+ $typeProperty = $this->createMock(core_kernel_classes_Property::class);
+
+ $translationType = $this->createMock(core_kernel_classes_Resource::class);
+ $translationType
+ ->method('getUri')
+ ->willReturn(TaoOntology::PROPERTY_VALUE_TRANSLATION_TYPE_ORIGINAL);
+
+ $resource = $this->createMock(core_kernel_classes_Resource::class);
+ $resource
+ ->method('getOnePropertyValue')
+ ->willReturn($translationType);
+
+ $resource
+ ->method('exists')
+ ->willReturn(true);
+
+ $this->ontology
+ ->method('getResource')
+ ->willReturn($resource);
+
+ $this->ontology
+ ->method('getProperty')
+ ->willReturnMap(
+ [
+ [
+ TaoOntology::PROPERTY_TRANSLATION_TYPE,
+ $typeProperty
+ ]
+ ]
+ );
+
+ $this->expectException(ResourceTranslationException::class);
+ $this->expectExceptionMessage('Resource http://example.com/resource/1 is not a translation');
+
+ $this->service->update(
+ new UpdateTranslationCommand(
+ 'http://example.com/resource/1',
+ TaoOntology::PROPERTY_VALUE_TRANSLATION_PROGRESS_TRANSLATED
+ )
+ );
+ }
+}