From 621daf0c7d12cb3576d884623a2d4d6a9ab9b196 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Thu, 9 Apr 2026 11:01:20 +0200 Subject: [PATCH 01/66] chore(foundation): introduce export/import foundation components This commit was created from various other commits across multiple branches. These were combined to simplify the merge process and to separate the foundation from the implementations in the Questions and Test components. --- .../class.ilTestQuestionPoolSetupAgent.php | 6 + .../src/ExportImport/Foundation/Builder.php | 230 ++++++++++++++++ .../Foundation/Contracts/DataCollector.php | 35 +++ .../Foundation/Contracts/Deserializer.php | 53 ++++ .../Foundation/Contracts/Envelope.php | 46 ++++ .../Foundation/Contracts/ImportStage.php | 54 ++++ .../Foundation/Contracts/Normalizable.php | 62 +++++ .../Foundation/Contracts/Normalizer.php | 57 ++++ .../Foundation/Contracts/Pipe.php | 39 +++ .../Foundation/Contracts/Pipeline.php | 98 +++++++ .../Foundation/Contracts/Serializer.php | 78 ++++++ .../Foundation/Contracts/Transformations.php | 105 ++++++++ .../ExportImport/Foundation/ExportContext.php | 108 ++++++++ .../Foundation/Importing/ImportContext.php | 51 ++++ .../Importing/ImportSessionRepository.php | 84 ++++++ .../Importing/ImportStageRunner.php | 122 +++++++++ .../Foundation/Importing/StageResult.php | 76 ++++++ .../Foundation/Importing/StageResultType.php | 47 ++++ .../Normalizing/Attributes/Normalizes.php | 44 ++++ .../Attributes/NormalizesLegacy.php | 44 ++++ .../Foundation/Normalizing/Envelopes/Id.php | 99 +++++++ .../Normalizer/DateTimeNormalizer.php | 58 +++++ .../Normalizer/EnvelopeNormalizer.php | 63 +++++ .../Normalizer/IlObjectNormalizer.php | 155 +++++++++++ .../Normalizer/Legacy11UUIDNormalizer.php | 57 ++++ .../Normalizing/Normalizer/Registry.php | 133 ++++++++++ .../Normalizer/TransformationNormalizer.php | 63 +++++ .../Normalizing/Normalizer/UUIDNormalizer.php | 61 +++++ .../Normalizing/NormalizingException.php | 37 +++ .../Normalizing/Pipes/DenormalizeCarry.php | 47 ++++ .../Normalizing/Pipes/DenormalizingPipe.php | 62 +++++ .../Normalizing/Pipes/FinalizeNormalizing.php | 56 ++++ .../Normalizing/Pipes/IdMappingPipe.php | 83 ++++++ .../Normalizing/Pipes/NormalizeCarry.php | 45 ++++ .../Normalizing/Pipes/NormalizingPipe.php | 66 +++++ .../Normalizing/Transformations.php | 151 +++++++++++ .../src/ExportImport/Foundation/Pipeline.php | 200 ++++++++++++++ .../Serializing/JSONMemoryDeserializer.php | 70 +++++ .../Serializing/JSONMemorySerializer.php | 113 ++++++++ .../Serializing/SimpleXMLDeserializer.php | 246 ++++++++++++++++++ .../Serializing/SimpleXMLSerializer.php | 186 +++++++++++++ .../Setup/NormalizerArtifactObjective.php | 80 ++++++ 42 files changed, 3570 insertions(+) create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Builder.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/DataCollector.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Deserializer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Envelope.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ImportStage.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Normalizable.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Normalizer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Pipe.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Pipeline.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Serializer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Transformations.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/ExportContext.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportContext.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportSessionRepository.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/StageResult.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/StageResultType.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/Normalizes.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/NormalizesLegacy.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Envelopes/Id.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/DateTimeNormalizer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/EnvelopeNormalizer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Legacy11UUIDNormalizer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Registry.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/TransformationNormalizer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/UUIDNormalizer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/NormalizingException.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizeCarry.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizingPipe.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/FinalizeNormalizing.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/IdMappingPipe.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizeCarry.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizingPipe.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Transformations.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Pipeline.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemoryDeserializer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemorySerializer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLDeserializer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Setup/NormalizerArtifactObjective.php diff --git a/components/ILIAS/TestQuestionPool/classes/Setup/class.ilTestQuestionPoolSetupAgent.php b/components/ILIAS/TestQuestionPool/classes/Setup/class.ilTestQuestionPoolSetupAgent.php index 7a18db41545c..d3e34d84d37b 100755 --- a/components/ILIAS/TestQuestionPool/classes/Setup/class.ilTestQuestionPoolSetupAgent.php +++ b/components/ILIAS/TestQuestionPool/classes/Setup/class.ilTestQuestionPoolSetupAgent.php @@ -20,6 +20,7 @@ use ILIAS\Setup\Objective; use ILIAS\Setup\ObjectiveCollection; use ILIAS\Setup\Metrics; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Setup\NormalizerArtifactObjective; class ilTestQuestionPoolSetupAgent extends NullAgent { @@ -60,4 +61,9 @@ public function getMigrations(): array ]; } + public function getBuildObjective(): Objective + { + return new NormalizerArtifactObjective(); + } + } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Builder.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Builder.php new file mode 100644 index 000000000000..ddab7b63f94c --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Builder.php @@ -0,0 +1,230 @@ + $prepend_pipes */ + private array $prepend_pipes = []; + + /** @var list $append_pipes */ + private array $append_pipes = []; + + /** @var list $containers */ + private array $containers = []; + + public function __construct( + private readonly ILIASContainer $dic, + Container ...$local_containers, + ) { + $this->containers = $local_containers; + } + + /* + Fluent interface methods + */ + + public function withDefaultNormalizers(bool $enable = true): self + { + $clone = clone $this; + $clone->default_normalizers = $enable; + return $clone; + } + + public function withLegacyNormalizers(string $version): self + { + $clone = clone $this; + $clone->legacy_version = $version; + return $clone; + } + + /** + * @param list $append + * @param list $prepend + */ + public function withAdditionalPipes(array $prepend = [], array $append = []): self + { + $clone = clone $this; + $clone->append_pipes += $append; + $clone->prepend_pipes += $prepend; + return $clone; + } + + /* + Object creation + */ + + /** + * Create a Transformations instance which was configured by the builder. + */ + + public function create(): TransformationsContract + { + $pipeline = new Pipeline(); + $object = new Transformations( + $this->dic->refinery(), + $pipeline + ); + + foreach ($this->prepend_pipes as $pipe) { + $pipeline->pipe($pipe); + } + + if ($this->legacy_version !== null) { + // TODO: Implement semver comparison here? Use ILIAS\Data\Version class? + + $registry = $this->buildRegistry($object, $this->legacy_version); + $pipeline->pipe(new NormalizingPipe($registry)); + $pipeline->pipe(new DenormalizingPipe($registry)); + } + + if ($this->default_normalizers) { + $registry = $this->buildRegistry($object); + $pipeline->pipe(new NormalizingPipe($registry)); + $pipeline->pipe(new DenormalizingPipe($registry)); + } + + foreach ($this->append_pipes as $pipe) { + $pipeline->pipe($pipe); + } + + $pipeline->pipe(new FinalizeNormalizing()); + + return $object; + } + + /** + * Register all normalizer classes from the type map artifact by checking for the given version and skipping if + * the normalizer is already registered. + */ + private function buildRegistry(Transformations $object, string $version = NormalizerArtifactObjective::DEFAULT_KEY): Registry + { + $type_map = require NormalizerArtifactObjective::PATH(); + $registry = new Registry(); + + foreach ($type_map as $type => $normalizer_classes) { + if (!isset($normalizer_classes[$version]) || $registry->hasNormalizer($type)) { + continue; + } + + $registry->registerNormalizer( + $type, + fn() => $this->createInstance($normalizer_classes[$version], $object) + ); + } + + return $registry; + } + + /* + Factory & Autowiring + */ + + /** + * Create an instance of a class by resolving the constructor arguments. + * + * @template T of object + * + * @param class-string $class_name + * @param Transformations $transformations + * @return T + */ + private function createInstance(string $class_name, Transformations $transformations): object + { + $reflection_class = new ReflectionClass($class_name); + $constructor = $reflection_class->getConstructor(); + + if ($constructor === null || $constructor->getNumberOfParameters() === 0) { + return $reflection_class->newInstance(); + } + + $arguments = []; + foreach ($constructor->getParameters() as $parameter) { + $arguments[] = $this->resolveConstructorArgument( + $parameter, + $transformations, + $class_name + ); + } + + return $reflection_class->newInstanceArgs($arguments); + } + + /** + * Resolve a constructor argument by trying to resolve it from the global ilias container, the local container or + * the default value. + * + * @throws RuntimeException if the argument cannot be resolved + */ + private function resolveConstructorArgument( + ReflectionParameter $parameter, + Transformations $transformations, + string $class_name + ) { + if ($parameter->isDefaultValueAvailable()) { + return $parameter->getDefaultValue(); + } + + $type = $parameter->getType(); + if ($type instanceof ReflectionNamedType && $type->allowsNull()) { + return null; + } + + + if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $type_name = $type->getName(); + + if ($type_name === TransformationsContract::class || in_array(TransformationsContract::class, class_implements($type_name))) { + return $transformations; + } + + foreach ([$this->dic, ...$this->containers] as $container) { + if ($type_name === get_class($container)) { + return $container; + } + } + } + + $name = $parameter->getName(); + throw new RuntimeException("Unable to resolve constructor parameter \${$name} for class {$class_name}."); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/DataCollector.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/DataCollector.php new file mode 100644 index 000000000000..b23c8eb65133 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/DataCollector.php @@ -0,0 +1,35 @@ + Language-neutral normalized representation. + */ + public function toArray(Transformations $tt): array; + + /** + * Reconstruct an envelope instance from its normalized array representation. + * + * @param array $value Normalized envelope payload. + * @param Transformations $tt Transformation helpers used for value casting and mapping. + * @return static Reconstructed envelope instance. + */ + public static function fromArray(array $value, Transformations $tt): static; +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ImportStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ImportStage.php new file mode 100644 index 000000000000..2dcb1fa9ff3d --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ImportStage.php @@ -0,0 +1,54 @@ + + * + * @template TValue + * @template TNormalized of null|scalar|NormalizedArray + */ +interface Normalizer +{ + /** + * Converts a value into a normalized form. + * + * @param TValue $value + * @return TNormalized + * + * @throws NormalizingException if the normalizer does not support the given type + */ + public function normalize($value): array|float|bool|int|string|null; + + /** + * Converts a normalized form back into a value. It uses the type hint to determine the expected type, which will be + * returned. + * + * @param TNormalized $normalized + * @param class-string $type + * @return TValue + * + * @throws NormalizingException if the normalizer does not support the given type + */ + public function denormalize(array|float|bool|int|string|null $normalized, string $type); +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Pipe.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Pipe.php new file mode 100644 index 000000000000..92b68b1a4e49 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Pipe.php @@ -0,0 +1,39 @@ + $pipes + */ + public function through(array $pipes): self; + + /** + * Push additional pipe onto the pipeline. + * + * @param PipeParam $pipes + */ + public function pipe(\Closure|Pipe $pipe): self; + + /** + * Push additional pipe onto the pipeline which will be executed when the condition is met. + * + * @param \Closure(TPassable): bool $condition + * @param PipeParam $pipe + */ + public function pipeWhen(\Closure $condition, \Closure|Pipe $pipe): self; + + /** + * Push additional pipe onto the pipeline which will be executed when the condition is not met. + * + * @param \Closure(TPassable): bool $condition + * @param PipeParam $pipe + */ + public function pipeUnless(\Closure $condition, \Closure|Pipe $pipe): self; + + /** + * Run the pipeline with a final destination callback. + * + * @param \Closure(TPassable): TPassable $destination + * @return TPassable|mixed + */ + public function then(\Closure $destination): mixed; + + /** + * Run the pipeline and return the result. + * + * @return TPassable + */ + public function thenReturn(): mixed; + + /** + * Set a final callback to be executed after the pipeline ends regardless of the outcome. + * + * @param \Closure(TPassable): TPassable $callback + */ + public function finally(\Closure $callback): self; + + /** + * Get the array of pipes. + * + * @return list + */ + public function pipes(): array; +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Serializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Serializer.php new file mode 100644 index 000000000000..71873e44e722 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Serializer.php @@ -0,0 +1,78 @@ +|T $expected + * @return T|null + * + * @throws NormalizingException if the type is not supported + */ + public function denormalize(array|float|bool|int|string|null $normalized, string|object $expected): mixed; + + /* + Transformations + */ + + /** + * Returns the pipe of the given class from the pipeline. + * + * @template T of Pipe + * @param class-string $pipe_class + * @return T + * + * @throws \InvalidArgumentException if the pipe is not found + */ + public function context(string $pipe_class): Pipe; + + /** + * Returns a group of transformations that can be used to create custom transformations. + */ + public function custom(): Group; + + /** + * @throws \InvalidArgumentException if the value cannot be transformed into an integer + */ + public function int(mixed $value): int; + + /** + * @throws \InvalidArgumentException if the value cannot be transformed into a float + */ + public function float(mixed $value): float; + + /** + * @throws \InvalidArgumentException if the value cannot be transformed into a string + */ + public function string(mixed $value): string; + + /** + * @throws \InvalidArgumentException if the value cannot be transformed into a boolean + */ + public function bool(mixed $value): bool; + + public function nullableInt(mixed $value): ?int; + + public function nullableFloat(mixed $value): ?float; + + public function nullableString(mixed $value): ?string; + + public function nullableBool(mixed $value): ?bool; +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/ExportContext.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/ExportContext.php new file mode 100644 index 000000000000..b5e38b014d9f --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/ExportContext.php @@ -0,0 +1,108 @@ +}> $dependencies + */ + private array $dependencies = []; + + public function __construct( + private ObjectId $pool_id, + private ExportConfig $config, + private Transformations $transformations, + ) { + } + + public function getPoolId(): ObjectId + { + return $this->pool_id; + } + + public function getConfig(): ExportConfig + { + return $this->config; + } + + public function getTransformations(): Transformations + { + return $this->transformations; + } + + public function getSerializer(): Serializer + { + if ($this->serializer === null) { + throw new RuntimeException( + 'Serializer not set. This may happen if the exporter steps are not executed in the correct order.' + ); + } + + return $this->serializer; + } + + public function setSerializer(Serializer $serializer): void + { + $this->serializer = $serializer; + } + + public function getDependencies(): array + { + return array_values($this->dependencies); + } + + public function addDependency(string $component, string $entity, array $ids): void + { + $key = "{$component}::{$entity}"; + + if (!isset($this->dependencies[$key])) { + $this->dependencies[$key] = [ + 'component' => $component, + 'entity' => $entity, + 'ids' => [], + ]; + } + + $this->dependencies[$key]['ids'] = array_values(array_unique(array_merge( + $this->dependencies[$key]['ids'], + $ids + ))); + } + + public function getContent(): string + { + return $this->getSerializer()->write(); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportContext.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportContext.php new file mode 100644 index 000000000000..01b35fbae0b0 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportContext.php @@ -0,0 +1,51 @@ + $data */ + public function __construct( + private array $data = [], + ) { + } + + public function with(string $key, mixed $value): self + { + $clone = clone $this; + $clone->data[$key] = $value; + return $clone; + } + + public function get(string $key, mixed $default = null): mixed + { + return $this->data[$key] ?? $default; + } + + public function has(string $key): bool + { + return array_key_exists($key, $this->data); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportSessionRepository.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportSessionRepository.php new file mode 100644 index 000000000000..f7eda4318390 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportSessionRepository.php @@ -0,0 +1,84 @@ +key_index = "{$base}index"; + $this->key_context = "{$base}context"; + } + + public function getCurrentStageIndex(): int + { + if (ilSession::has($this->key_index)) { + return (int) ilSession::get($this->key_index); + } + + return 0; + } + + public function setCurrentStageIndex(int $index): void + { + ilSession::set($this->key_index, $index); + } + + public function getContext(): ImportContext + { + if (ilSession::has($this->key_context)) { + return unserialize( + ilSession::get($this->key_context), + ['allowed_classes' => [ImportContext::class]] + ); + } + + return new ImportContext([]); + } + + public function setContext(ImportContext $context): void + { + ilSession::set($this->key_context, serialize($context)); + } + + public function clear(): void + { + if (ilSession::has($this->key_index)) { + ilSession::clear($this->key_index); + } + if (ilSession::has($this->key_context)) { + ilSession::clear($this->key_context); + } + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php new file mode 100644 index 000000000000..c31a51d505b2 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php @@ -0,0 +1,122 @@ + $stages */ + public function __construct( + private readonly array $stages, + private readonly ImportSessionRepository $session, + ) { + } + + /** + * Run the import stage runner. It manages the state of the import process and delegates to the current stage. + * It will return a StageResult that indicates the next action to take. + */ + public function run(ServerRequestInterface $request): StageResult + { + $index = $this->session->getCurrentStageIndex(); + $context = $this->session->getContext(); + + if ($index >= count($this->stages)) { + return StageResult::complete($context); + } + + $stage = $this->stages[$index]; + $result = $stage->process($context, $request); + + switch ($result->type) { + case StageResultType::ADVANCE: + $next_index = $index + 1; + $this->session->setContext($result->context); + $this->session->setCurrentStageIndex($next_index); + + if ($next_index >= count($this->stages)) { + return StageResult::complete($result->context); + } + + return $result; + + case StageResultType::ERROR: + case StageResultType::INTERACT: + $this->session->setContext($result->context); + return $result; + + case StageResultType::COMPLETE: + $this->session->clear(); + return $result; + } + + throw new \LogicException("Invalid stage result type: {$result->type->name}"); + } + + /** + * Build the workflow UI component for the import process. + */ + public function buildWorkflow(UIFactory $ui, string $title): Linear + { + $steps = []; + $active_index = $this->session->getCurrentStageIndex(); + + foreach ($this->stages as $i => $stage) { + $step = $ui->listing()->workflow()->step( + $stage->getLabel(), + $stage->getDescription() + ); + + if ($i < $active_index) { + $step = $step->withStatus(Step::SUCCESSFULLY) + ->withAvailability(Step::NOT_ANYMORE); + } elseif ($i === $active_index) { + $step = $step->withStatus(Step::IN_PROGRESS) + ->withAvailability(Step::AVAILABLE); + } else { + $step = $step->withStatus(Step::NOT_STARTED) + ->withAvailability(Step::NOT_AVAILABLE); + } + + $steps[] = $step; + } + + return $ui->listing()->workflow()->linear($title, $steps) + ->withActive($active_index); + } + + /** + * Reset the import stage session. + */ + public function reset(): void + { + $this->session->clear(); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/StageResult.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/StageResult.php new file mode 100644 index 000000000000..5e1dfa1d9a71 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/StageResult.php @@ -0,0 +1,76 @@ + $components + */ + private function __construct( + public readonly StageResultType $type, + public readonly ImportContext $context, + public readonly array $components = [], + public readonly ?string $error_message = null, + ) { + } + + /** + * Create a result that indicates the process should interrupt to display the given UI components. + * + * @param list $components + */ + public static function interact(ImportContext $context, array $components): self + { + return new self(StageResultType::INTERACT, $context, $components); + } + + /** + * Create a result that indicates the process should advance to the next stage. + */ + public static function advance(ImportContext $context): self + { + return new self(StageResultType::ADVANCE, $context); + } + + /** + * Create a result that indicates the process should fail with the given error message. + */ + public static function error(ImportContext $context, string $message): self + { + return new self(StageResultType::ERROR, $context, [], $message); + } + + /** + * Create a result that indicates the process should complete successfully. + */ + public static function complete(ImportContext $context): self + { + return new self(StageResultType::COMPLETE, $context); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/StageResultType.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/StageResultType.php new file mode 100644 index 000000000000..55e42ea50fa3 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/StageResultType.php @@ -0,0 +1,47 @@ +types = $types; + } + + /** @var list */ + public array $types; +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/NormalizesLegacy.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/NormalizesLegacy.php new file mode 100644 index 000000000000..c1ddb635de5f --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/NormalizesLegacy.php @@ -0,0 +1,44 @@ +types = $types; + } + + /** @var list */ + public array $types; +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Envelopes/Id.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Envelopes/Id.php new file mode 100644 index 000000000000..1fb2a36186f0 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Envelopes/Id.php @@ -0,0 +1,99 @@ +id; + } + + public function getObject(): string + { + return $this->object; + } + + /** + * @inheritDoc + */ + public function toArray(Transformations $tt): array + { + if (is_object($this->id)) { + return [ + 'id' => $tt->normalize($this->id), + 'type' => get_class($this->id), + 'object' => $this->object, + ]; + } else { + return [ + 'id' => (string) $this->id, + 'type' => gettype($this->id), + 'object' => $this->object, + ]; + } + } + + /** + * @inheritDoc + */ + public static function fromArray(array $value, Transformations $tt): static + { + $raw_id = $value['id']; + $type = $value['type']; + + if (class_exists($type)) { + return new Id($tt->denormalize($raw_id, $type), $value['object']); + } else { + $id = match($type) { + 'integer' => (int) $raw_id, + 'string' => (string) $raw_id, + 'float' => (float) $raw_id, + 'bool' => (bool) $raw_id, + 'null' => null, + default => throw new NormalizingException("Invalid type for id: {$type}") + }; + return new Id($id, $value['object']); + } + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/DateTimeNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/DateTimeNormalizer.php new file mode 100644 index 000000000000..e2fb07c8d584 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/DateTimeNormalizer.php @@ -0,0 +1,58 @@ + + */ +#[Normalizes(DateTime::class, DateTimeImmutable::class)] +class DateTimeNormalizer implements Normalizer +{ + /** + * @inheritDoc + */ + public function normalize($value): array|float|bool|int|string|null + { + if ($value instanceof DateTimeImmutable || $value instanceof DateTime) { + return $value->format(DATE_ATOM); + } + + throw new NormalizingException('Invalid datetime value', $value); + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $value, string $type): DateTime|DateTimeImmutable + { + return match($type) { + DateTimeImmutable::class => DateTimeImmutable::createFromFormat(DATE_ATOM, $value), + DateTime::class => DateTime::createFromFormat(DATE_ATOM, $value), + default => throw new NormalizingException("Invalid type for datetime: {$type}") + }; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/EnvelopeNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/EnvelopeNormalizer.php new file mode 100644 index 000000000000..697a6765077f --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/EnvelopeNormalizer.php @@ -0,0 +1,63 @@ + + */ +#[Normalizes(Envelope::class)] +class EnvelopeNormalizer implements Normalizer +{ + public function __construct( + private readonly Transformations $tt, + ) { + } + + /** + * @inheritDoc + */ + public function normalize($value): array|float|bool|int|string|null + { + if (!$value instanceof Envelope) { + throw new NormalizingException('Invalid envelope value', $value); + } + + return $value->toArray($this->tt); + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $value, string $type): Envelope + { + if (!in_array(Envelope::class, class_implements($type))) { + throw new NormalizingException('Invalid envelope type', $type); + } + + return $type::fromArray($value, $this->tt); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php new file mode 100644 index 000000000000..50aadff251f6 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php @@ -0,0 +1,155 @@ + + */ +#[Normalizes(ilObject::class)] +class IlObjectNormalizer implements Normalizer +{ + public function __construct( + protected readonly Transformations $tt, + ) { + } + + /** + * @inheritDoc + */ + public function normalize($value): array|float|bool|int|string|null + { + if (!$value instanceof ilObject) { + throw new NormalizingException('Invalid value', $value); + } + + // TODO: Icon + // TODO: Tile Image + // TODO: Translations + // TODO: Container Settings + + return [ + 'obj_id' => $this->tt->normalize(new Id($value->getId(), 'object')), + 'title' => $value->getTitle(), + 'description' => $value->getLongDescription(), + 'type' => $value->getType(), + 'owner' => $value->getOwner(), + 'create_date' => $value->getCreateDate(), + 'last_update' => $value->getLastUpdateDate(), + 'import_id' => $value->getImportId(), + 'properties' => $this->normalizeProperties($value->getObjectProperties()), + ]; + } + + private function normalizeProperties(Properties $properties): array + { + return [ + 'owner' => $properties->getOwner(), + 'import_id' => $properties->getImportId(), + 'title_and_description' => $this->normalizeProperty($properties->getPropertyTitleAndDescription()), + 'title_and_icon_visibility' => $this->normalizeProperty($properties->getPropertyTitleAndIconVisibility()), + 'header_action_visibility' => $this->normalizeProperty($properties->getPropertyHeaderActionVisibility()), + 'info_tab_visibility' => $this->normalizeProperty($properties->getPropertyInfoTabVisibility()), + // 'tile_image' => $properties->getPropertyTileImage(), + // 'icon' => $this->normalizeProperty($properties->getPropertyIcon()), + 'translations' => $this->normalizeTranslations($properties->getPropertyTranslations()), + ]; + } + + private function normalizeProperty(Property $property): array|bool|int|string|null + { + if ($property instanceof TitleAndDescription) { + return [ + 'title' => $property->getTitle(), + 'description' => $property->getDescription(), + 'long_description' => $property->getLongDescription(), + ]; + } + if ($property instanceof Online) { + return $property->getIsOnline(); + } + if ($property instanceof TitleAndIconVisibility || $property instanceof HeaderActionVisibility || $property instanceof InfoTabVisibility) { + return $property->getVisibility(); + } + + return null; + } + + private function normalizeTranslations(Translations $translations): array + { + return [ + 'default_language' => $translations->getDefaultLanguage(), + 'base_language' => $translations->getBaseLanguage(), + 'languages' => array_map(fn(Language $language): array => [ + 'language_code' => $language->getLanguageCode(), + 'title' => $language->getTitle(), + 'description' => $language->getDescription(), + ], $translations->getLanguages()), + ]; + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $value, string $type): ilObject + { + if ($type !== ilObject::class && !in_array(ilObject::class, class_parents($type))) { + throw new NormalizingException("Invalid type for ilObject: {$type}"); + } + + // Validate the class of the object by its type field + $object_type = $this->tt->string($value['type']); + $object_class = ilObjectFactory::getClassByType($object_type); + if ($object_class !== $type) { + throw new NormalizingException("Expected {$type}, got object of type {$object_type} ({$object_class})"); + } + + // Create new object instance without id to avoid reading the object from the database + $object = new $object_class(0, false); + + $object->setId($this->tt->denormalize($value['obj_id'], Id::class)->getId()); + $object->setTitle($this->tt->string($value['title'])); + $object->setDescription($this->tt->string($value['description'])); + $object->setType($this->tt->string($value['type'])); + $object->setOwner($this->tt->int($value['owner'])); + $object->setImportId($this->tt->string($value['import_id'])); + + // TODO: Properties + + return $object; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Legacy11UUIDNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Legacy11UUIDNormalizer.php new file mode 100644 index 000000000000..01c61026d6ac --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Legacy11UUIDNormalizer.php @@ -0,0 +1,57 @@ + + */ + private array $type_map = []; + + /** + * Register a normalizer resolving callable for a type. The callable allows to defer the instantiation of the + * normalizer until it is actually needed. + * + * @param class-string $type + * @param callable():Normalizer $normalizer + * + * @throws NormalizingException if the type is already registered + */ + public function registerNormalizer(string $type, callable $normalizer): void + { + if (isset($this->type_map[$type])) { + throw new NormalizingException("Type {$type} is already registered"); + } + $this->type_map[$type] = $normalizer; + } + + /** + * Check if a normalizer is registered for a type. + * + * @param class-string $type + */ + public function hasNormalizer(string $type): bool + { + return isset($this->type_map[$type]); + } + + /** + * Return the normalizer that should handle the given type. Resolves to the most specific + * registered type (child classes / implementing classes before parents/interfaces). + * + * @param class-string $type + * @return Normalizer|null null if no normalizer supports this type + */ + public function getNormalizerFor(string $type): ?Normalizer + { + $candidates = $this->findCandidateTypes($type); + if ($candidates === []) { + return null; + } + $key = $this->selectMostSpecificType($type, $candidates); + + // Instantiate the normalizer on demand + if (is_callable($this->type_map[$key])) { + $this->type_map[$key] = $this->type_map[$key](); + } + + return $this->type_map[$key]; + } + + /** + * Types S from registry such that $type is assignable to S (same class or subclass/implementation). + * + * @param class-string $type + * @return list + */ + private function findCandidateTypes(string $type): array + { + $candidates = []; + foreach (array_keys($this->type_map) as $registeredType) { + if ($type === $registeredType || is_subclass_of($type, $registeredType)) { + $candidates[] = $registeredType; + } + } + + return $candidates; + } + + /** + * Among candidate types (all are assignable from $type), return the most specific one + * (the one closest to $type: child classes before parent classes/interfaces). + * + * @param class-string $type + * @param list $candidate_types + * @return class-string + */ + private function selectMostSpecificType(string $type, array $candidate_types): string + { + if (count($candidate_types) === 1) { + return $candidate_types[0]; + } + + usort($candidate_types, function (string $a, string $b): int { + if ($a === $b) { + return 0; + } + if (is_subclass_of($a, $b)) { + return -1; + } + if (is_subclass_of($b, $a)) { + return 1; + } + return 0; + }); + + return $candidate_types[0]; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/TransformationNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/TransformationNormalizer.php new file mode 100644 index 000000000000..1f55670c6238 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/TransformationNormalizer.php @@ -0,0 +1,63 @@ + + */ +#[Normalizes(Transformation::class)] +class TransformationNormalizer implements Normalizer +{ + private readonly Refinery $refinery; + + public function __construct( + Container $dic + ) { + $this->refinery = $dic->refinery(); + } + + /** + * @inheritDoc + */ + public function normalize($value): array|float|bool|int|string|null + { + if ($value instanceof Transformation) { + return $value->transform([]); + } + + throw new NormalizingException('Invalid transformation value', $value); + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $value, string $type): Transformation + { + return $this->refinery->custom()->transformation(fn() => $value); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/UUIDNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/UUIDNormalizer.php new file mode 100644 index 000000000000..a0ef76fd1bfe --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/UUIDNormalizer.php @@ -0,0 +1,61 @@ + + */ +#[Normalizes(Uuid::class)] +class UUIDNormalizer implements Normalizer +{ + private readonly Factory $factory; + + public function __construct() + { + $this->factory = new Factory(); + } + + /** + * @inheritDoc + */ + public function normalize($value): array|float|bool|int|string|null + { + if ($value instanceof Uuid) { + return $value->toString(); + } + + throw new NormalizingException('Invalid UUID value', $value); + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $value, string $type): Uuid + { + return $this->factory->fromString((string) $value); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/NormalizingException.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/NormalizingException.php new file mode 100644 index 000000000000..ebf6c5344d12 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/NormalizingException.php @@ -0,0 +1,37 @@ +result = $result; + return $this; + } + + /** + * Get the result of the denormalization carry. If no result is set, an exception will be thrown. + * + * @throws NormalizingException if the result is not set + */ + public function result(): mixed + { + if (!isset($this->result)) { + $expected_type = get_debug_type($this->expected); + $normalized_type = get_debug_type($this->normalized); + throw new NormalizingException("Unsupported value, expected: {$expected_type}, got: {$normalized_type}"); + } + return $this->result; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizingPipe.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizingPipe.php new file mode 100644 index 000000000000..233b8b825bf2 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizingPipe.php @@ -0,0 +1,62 @@ + + */ +class DenormalizingPipe implements Pipe +{ + public function __construct( + private readonly Registry $registry + ) { + } + + public function handle(mixed $passable, \Closure $next): mixed + { + if (!$passable instanceof DenormalizeCarry) { + return $next($passable); + } + + // Check if normalizer for the expected type is registered. + if (is_string($passable->expected) && $normalizer = $this->registry->getNormalizerFor($passable->expected)) { + return $next( + $passable->setResult($normalizer->denormalize($passable->normalized, $passable->expected)) + ); + } + + // Use the fromNormalized method of the expected object to set the state of the object from the normalized form. + if ($passable->expected instanceof Normalizable) { + return $next( + $passable->setResult($passable->expected->fromNormalized($passable->transformations)->transform($passable->normalized)) + ); + } + + return $next($passable); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/FinalizeNormalizing.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/FinalizeNormalizing.php new file mode 100644 index 000000000000..4964f65e3bd1 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/FinalizeNormalizing.php @@ -0,0 +1,56 @@ +ensureNormalized($passable->result()); + return $next($passable); + } + + private function ensureNormalized(mixed $value): mixed + { + if (is_scalar($value) || $value === null) { + return $value; + } + + if (is_array($value)) { + return array_map($this->ensureNormalized(...), $value); + } + + throw new NormalizingException('Value is not normalized: ' . get_debug_type($value)); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/IdMappingPipe.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/IdMappingPipe.php new file mode 100644 index 000000000000..138c4d6533bc --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/IdMappingPipe.php @@ -0,0 +1,83 @@ + + */ +class IdMappingPipe implements Pipe +{ + private array $unresolved = []; + + public function __construct( + private readonly ilImportMapping $mapping, + private readonly string $component + ) { + } + + public function handle(mixed $passable, \Closure $next): mixed + { + if (!$passable instanceof DenormalizeCarry || $passable->expected !== Id::class) { + return $next($passable); + } + + $envelope = $passable->result(); + if (!$envelope instanceof Id) { + throw new NormalizingException('Expected id envelope, got ' . get_debug_type($envelope)); + } + + if ($new_id = $this->mapping->getMapping($this->component, $envelope->getObject(), (string) $envelope->getId())) { + // Replace the envelope with the mapped new id + if (is_int($envelope->getId())) { + $new_id = (int) $new_id; + } + + $passable->setResult(new Id($new_id, $envelope->getObject())); + } else { + $this->unresolved[] = $envelope; + } + + return $next($passable); + } + + /** + * @return list + */ + public function unresolved(): array + { + return $this->unresolved; + } + + public function mapping(): ilImportMapping + { + return $this->mapping; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizeCarry.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizeCarry.php new file mode 100644 index 000000000000..27cebb51fdad --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizeCarry.php @@ -0,0 +1,45 @@ +result = $result; + return $this; + } + + /** + * Get the result of the normalization carry. If no result is set, an exception will be thrown. + * + * @throws NormalizingException if the result is not set + */ + public function result(): array|float|bool|int|string|null + { + if ($this->result === null) { + throw new NormalizingException('Unsupported value', $this->value); + } + return $this->result; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizingPipe.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizingPipe.php new file mode 100644 index 000000000000..fa255289d8c9 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizingPipe.php @@ -0,0 +1,66 @@ + + */ +class NormalizingPipe implements Pipe +{ + public function __construct( + private readonly Registry $registry + ) { + } + + public function handle(mixed $passable, \Closure $next): mixed + { + if (!$passable instanceof NormalizeCarry) { + return $next($passable); + } + + if (is_scalar($passable->value)) { + return $next($passable->setResult($passable->value)); + } + + // Check if object is self-normalizable and use the toNormalized method + if ($passable->value instanceof Normalizable) { + $normalized = $passable->value->toNormalized($passable->transformations)->transform($passable->context); + + return $next($passable->setResult($normalized)); + } + + // Lookup normalizer for the object type and use the normalize method + if ($normalizer = $this->registry->getNormalizerFor(get_class($passable->value))) { + return $next( + $passable->setResult($normalizer->normalize($passable->value)) + ); + } + + return $next($passable); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Transformations.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Transformations.php new file mode 100644 index 000000000000..0cc194358ad5 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Transformations.php @@ -0,0 +1,151 @@ + $this->normalize($value, $context), $value); + } + + return $this->pipeline->send(new NormalizeCarry($this, $value, $context)) + ->then(fn(NormalizeCarry $carry) => $carry->result()); + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $normalized, string|object $expected): mixed + { + if ($normalized === null) { + return null; + } + + return $this->pipeline->send(new DenormalizeCarry($this, $normalized, $expected)) + ->then(fn(DenormalizeCarry $carry) => $carry->result()); + } + + /* + Transformations + */ + + /** + * @inheritDoc + */ + public function context(string $pipe_class): Pipe + { + foreach ($this->pipeline->pipes() as $pipe) { + if ($pipe instanceof $pipe_class) { + return $pipe; + } + } + throw new InvalidArgumentException("Pipe {$pipe_class} not found"); + } + + public function custom(): Group + { + return $this->refinery->custom(); + } + + /** + * @throws InvalidArgumentException if the value cannot be transformed into an integer + */ + public function int(mixed $value): int + { + return $this->refinery->kindlyTo()->int()->transform($value); + } + + /** + * @throws InvalidArgumentException if the value cannot be transformed into a float + */ + public function float(mixed $value): float + { + return $this->refinery->kindlyTo()->float()->transform($value); + } + + /** + * @throws InvalidArgumentException if the value cannot be transformed into a string + */ + public function string(mixed $value): string + { + return $this->refinery->kindlyTo()->string()->transform($value); + } + + /** + * @throws InvalidArgumentException if the value cannot be transformed into a boolean + */ + public function bool(mixed $value): bool + { + return $this->refinery->kindlyTo()->bool()->transform($value); + } + + public function nullableInt(mixed $value): ?int + { + return $this->refinery->byTrying([ + $this->refinery->kindlyTo()->int(), + $this->refinery->always(null) + ])->transform($value); + } + + public function nullableFloat(mixed $value): ?float + { + return $this->refinery->byTrying([ + $this->refinery->kindlyTo()->float(), + $this->refinery->always(null) + ])->transform($value); + } + + public function nullableString(mixed $value): ?string + { + return $this->refinery->byTrying([ + $this->refinery->kindlyTo()->string(), + $this->refinery->always(null) + ])->transform($value); + } + + public function nullableBool(mixed $value): ?bool + { + return $this->refinery->byTrying([ + $this->refinery->kindlyTo()->bool(), + $this->refinery->always(null) + ])->transform($value); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Pipeline.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Pipeline.php new file mode 100644 index 000000000000..fe32b10d38eb --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Pipeline.php @@ -0,0 +1,200 @@ + + * + * @phpstan-type PipeParam \Closure(TPassable, PipeParam)|Pipe: TPassable + */ +class Pipeline implements PipelineContract +{ + /** + * The object being passed through the pipeline. + * + * @var TPassable $passable + */ + protected mixed $passable; + + /** + * The array of class pipes. + * + * @var list $pipes + */ + protected array $pipes = []; + + /** + * The final callback to be executed after the pipeline ends regardless of the outcome. + */ + protected ?Closure $finally = null; + + + /** + * @inheritDoc + */ + public function send(mixed $passable): self + { + $this->passable = $passable; + return $this; + } + + /** + * @inheritDoc + */ + public function through(array $pipes): self + { + $this->pipes = $pipes; + return $this; + } + + /** + * @inheritDoc + */ + public function pipe(Closure|Pipe $pipe): self + { + $this->pipes[] = $pipe; + return $this; + } + + /** + * @inheritDoc + */ + public function pipeWhen(Closure $condition, Closure|Pipe $pipe): self + { + return $this->pipe(fn($passable, $next) => $condition($passable) + ? $this->executePipe($pipe, $passable, $next) + : $next($passable)); + } + + /** + * @inheritDoc + */ + public function pipeUnless(Closure $condition, Closure|Pipe $pipe): self + { + return $this->pipeWhen(fn($passable) => !$condition($passable), $pipe); + } + + /** + * @inheritDoc + */ + public function then(Closure $destination): mixed + { + $pipeline = array_reduce( + array_reverse($this->pipes), + $this->carry(), + $this->prepareDestination($destination) + ); + + try { + return $pipeline($this->passable); + } finally { + if ($this->finally) { + ($this->finally)($this->passable); + } + } + } + + /** + * @inheritDoc + */ + public function thenReturn(): mixed + { + return $this->then(fn($passable) => $passable); + } + + /** + * @inheritDoc + */ + public function finally(Closure $callback): self + { + $this->finally = $callback; + return $this; + } + + /** + * @inheritDoc + */ + public function pipes(): array + { + return $this->pipes; + } + + /** + * Get the final piece of the Closure onion. + */ + protected function prepareDestination(Closure $destination) + { + return function ($passable) use ($destination) { + try { + return $destination($passable); + } catch (Throwable $e) { + return $this->handleException($passable, $e); + } + }; + } + + /** + * Get a Closure that represents a slice of the application onion. + */ + protected function carry() + { + return fn($stack, $pipe) => function ($passable) use ($stack, $pipe) { + try { + return $this->executePipe($pipe, $passable, $stack); + } catch (Throwable $e) { + return $this->handleException($passable, $e); + } + }; + } + + /** + * Execute a single pipe, handling both Closure and Pipe instances. + * Pipe instances are skipped when their skip() method returns true. + */ + protected function executePipe(Closure|Pipe $pipe, mixed $passable, Closure $next): mixed + { + if ($pipe instanceof Pipe) { + return $pipe->handle($passable, $next); + } + + if (is_callable($pipe)) { + return $pipe($passable, $next); + } + + throw new \InvalidArgumentException('Invalid pipe'); + } + + /** + * Handle the given exception. + * + * @throws \Throwable + */ + protected function handleException(mixed $passable, Throwable $e): void + { + throw $e; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemoryDeserializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemoryDeserializer.php new file mode 100644 index 000000000000..3ea8e3904b3b --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemoryDeserializer.php @@ -0,0 +1,70 @@ + $handler */ + private array $handler = []; + + /** + * @inheritDoc + */ + public function open(string $json): static + { + $clone = clone $this; + $clone->decoded = json_decode($json, true); + return $clone; + } + + /** + * @inheritDoc + */ + public function addHandler(string $group, callable $handler): void + { + $this->handler[$group] = $handler; + } + + /** + * @inheritDoc + */ + public function process(): void + { + foreach ($this->decoded as $key => $value) { + if (is_array($value)) { + $head = $value[array_key_first($value)]; + $value = (!array_is_list($head)) ? [$head] : $head; + } + + if (isset($this->handler[$key])) { + $this->handler[$key]($value); + } + } + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemorySerializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemorySerializer.php new file mode 100644 index 000000000000..dcf935c8dfd5 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemorySerializer.php @@ -0,0 +1,113 @@ + Stack of for nested structure + */ + private array $stack = []; + + /** + * @inheritDoc + */ + public function open(string $path): static + { + $clone = clone $this; + $clone->stack = [ + ['name' => '', 'data' => []], + ]; + return $clone; + } + + /** + * @inheritDoc + */ + public function startGroup(string $name): void + { + $this->stack[] = ['name' => $name, 'data' => []]; + } + + /** + * @inheritDoc + */ + public function endGroup(string $name): void + { + $frame = array_pop($this->stack); + if ($frame['name'] !== $name) { + throw new \LogicException( + "Group name mismatch: expected end of '{$frame['name']}', got '{$name}'" + ); + } + $top = &$this->stack[array_key_last($this->stack)]; + $top['data'][$name] = $frame['data']; + } + + /** + * @inheritDoc + */ + public function group(string $name, callable $callback): void + { + $this->startGroup($name); + $callback(); + $this->endGroup($name); + } + + /** + * @inheritDoc + */ + public function append(string $name, array $data): void + { + $top = &$this->stack[array_key_last($this->stack)]; + if (array_key_exists($name, $top['data'])) { + $existing = $top['data'][$name]; + if (is_array($existing) && array_is_list($existing)) { + $top['data'][$name][] = $data; + } else { + $top['data'][$name] = [$existing, $data]; + } + } else { + $top['data'][$name] = $data; + } + } + + /** + * @inheritDoc + */ + public function write(): string + { + if ($this->stack === []) { + return '{}'; + } + $root = $this->stack[0]['data']; + $json = json_encode($root, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + $this->stack = [ + ['name' => '', 'data' => []], + ]; + return $json; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLDeserializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLDeserializer.php new file mode 100644 index 000000000000..7e93b5eac22b --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLDeserializer.php @@ -0,0 +1,246 @@ + $handler */ + private array $handler = []; + + /** + * @inheritDoc + */ + public function open(string $path): static + { + $clone = clone $this; + $clone->xml = $path; + return $clone; + } + + /** + * @inheritDoc + */ + public function addHandler(string $group, callable $handler): void + { + $this->handler[$group] = $handler; + } + + /** + * @inheritDoc + */ + public function process(): void + { + $reader = new \XMLReader(); + $xml = $this->prepareXmlInput($this->xml); + + if (!$reader->XML($xml, null, LIBXML_NONET)) { + throw new \RuntimeException('Unable to read XML input.'); + } + + while ($reader->read()) { + $node_name = $this->kebabToSnake($reader->name); + + if ($reader->nodeType !== \XMLReader::ELEMENT || !isset($this->handler[$node_name])) { + continue; + } + + $group_data = $this->readGroup($reader); + $this->handler[$node_name]($group_data); + } + + $reader->close(); + } + + /** + * @return list + */ + private function readGroup(\XMLReader $reader): array + { + if ($reader->isEmptyElement) { + return []; + } + + // Track the current group boundary so we can stop exactly at its closing tag + $group_depth = $reader->depth; + $group_name = $reader->name; + $group_data = []; + + while ($reader->read()) { + if ( + $reader->nodeType === \XMLReader::END_ELEMENT + && $reader->depth === $group_depth + && $reader->name === $group_name + ) { + break; + } + + if ( + $reader->nodeType !== \XMLReader::ELEMENT + || $reader->depth !== $group_depth + 1 + ) { + // Only direct children belong to this group entry list + continue; + } + + $group_data[] = $this->readElementValue($reader); + } + + return $group_data; + } + + private function readElementValue(\XMLReader $reader): mixed + { + $is_marked_empty_array = $this->isMarkedEmptyArray($reader); + + // Keep empty elements as empty strings to preserve the legacy XML shape + if ($reader->isEmptyElement) { + if ($is_marked_empty_array) { + return []; + } + return ''; + } + + $element_depth = $reader->depth; + $element_name = $reader->name; + $children = []; + $text_content = ''; + + while ($reader->read()) { + if ( + $reader->nodeType === \XMLReader::END_ELEMENT + && $reader->depth === $element_depth + && $reader->name === $element_name + ) { + break; + } + + if ( + $reader->nodeType === \XMLReader::ELEMENT + && $reader->depth === $element_depth + 1 + ) { + // Direct child elements are deserialized recursively + $child_key = $this->resolveElementKey($reader); + $child_value = $this->readElementValue($reader); + + if ($child_key === null) { + // Unkeyed nodes are appended as list entries + $children[] = $child_value; + continue; + } + + // Repeated keys are normalized to list values via appendValue() + $this->appendValue($children, $child_key, $child_value); + continue; + } + + if ( + $reader->depth === $element_depth + 1 + && in_array( + $reader->nodeType, + [ + \XMLReader::TEXT, + \XMLReader::CDATA, + \XMLReader::SIGNIFICANT_WHITESPACE + ], + true + ) + ) { + // Keep raw text content and decode scalar tokens after traversal + $text_content .= $reader->value; + } + } + + if ($children !== []) { + // Structured child data has precedence over accumulated text content + return $children; + } + + if ($is_marked_empty_array && trim($text_content) === '') { + return []; + } + + return $this->decodeScalarValue($text_content); + } + + private function isMarkedEmptyArray(\XMLReader $reader): bool + { + return $reader->getAttribute('type') === 'empty-array'; + } + + private function resolveElementKey(\XMLReader $reader): ?string + { + // Regular element names map directly to associative keys + if ($reader->name !== 'item') { + return $this->kebabToSnake($reader->name); + } + + // nodes are list-like unless they define an explicit key attribute + $raw_key = $reader->getAttribute('key'); + if ($raw_key === null || $raw_key === '') { + return null; + } + + // Normalize kebab-case keys to snake_case for downstream consumers + return $this->kebabToSnake($raw_key); + } + + /** + * @param array $target + * @param mixed $value + */ + private function appendValue(array &$target, string $key, mixed $value): void + { + if (!array_key_exists($key, $target)) { + $target[$key] = $value; + return; + } + + if (!is_array($target[$key]) || !array_is_list($target[$key])) { + $target[$key] = [$target[$key]]; + } + + $target[$key][] = $value; + } + + private function decodeScalarValue(string $value): mixed + { + $decoded = htmlspecialchars_decode($value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5); + return $decoded === 'NULL' ? null : $decoded; + } + + private function kebabToSnake(string $name): string + { + return str_replace('-', '_', $name); + } + + private function prepareXmlInput(string $xml): string + { + $xml = preg_replace('/^\s*<\?xml[^>]*\?>\s*/i', '', trim($xml)) ?? trim($xml); + return "{$xml}"; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php new file mode 100644 index 000000000000..70c73271663c --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php @@ -0,0 +1,186 @@ +writer = new \XMLWriter(); + } + + /** + * @inheritDoc + */ + public function open(string $path): static + { + $clone = clone $this; + $clone->writer->openMemory(); + $clone->writer->setIndent(true); + return $clone; + } + + /** + * Start a new xml document in the current writer. if a document has already been started, an exception will be + * thrown. + * + * @throws \LogicException if a document has already been started + */ + private function createDocument(string $comment): void + { + if ($this->has_document) { + throw new \LogicException('XML document already started'); + } + + $this->writer->startDocument('1.0', 'UTF-8'); + $this->writer->writeComment($comment); + $this->has_document = true; + } + + + /** + * @inheritDoc + */ + public function startGroup(string $name): void + { + $this->current_group = $name; + $this->writer->startElement($this->formatName($name)); + } + + /** + * @inheritDoc + */ + public function endGroup(string $name): void + { + if ($this->current_group !== $name) { + throw new \LogicException( + "Group name mismatch: expected end of '{$this->current_group}', got '{$name}'" + ); + } + + $this->current_group = ''; + $this->writer->endElement(); + } + + /** + * @inheritDoc + */ + public function group(string $name, callable $callback): void + { + $this->startGroup($name); + $callback(); + $this->endGroup($name); + } + + /** + * @inheritDoc + */ + public function append(string $name, array $data): void + { + $this->writer->startElement($this->formatName($name)); + $this->appendRecursive($data); + $this->writer->endElement(); + } + + /** + * @inheritDoc + */ + public function write(): string + { + if ($this->has_document) { + $this->writer->endDocument(); + } + + return $this->writer->outputMemory(true); + } + + /** + * @param array $data + */ + private function appendRecursive(array $data): void + { + foreach ($data as $key => $value) { + $is_nested = is_array($value); + $formatted_key = $this->formatName($key); + + if ($this->shouldUseItemElement($key, $formatted_key)) { + $this->writer->startElement('item'); + + if (!array_is_list($data)) { + $this->writer->writeAttribute('key', (string) $key); + } + } else { + $this->writer->startElement($formatted_key); + } + + if (!$is_nested) { + $value = match (gettype($value)) { + 'NULL' => 'NULL', + 'integer' => (string) $value, + 'float' => (string) $value, + 'boolean' => $value ? '1' : '0', + default => htmlspecialchars((string) $value), + }; + + $this->writer->writeRaw($value); + } else { + if (count($value) === 0) { + $this->writer->writeAttribute('type', 'empty-array'); + } + $this->appendRecursive($value); + } + + $this->writer->endElement(); + } + } + + private function shouldUseItemElement(int|string $key, string $formatted_key): bool + { + if (is_numeric($key) || str_contains((string) $key, '-') || $key === '') { + return true; + } + + return !$this->isValidXmlElementName($formatted_key); + } + + private function isValidXmlElementName(string $name): bool + { + return $name !== '' && preg_match('/^[A-Za-z_][A-Za-z0-9._-]*$/', $name) === 1; + } + + private function formatName(int|string $name): string + { + // Transform key to kebab-case + $output = strtolower(preg_replace('/(?getMatchingClassNames(Normalizer::class) as $class_name) { + $ref = new ReflectionClass($class_name); + + $attrs = $ref->getAttributes(Normalizes::class); + foreach ($attrs as $attr) { + $instance = $attr->newInstance(); + foreach ($instance->types as $type) { + $type_map[$type][self::DEFAULT_KEY] = $class_name; + } + } + + $attrs = $ref->getAttributes(NormalizesLegacy::class); + foreach ($attrs as $attr) { + $instance = $attr->newInstance(); + foreach ($instance->types as $type) { + $type_map[$type][$instance->version] = $class_name; + } + } + } + + return new ArrayArtifact($type_map); + } +} From 7c649534e267b6c052fbac4868e6c6f3cff40ba5 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Thu, 9 Apr 2026 12:43:17 +0200 Subject: [PATCH 02/66] feat(qpl): implement normalizing/denormalizing in question pool component --- .../classes/class.assAnswerBinaryState.php | 26 ++++ .../class.assAnswerBinaryStateImage.php | 33 +++- .../classes/class.assAnswerCloze.php | 30 ++++ .../classes/class.assAnswerErrorText.php | 32 +++- .../classes/class.assAnswerImagemap.php | 30 ++++ .../classes/class.assAnswerMatching.php | 34 +++- .../classes/class.assAnswerMatchingPair.php | 30 +++- .../classes/class.assAnswerMatchingTerm.php | 33 +++- .../class.assAnswerMultipleResponse.php | 26 ++++ .../class.assAnswerMultipleResponseImage.php | 29 ++++ .../classes/class.assAnswerSimple.php | 33 +++- .../classes/class.assAnswerTrueFalse.php | 26 ++++ .../classes/class.assClozeGap.php | 35 ++++- .../classes/class.assClozeTest.php | 47 +++++- .../classes/class.assErrorText.php | 38 ++++- .../classes/class.assFileUpload.php | 33 +++- .../classes/class.assFormulaQuestion.php | 40 ++++- .../class.assFormulaQuestionResult.php | 58 ++++++- .../classes/class.assFormulaQuestionUnit.php | 42 ++++- .../class.assFormulaQuestionUnitCategory.php | 34 +++- .../class.assFormulaQuestionVariable.php | 43 +++++- .../classes/class.assImagemapQuestion.php | 37 ++++- .../classes/class.assKprimChoice.php | 46 +++++- .../classes/class.assLongMenu.php | 39 ++++- .../classes/class.assMatchingQuestion.php | 45 +++++- .../classes/class.assMultipleChoice.php | 37 ++++- .../classes/class.assNumeric.php | 33 +++- .../classes/class.assOrderingHorizontal.php | 35 ++++- .../classes/class.assOrderingQuestion.php | 34 +++- .../classes/class.assQuestion.php | 62 ++++++++ .../classes/class.assSingleChoice.php | 36 ++++- .../classes/class.assTextQuestion.php | 41 ++++- .../classes/class.assTextSubset.php | 35 ++++- .../classes/class.ilAssKprimChoiceAnswer.php | 33 +++- .../questions/class.ilAssOrderingElement.php | 42 ++++- .../class.ilAssQuestionLifecycle.php | 26 +++- .../SuggestedSolutionNormalizer.php | 139 +++++++++++++++++ ...ilAssQuestionSkillAssignmentNormalizer.php | 145 ++++++++++++++++++ .../ilObjQuestionPoolNormalizer.php | 65 ++++++++ 39 files changed, 1631 insertions(+), 31 deletions(-) create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/SuggestedSolutionNormalizer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilObjQuestionPoolNormalizer.php diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryState.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryState.php index dde732f7ecb9..1c44eb1ed9cf 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryState.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryState.php @@ -16,6 +16,9 @@ * *********************************************************************/ +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\Refinery\Transformation; + /** * Class for true/false or yes/no answers * @@ -180,4 +183,27 @@ public function setUnchecked(): void { $this->checked = false; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'checked' => $this->checked, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->setState($tt->bool($normalized['checked'])); + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php index f2f19ca3afa4..999ba3865d36 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php @@ -18,6 +18,10 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; + /** * Class for answers with a binary state indicator * @@ -43,11 +47,11 @@ class ASS_AnswerBinaryStateImage extends ASS_AnswerBinaryState * @param string $answertext A string defining the answer text * @param double $points The number of points given for the selected answer * @param integer $order A nonnegative value representing a possible display or sort order - * @param integer $state A integer value indicating the state of the answer + * @param bool $state A integer value indicating the state of the answer * @param string $a_image The image filename * @param integer $id The database id of the answer */ - public function __construct($answertext = "", $points = 0.0, $order = 0, $state = false, ?string $a_image = null, int $id = -1) + public function __construct(string $answertext = "", float $points = 0.0, int $order = 0, bool $state = false, ?string $a_image = null, int $id = -1) { parent::__construct($answertext, (float) $points, $order, $state, $id); $this->setImage($a_image); @@ -70,4 +74,29 @@ public function hasImage(): bool { return $this->image !== null; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(array $context): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'image' => $this->image ? $tt->normalize( + new QuestionImage($this->image, $context['question_id'] ?? null) + ) : null, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->setImage($tt->denormalize($normalized['image'], QuestionImage::class)?->getFilename()); + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerCloze.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerCloze.php index 1b6915fd607b..04f4bd4381d7 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerCloze.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerCloze.php @@ -16,6 +16,9 @@ * *********************************************************************/ +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\Refinery\Transformation; + /** * Class for cloze question numeric answers * @@ -150,4 +153,31 @@ public function getGapSize(): int { return $this->gap_size; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'lower_bound' => $this->lowerBound, + 'upper_bound' => $this->upperBound, + 'gap_size' => $this->gap_size, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->lowerBound = $tt->nullableString($normalized['lower_bound']); + $clone->upperBound = $tt->nullableString($normalized['upper_bound']); + $clone->gap_size = $tt->int($normalized['gap_size']); + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerErrorText.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerErrorText.php index 1736454b78ca..0567ce2925a4 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerErrorText.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerErrorText.php @@ -16,6 +16,10 @@ * *********************************************************************/ +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\Refinery\Transformation; + /** * Class for error text answers * @@ -24,7 +28,7 @@ * * @ingroup components\ILIASTestQuestionPool */ -class assAnswerErrorText +class assAnswerErrorText implements Normalizable { protected string $text_wrong; protected string $text_correct; @@ -94,4 +98,30 @@ public function getLength(): int { return $this->length; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + 'text_wrong' => $this->text_wrong, + 'text_correct' => $this->text_correct, + 'points' => $this->points, + 'position' => $this->position, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(array $normalized): self => new self( + $tt->string($normalized['text_wrong']), + $tt->string($normalized['text_correct']), + $tt->float($normalized['points']), + $tt->int($normalized['position']) + )); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerImagemap.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerImagemap.php index 7356acba7457..b3b69c42d554 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerImagemap.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerImagemap.php @@ -16,6 +16,9 @@ * *********************************************************************/ +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\Refinery\Transformation; + /** * Class for true/false or yes/no answers * @@ -142,4 +145,31 @@ public function setPointsUnchecked($points_unchecked): void $this->points_unchecked = 0.0; } } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'coords' => $this->coords, + 'area' => $this->area, + 'points_unchecked' => $this->points_unchecked, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->setCoords($tt->string($normalized['coords'])); + $clone->setArea($tt->string($normalized['area'])); + $clone->setPointsUnchecked($tt->float($normalized['points_unchecked'])); + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatching.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatching.php index ce7292c05eba..71137aac7763 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatching.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatching.php @@ -16,6 +16,10 @@ * *********************************************************************/ +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\Refinery\Transformation; + /** * Class for matching question answers * @@ -24,7 +28,7 @@ * @author Helmut Schottmüller * @ingroup components\ILIASTestQuestionPool */ -class ASS_AnswerMatching +class ASS_AnswerMatching implements Normalizable { public float $points; @@ -236,4 +240,32 @@ public function setPoints(float $points = 0.0): void { $this->points = $points; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + 'points' => $this->points, + 'picture_or_definition' => $this->picture_or_definition, + 'picture_or_definition_id' => $this->picture_or_definition_id, + 'term_id' => $this->term_id, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = clone $this; + $clone->setPoints($tt->float($normalized['points'])); + $clone->setPicture($tt->string($normalized['picture_or_definition'])); + $clone->setPictureId($tt->int($normalized['picture_or_definition_id'])); + $clone->setTermId($tt->int($normalized['term_id'])); + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingPair.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingPair.php index 4c628ee66d7e..9601aabf9026 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingPair.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingPair.php @@ -18,13 +18,17 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\Refinery\Transformation; + /** * Class for matching question pairs * * @author Helmut Schottmüller * @ingroup components\ILIASTestQuestionPool */ -class assAnswerMatchingPair +class assAnswerMatchingPair implements Normalizable { protected assAnswerMatchingTerm $term; protected assAnswerMatchingDefinition $definition; @@ -72,4 +76,28 @@ public function withPoints(float $points): self $clone->points = $points; return $clone; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(array $context): array => [ + 'points' => $this->points, + 'term' => $tt->normalize($this->term, $context), + 'definition' => $tt->normalize($this->definition, $context), + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + return $this->withPoints($tt->float($normalized['points'])) + ->withTerm($tt->denormalize($normalized['term'], $this->term)) + ->withDefinition($tt->denormalize($normalized['definition'], $this->definition)); + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php index b2500c8e93d6..c2a986f71671 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php @@ -18,13 +18,18 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; + /** * Class for matching question terms * * @author Helmut Schottmüller * @ingroup components\ILIASTestQuestionPool */ -class assAnswerMatchingTerm +class assAnswerMatchingTerm implements Normalizable { protected string $text; protected string $picture; @@ -76,4 +81,30 @@ public function withIdentifier(int $identifier): self $clone->identifier = $identifier; return $clone; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(array $context): array => [ + 'text' => $this->text, + 'picture' => $this->picture ? $tt->normalize( + new QuestionImage($this->picture, $context['question_id'] ?? null) + ) : null, + 'identifier' => $this->identifier, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + return $this->withText($tt->string($normalized['text'])) + ->withPicture($tt->denormalize($normalized['picture'], QuestionImage::class)?->getFilename() ?? '') + ->withIdentifier($tt->int($normalized['identifier'])); + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponse.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponse.php index ace7464d312e..331fd04594e2 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponse.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponse.php @@ -16,6 +16,9 @@ * *********************************************************************/ +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\Refinery\Transformation; + /** * Class for true/false or yes/no answers * @@ -97,4 +100,27 @@ public function getPointsChecked(): float { return $this->getPoints(); } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'points_unchecked' => $this->points_unchecked, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->points_unchecked = $tt->nullableFloat($normalized['points_unchecked']); + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php index 559c06ef0b49..c410a7e23463 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php @@ -18,6 +18,10 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; + /** * ASS_AnswerBinaryStateImage is a class for answers with a binary state * indicator (checked/unchecked, set/unset) and an image file @@ -74,4 +78,29 @@ public function hasImage(): bool { return $this->image !== null; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(array $context): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'image' => $this->image ? $tt->normalize( + new QuestionImage($this->image, $context['question_id'] ?? null) + ) : null, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->setImage($tt->denormalize($normalized['image'], QuestionImage::class)?->getFilename()); + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php index b6c6cff29fea..72b59cab0c11 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php @@ -16,6 +16,11 @@ * *********************************************************************/ +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; +use ILIAS\Refinery\Transformation; + /** * Class for simple answers * @@ -26,7 +31,7 @@ * * @ingroup components\ILIASTestQuestionPool */ -class ASS_AnswerSimple +class ASS_AnswerSimple implements Normalizable { protected string $answertext; @@ -211,4 +216,30 @@ public function setPoints($points = 0.0): void $this->points = 0.0; } } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + 'id' => $tt->normalize(new Id($this->id, 'answer')), + 'answertext' => $this->answertext, + 'points' => $this->points, + 'order' => $this->order, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(array $normalized) => new static( + $tt->string($normalized['answertext']), + $tt->float($normalized['points']), + $tt->int($normalized['order']), + $tt->denormalize($normalized['id'], Id::class)->getId() + )); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerTrueFalse.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerTrueFalse.php index d366c5330d05..2ce73b3e9940 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerTrueFalse.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerTrueFalse.php @@ -16,6 +16,9 @@ * *********************************************************************/ +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\Refinery\Transformation; + /** * Class for true/false or yes/no answers * @@ -147,4 +150,27 @@ public function setFalse(): void { $this->correctness = "0"; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'correctness' => $this->correctness, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->setCorrectness($tt->string($normalized['correctness'])); + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php b/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php index 98ad47494b5b..1f12458585bb 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php @@ -16,6 +16,8 @@ * *********************************************************************/ +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\Refinery\Transformation; use ILIAS\Refinery\Random\Transformation\ShuffleTransformation; @@ -31,7 +33,7 @@ * * @ingroup components\ILIASTestQuestionPool */ -class assClozeGap +class assClozeGap implements Normalizable { public const TYPE_TEXT = 0; public const TYPE_SELECT = 1; @@ -454,4 +456,35 @@ public function numericRangeExists(): bool return false; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + 'type' => $this->type, + 'shuffle' => $this->shuffle, + 'gap_size' => $this->gap_size, + 'items' => $tt->normalize($this->items), + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = new self($normalized['type']); + $clone->setShuffle($normalized['shuffle']); + $clone->setGapSize($normalized['gap_size']); + $clone->items = array_map( + fn(array $item) => $tt->denormalize($item, new assAnswerCloze()), + $normalized['items'] + ); + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php b/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php index a7934c1a21c2..c92d9ccb7ad1 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php @@ -18,6 +18,9 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\Refinery\Transformation; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\Questions\QuestionPartiallySaveable; @@ -35,7 +38,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assClozeTest extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionPartiallySaveable, QuestionLMExportable, QuestionAutosaveable +class assClozeTest extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionPartiallySaveable, QuestionLMExportable, QuestionAutosaveable, Normalizable { /** * The gaps of the cloze question @@ -1767,4 +1770,46 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra } return $answers; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'feedback_mode' => $this->feedbackMode, + 'cloze_text' => $this->cloze_text, + 'textgap_rating' => $this->textgap_rating, + 'identical_scoring' => $this->identical_scoring, + 'fixed_text_length' => $this->fixed_text_length, + 'gaps' => $tt->normalize($this->gaps), + 'gap_combinations' => $this->gap_combinations, + 'gap_combinations_exist' => $this->gap_combinations_exist, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->feedbackMode = $tt->string($normalized['feedback_mode']); + $clone->cloze_text = $tt->string($normalized['cloze_text']); + $clone->textgap_rating = $tt->string($normalized['textgap_rating']); + $clone->identical_scoring = $tt->bool($normalized['identical_scoring']); + $clone->fixed_text_length = $tt->int($normalized['fixed_text_length']); + $clone->gap_combinations_exist = $tt->bool($normalized['gap_combinations_exist']); + $clone->gap_combinations = $normalized['gap_combinations']; + + foreach ($normalized['gaps'] as $gap) { + $type = $tt->int($gap['type']); + $clone->gaps[] = $tt->denormalize($gap, new assClozeGap($type)); + } + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php b/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php index c3cc6ff3d1fc..b98d14cab935 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php @@ -18,6 +18,9 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\Refinery\Transformation; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; @@ -34,7 +37,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assErrorText extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable +class assErrorText extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable, Normalizable { protected const ERROR_TYPE_WORD = 1; protected const ERROR_TYPE_PASSAGE = 2; @@ -1000,4 +1003,37 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): stri { return $this->createErrorTextExport($this->getBestSelection()); } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'errortext' => $this->errortext, + 'errortext_parsed' => $this->parsed_errortext, + 'errordata' => $tt->normalize($this->errordata), + 'points_wrong' => $this->points_wrong, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->errortext = $tt->string($normalized['errortext']); + $clone->parsed_errortext = $normalized['errortext_parsed']; + $clone->points_wrong = $tt->nullableFloat($normalized['points_wrong']); + $clone->errordata = array_map( + fn(array $error) => $tt->denormalize($error, new assAnswerErrorText()), + $normalized['errordata'] + ); + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php b/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php index 87fd4898d2ba..8bdffab786e8 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php @@ -18,6 +18,9 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\Refinery\Transformation; use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\Test\Participants\ParticipantRepository; use ILIAS\Test\Logging\AdditionalInformationGenerator; @@ -35,7 +38,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assFileUpload extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjFileHandlingQuestionType +class assFileUpload extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjFileHandlingQuestionType, Normalizable { public const REUSE_FILES_TBL_POSTVAR = 'reusefiles'; public const DELETE_FILES_TBL_POSTVAR = 'deletefiles'; @@ -950,4 +953,32 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): stri { return ''; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'maxsize' => $this->maxsize, + 'allowedextensions' => $this->allowedextensions, + 'completion_by_submission' => $this->completion_by_submission, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->maxsize = $tt->nullableInt($normalized['maxsize']); + $clone->allowedextensions = $tt->string($normalized['allowedextensions']); + $clone->completion_by_submission = $tt->bool($normalized['completion_by_submission']); + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php index b5389fc27efe..0dbd51dc5742 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php @@ -18,7 +18,11 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\Refinery\Transformation; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; +use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\Test\Logging\AdditionalInformationGenerator; /** @@ -28,7 +32,7 @@ * @version $Id: class.assFormulaQuestion.php 1236 2010-02-15 15:44:16Z hschottm $ * @ingroup components\ILIASTestQuestionPool */ -class assFormulaQuestion extends assQuestion implements iQuestionCondition, QuestionAutosaveable +class assFormulaQuestion extends assQuestion implements iQuestionCondition, QuestionAutosaveable, Normalizable { private array $variables; private array $results; @@ -1486,4 +1490,38 @@ function (array $c, array $v): array { ] ); } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'variables' => $tt->normalize($this->variables), + 'results' => $tt->normalize($this->results) + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + + foreach ($normalized['variables'] as $key => $data) { + $dummy = new assFormulaQuestionVariable('', '', ''); + $clone->variables[$key] = $tt->denormalize($data, $dummy); + } + + foreach ($normalized['results'] as $key => $data) { + $dummy = new assFormulaQuestionResult('', '', '', 0, null, '', 0, 0); + $clone->results[$key] = $tt->denormalize($data, $dummy); + } + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php index 715ce465d734..438a44270f2f 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php @@ -16,7 +16,10 @@ * *********************************************************************/ +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\Refinery\Factory as Refinery; +use ILIAS\Refinery\Transformation; /** * Formula Question Result @@ -24,7 +27,7 @@ * @version $Id: class.assFormulaQuestionResult.php 944 2009-11-09 16:11:30Z hschottm $ * @ingroup components\ILIASTestQuestionPool * */ -class assFormulaQuestionResult +class assFormulaQuestionResult implements Normalizable { public const RESULT_NO_SELECTION = 0; public const RESULT_DEC = 1; @@ -862,4 +865,57 @@ public function getAvailableResultUnits($question_id): array return $this->available_units; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + 'available_units' => $tt->normalize($this->available_units), + 'range_min' => $this->range_min, + 'range_max' => $this->range_max, + 'range_min_txt' => $this->range_min_txt, + 'range_max_txt' => $this->range_max_txt, + 'result' => $this->result, + 'tolerance' => $this->tolerance, + 'unit' => $tt->normalize($this->unit), + 'formula' => $this->formula, + 'points' => $this->points, + 'precision' => $this->precision, + 'rating_simple' => $this->rating_simple, + 'rating_sign' => $this->rating_sign, + 'rating_value' => $this->rating_value, + 'rating_unit' => $this->rating_unit, + 'result_type' => $this->result_type, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = clone $this; + $clone->available_units = array_map(fn(array $unit) => $tt->denormalize($unit, new assFormulaQuestionUnit()), $normalized['available_units']); + $clone->range_min = $tt->float($normalized['range_min']); + $clone->range_max = $tt->float($normalized['range_max']); + $clone->range_min_txt = $tt->string($normalized['range_min_txt']); + $clone->range_max_txt = $tt->string($normalized['range_max_txt']); + $clone->result = $tt->string($normalized['result']); + $clone->tolerance = $tt->float($normalized['tolerance']); + $clone->unit = $tt->denormalize($normalized['unit'], new assFormulaQuestionUnit()); + $clone->formula = $tt->string($normalized['formula']); + $clone->points = $tt->float($normalized['points']); + $clone->precision = $tt->int($normalized['precision']); + $clone->rating_simple = $tt->bool($normalized['rating_simple']); + $clone->rating_sign = $tt->float($normalized['rating_sign']); + $clone->rating_value = $tt->float($normalized['rating_value']); + $clone->rating_unit = $tt->float($normalized['rating_unit']); + $clone->result_type = $tt->int($normalized['result_type']); + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php index 959c8362192a..aecd91246e3f 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php @@ -18,12 +18,17 @@ declare(strict_types=1); +use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; + /** * Formula Question Unit * @author Helmut Schottmüller * @ingroup components\ILIASTestQuestionPool */ -class assFormulaQuestionUnit +class assFormulaQuestionUnit implements Normalizable { private int $id = 0; private string $unit = ''; @@ -159,4 +164,39 @@ private function sanitizeString(string $string): string { return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'utf-8'); } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + 'id' => $tt->normalize(new Id($this->id, 'unit')), + 'unit' => $this->unit, + 'factor' => $this->factor, + 'category_id' => $tt->normalize(new Id($this->category, 'unit_category')), + 'sequence' => $this->sequence, + 'baseunit' => $this->baseunit, + 'baseunit_title' => $this->baseunit_title, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = clone $this; + $clone->id = $tt->denormalize($normalized['id'], Id::class)->getId(); + $clone->unit = $tt->string($normalized['unit']); + $clone->factor = $tt->float($normalized['factor']); + $clone->category = $tt->denormalize($normalized['category_id'], Id::class)->getId(); + $clone->sequence = $tt->int($normalized['sequence']); + $clone->baseunit = $tt->int($normalized['baseunit']); + $clone->baseunit_title = $tt->string($normalized['baseunit_title']); + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php index 963c0d7637e5..4ce48ecb7caf 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php @@ -18,12 +18,17 @@ declare(strict_types=1); +use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; + /** * Formula Question Unit Category * @author Helmut Schottmüller * @ingroup components\ILIASTestQuestionPool */ -class assFormulaQuestionUnitCategory +class assFormulaQuestionUnitCategory implements Normalizable { private int $id = 0; private string $category = ''; @@ -86,4 +91,31 @@ private function sanitizeString(string $string): string { return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'utf-8'); } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + 'id' => $tt->normalize(new Id($this->id, 'unit_category')), + 'name' => $this->category, + 'question_id' => $tt->normalize(new Id($this->question_fi, 'question')), + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = clone $this; + $clone->id = $tt->denormalize($normalized['id'], Id::class)->getId(); + $clone->category = $tt->string($normalized['name']); + $clone->question_fi = $tt->denormalize($normalized['question_id'], Id::class)->getId(); + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionVariable.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionVariable.php index b568847e9442..3469ca82afcd 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionVariable.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionVariable.php @@ -18,13 +18,17 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\Refinery\Transformation; + /** * Formula Question Variable * @author Helmut Schottmüller * @version $Id: class.assFormulaQuestionVariable.php 465 2009-06-29 08:27:36Z hschottm $ * @ingroup components\ILIASTestQuestionPool * */ -class assFormulaQuestionVariable +class assFormulaQuestionVariable implements Normalizable { private $value = null; private float $range_min; @@ -197,4 +201,41 @@ public function getRangeMinTxt(): string { return $this->range_min_txt; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + 'variable' => $this->variable, + 'range_min' => $this->range_min, + 'range_max' => $this->range_max, + 'range_min_txt' => $this->range_min_txt, + 'range_max_txt' => $this->range_max_txt, + 'unit' => $tt->normalize($this->unit), + 'precision' => $this->precision, + 'intprecision' => $this->intprecision, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = clone $this; + $clone->variable = $tt->string($normalized['variable']); + $clone->range_min = $tt->float($normalized['range_min']); + $clone->range_max = $tt->float($normalized['range_max']); + $clone->range_min_txt = $tt->string($normalized['range_min_txt']); + $clone->range_max_txt = $tt->string($normalized['range_max_txt']); + $clone->unit = $tt->denormalize($normalized['unit'], new assFormulaQuestionUnit()); + $clone->precision = $tt->int($normalized['precision']); + $clone->intprecision = $tt->int($normalized['intprecision']); + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php index 5408fda96360..e61177ffbf3f 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php @@ -18,10 +18,14 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\TestQuestionPool\RequestDataCollector; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\Test\Logging\AdditionalInformationGenerator; +use ILIAS\Refinery\Transformation; /** * Class for image map questions @@ -36,7 +40,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assImagemapQuestion extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable +class assImagemapQuestion extends assQuestion implements ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, Normalizable { private RequestDataCollector $request; // Hate it. @@ -927,4 +931,35 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra $this->getAnswers() ); } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'image' => $tt->normalize(new QuestionImage($this->image_filename, $this->getId())), + 'multiple_choice' => $this->is_multiple_choice, + 'answers' => $tt->normalize($this->answers), + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->image_filename = $tt->denormalize($normalized['image'], QuestionImage::class)->getFilename(); + $clone->is_multiple_choice = $tt->bool($normalized['multiple_choice']); + $clone->answers = array_map( + fn(array $answer) => $tt->denormalize($answer, new ASS_AnswerImagemap()), + $normalized['answers'] + ); + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php index c8356c52e5b6..d25f05d1b540 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php @@ -18,10 +18,13 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\ManipulateImagesInChoiceQuestionsTrait; use ILIAS\Test\Logging\AdditionalInformationGenerator; +use ILIAS\Refinery\Transformation; /** * @author Björn Heyser @@ -29,7 +32,7 @@ * * @package components\ILIAS/TestQuestionPool */ -class assKprimChoice extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, ilAssSpecificFeedbackOptionLabelProvider, QuestionLMExportable, QuestionAutosaveable +class assKprimChoice extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, ilAssSpecificFeedbackOptionLabelProvider, QuestionLMExportable, QuestionAutosaveable, Normalizable { use ManipulateImagesInChoiceQuestionsTrait; @@ -920,4 +923,45 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra $this->getAnswers() ); } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'shuffle_answers' => $this->shuffle_answers_enabled, + 'answer_type' => $this->answerType, + 'option_label' => $this->option_label, + 'custom_true_option_label' => $this->customTrueOptionLabel, + 'custom_false_option_label' => $this->customFalseOptionLabel, + 'score_partial_solution' => $this->scorePartialSolutionEnabled, + 'specific_feedback_setting' => $this->specific_feedback_setting, + 'answers' => $tt->normalize($this->answers, ['question_id' => $this->getId()]), + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->shuffle_answers_enabled = $tt->bool($normalized['shuffle_answers']); + $clone->answerType = $tt->string($normalized['answer_type']); + $clone->option_label = $tt->string($normalized['option_label']); + $clone->customTrueOptionLabel = $tt->string($normalized['custom_true_option_label']); + $clone->customFalseOptionLabel = $tt->string($normalized['custom_false_option_label']); + $clone->scorePartialSolutionEnabled = $tt->bool($normalized['score_partial_solution']); + $clone->specific_feedback_setting = $tt->int($normalized['specific_feedback_setting']); + $clone->answers = array_map( + fn(array $answer) => $tt->denormalize($answer, new ilAssKprimChoiceAnswer()), + $normalized['answers'] + ); + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php b/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php index 81dde64d6bad..addc75f602c3 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php @@ -18,11 +18,14 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; +use ILIAS\Refinery\Transformation; -class assLongMenu extends assQuestion implements ilObjQuestionScoringAdjustable, QuestionLMExportable, QuestionAutosaveable +class assLongMenu extends assQuestion implements ilObjQuestionScoringAdjustable, QuestionLMExportable, QuestionAutosaveable, Normalizable { public const ANSWER_TYPE_SELECT_VAL = 0; public const ANSWER_TYPE_TEXT_VAL = 1; @@ -833,4 +836,38 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra } return $correct_answers; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'long_menu_text' => $this->long_menu_text, + 'json_structure' => $this->json_structure, + 'min_auto_complete' => $this->minAutoComplete, + 'identical_scoring' => $this->identical_scoring, + 'correct_answers' => $this->correct_answers, + 'answers' => $this->answers, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->long_menu_text = $tt->string($normalized['long_menu_text']); + $clone->json_structure = $tt->string($normalized['json_structure']); + $clone->minAutoComplete = $tt->int($normalized['min_auto_complete']); + $clone->identical_scoring = $tt->bool($normalized['identical_scoring']); + $clone->correct_answers = $normalized['correct_answers']; + $clone->answers = $normalized['answers']; + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php index c5484858e9ef..80979e2b8692 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php @@ -18,11 +18,14 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; use ILIAS\Refinery\Random\Group as RandomGroup; use ILIAS\Refinery\Random\Seed\RandomSeed; +use ILIAS\Refinery\Transformation; /** * Class for matching questions @@ -37,7 +40,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assMatchingQuestion extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable +class assMatchingQuestion extends assQuestion implements ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable, Normalizable { public const MT_TERMS_PICTURES = 0; public const MT_TERMS_DEFINITIONS = 1; @@ -1423,4 +1426,44 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra $this->getMatchingPairs() ); } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'shuffle_mode' => $this->shufflemode, + 'matching_mode' => $this->matching_mode, + 'matching_type' => $this->matching_type, + 'thumb_geometry' => $this->thumb_geometry, + 'matching_pairs' => $tt->normalize($this->matchingpairs, ['question_id' => $this->getId()]), + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->shufflemode = $tt->int($normalized['shuffle_mode']); + $clone->matching_mode = $tt->string($normalized['matching_mode']); + $clone->matching_type = $tt->int($normalized['matching_type']); + $clone->thumb_geometry = $tt->int($normalized['thumb_geometry']); + + foreach ($normalized['matching_pairs'] as $matching_pair) { + $term = $tt->denormalize($matching_pair['term'], new assAnswerMatchingTerm()); + $definition = $tt->denormalize($matching_pair['definition'], new assAnswerMatchingDefinition()); + + $clone->matchingpairs[] = new assAnswerMatchingPair($term, $definition, $tt->float($matching_pair['points'])); + $clone->terms[] = $term; + $clone->definitions[] = $definition; + } + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php index 2d2594587ee1..12a4e4373195 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php @@ -18,11 +18,13 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\ManipulateImagesInChoiceQuestionsTrait; use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\TestQuestionPool\RequestDataCollector; +use ILIAS\Refinery\Transformation; /** * Class for multiple choice tests. @@ -39,7 +41,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assMultipleChoice extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, ilAssSpecificFeedbackOptionLabelProvider, QuestionLMExportable, QuestionAutosaveable +class assMultipleChoice extends assQuestion implements ilObjAnswerScoringAdjustable, iQuestionCondition, ilAssSpecificFeedbackOptionLabelProvider, QuestionLMExportable, QuestionAutosaveable, Normalizable { use ManipulateImagesInChoiceQuestionsTrait; @@ -957,4 +959,35 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra $this->getAnswers() ); } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'selection_limit' => $this->selection_limit, + 'single_line' => $this->is_singleline, + 'answers' => $tt->normalize($this->answers, ['question_id' => $this->getId()]), + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->selection_limit = $tt->nullableInt($normalized['selection_limit']); + $clone->is_singleline = $tt->bool($normalized['single_line']); + $clone->answers = array_map( + fn(array $answer) => $tt->denormalize($answer, new ASS_AnswerMultipleResponseImage()), + $normalized['answers'] + ); + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php b/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php index e2d6e1ee9fe4..cb598474a73e 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php @@ -18,8 +18,11 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; +use ILIAS\Refinery\Transformation; /** * Class for numeric questions @@ -36,7 +39,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assNumeric extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionAutosaveable +class assNumeric extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionAutosaveable, Normalizable { protected $lower_limit; protected $upper_limit; @@ -463,4 +466,32 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): stri { return "{$this->getLowerLimit()}-{$this->getUpperLimit()}"; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'lower_limit' => $this->lower_limit, + 'upper_limit' => $this->upper_limit, + 'maxchars' => $this->maxchars, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->lower_limit = $tt->string($normalized['lower_limit']); + $clone->upper_limit = $tt->string($normalized['upper_limit']); + $clone->maxchars = $tt->int($normalized['maxchars']); + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php b/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php index d08e5d863841..d38540a1d712 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php @@ -18,9 +18,12 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; +use ILIAS\Refinery\Transformation; /** * Class for horizontal ordering questions @@ -33,7 +36,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assOrderingHorizontal extends assQuestion implements ilObjQuestionScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable +class assOrderingHorizontal extends assQuestion implements ilObjQuestionScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable, Normalizable { protected const HAS_SPECIFIC_FEEDBACK = false; protected const DEFAULT_TEXT_SIZE = 100; @@ -582,4 +585,34 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): stri { return $this->getOrderText(); } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'ordertext' => $this->ordertext, + 'textsize' => $this->textsize, + 'separator' => $this->separator, + 'answer_separator' => $this->answer_separator, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->ordertext = $tt->string($normalized['ordertext']); + $clone->textsize = $tt->float($normalized['textsize']); + $clone->separator = $tt->string($normalized['separator']); + $clone->answer_separator = $tt->string($normalized['answer_separator']); + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php index 2fcf3643960d..7837a560ff73 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php @@ -18,10 +18,13 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\Questions\Ordering\OrderingQuestionDatabaseRepository as OQRepository; use ILIAS\Test\Logging\AdditionalInformationGenerator; +use ILIAS\Refinery\Transformation; /** * Class for ordering questions @@ -37,7 +40,7 @@ * * @ingroup components\ILIASTestQuestionPool */ -class assOrderingQuestion extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable +class assOrderingQuestion extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable, Normalizable { public const ORDERING_ELEMENT_FORM_FIELD_POSTVAR = 'order_elems'; @@ -1366,4 +1369,33 @@ function (ilAssOrderingElement $v): string { $elements ); } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'ordering_type' => $this->ordering_type, + 'ordering_elements' => $tt->normalize($this->getOrderingElementList()->getElements(), ['question_id' => $this->getId()]), + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->ordering_type = $tt->int($normalized['ordering_type']); + $clone->getOrderingElementList()->setElements(array_map( + fn(array $element) => $tt->denormalize($element, new ilAssOrderingElement()), + $normalized['ordering_elements'] + )); + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php index 5395d9924ff2..08101ca675f9 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php @@ -18,6 +18,8 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\Test\Results\Data\Repository as TestResultRepository; use ILIAS\Test\TestDIC; use ILIAS\TestQuestionPool\Questions\QuestionPartiallySaveable; @@ -2960,4 +2962,64 @@ public function getVariablesAsTextArray( ): array { return []; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + 'id' => $tt->normalize(new Id($this->id, 'question')), + 'pool_id' => $tt->normalize(new Id($this->obj_id, 'qpl')), + 'original_id' => $this->original_id, + 'external_id' => $this->external_id, + 'type' => $this->getQuestionType(), + 'owner' => $this->owner, + 'title' => $this->title, + 'description' => $this->comment, + 'question_text' => $this->question, + 'available_points' => $this->points, + 'nr_of_tries' => $this->nr_of_tries, + 'lifecycle' => $tt->normalize($this->lifecycle), + 'author' => $this->author, + 'updated_timestamp' => $this->lastChange, + 'additional_content_editing_mode' => $this->additionalContentEditingMode, + 'thumb_size' => $this->thumb_size, + 'shuffle' => $this->shuffle, + 'suggested_solutions' => $tt->normalize($this->suggested_solutions), + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = clone $this; + $clone->id = $tt->denormalize($normalized['id'], Id::class)->getId(); + $clone->obj_id = $tt->denormalize($normalized['pool_id'], Id::class)->getId(); + $clone->original_id = $tt->nullableInt($normalized['original_id']); + $clone->external_id = $tt->nullableString($normalized['external_id']); + $clone->owner = $tt->int($normalized['owner']); + $clone->title = $tt->string($normalized['title']); + $clone->comment = $tt->string($normalized['description']); + $clone->question = $tt->string($normalized['question_text']); + $clone->points = $tt->float($normalized['available_points']); + $clone->nr_of_tries = $tt->int($normalized['nr_of_tries']); + $clone->lifecycle = $tt->denormalize($normalized['lifecycle'], $clone->lifecycle); + $clone->author = $tt->string($normalized['author']); + $clone->lastChange = $tt->nullableInt($normalized['updated_timestamp']); + $clone->additionalContentEditingMode = $tt->string($normalized['additional_content_editing_mode']); + $clone->thumb_size = $tt->int($normalized['thumb_size']); + $clone->shuffle = $tt->bool($normalized['shuffle']); + + $clone->suggested_solutions = array_map( + fn(array $suggested_solution) => $tt->denormalize($suggested_solution, SuggestedSolution::class), + $normalized['suggested_solutions'] + ); + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php index e90cb6c53ec0..8a15886c81a0 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php @@ -18,10 +18,13 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\ManipulateImagesInChoiceQuestionsTrait; use ILIAS\Test\Logging\AdditionalInformationGenerator; +use ILIAS\Refinery\Transformation; /** * Class for single choice questions @@ -36,7 +39,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assSingleChoice extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, ilAssSpecificFeedbackOptionLabelProvider, QuestionLMExportable, QuestionAutosaveable +class assSingleChoice extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, ilAssSpecificFeedbackOptionLabelProvider, QuestionLMExportable, QuestionAutosaveable, Normalizable { use ManipulateImagesInChoiceQuestionsTrait; @@ -944,4 +947,35 @@ function (array $c, ASS_AnswerBinaryStateImage $v): array { [] ); } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): array { + $normalized = $tt->normalize(parent::toNormalized($tt)); + $normalized['is_singleline'] = $this->is_singleline; + $normalized['feedback_setting'] = $this->feedback_setting; + $normalized['answers'] = $tt->normalize($this->answers, ['question_id' => $this->getId()]); + return $normalized; + }); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->is_singleline = $tt->bool($normalized['is_singleline']); + $clone->feedback_setting = $tt->int($normalized['feedback_setting']); + $clone->answers = array_map( + fn(array $answer) => $tt->denormalize($answer, new ASS_AnswerBinaryStateImage()), + $normalized['answers'] + ); + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php index 7f7be1a73ffa..7228f5cb4f04 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php @@ -18,9 +18,12 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; +use ILIAS\Refinery\Transformation; /** * Class for text questions @@ -35,7 +38,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assTextQuestion extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, QuestionLMExportable, QuestionAutosaveable +class assTextQuestion extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, QuestionLMExportable, QuestionAutosaveable, Normalizable { protected const HAS_SPECIFIC_FEEDBACK = false; @@ -855,4 +858,40 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra ); } } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'word_counter_enabled' => $this->word_counter_enabled, + 'max_num_of_chars' => $this->max_num_of_chars, + 'text_rating' => $this->text_rating, + 'matchcondition' => $this->matchcondition, + 'keyword_relation' => $this->keyword_relation, + 'answers' => $tt->normalize($this->answers), + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->word_counter_enabled = $tt->bool($normalized['word_counter_enabled']); + $clone->max_num_of_chars = $tt->int($normalized['max_num_of_chars']); + $clone->text_rating = $tt->string($normalized['text_rating']); + $clone->matchcondition = $tt->int($normalized['matchcondition']); + $clone->keyword_relation = $tt->string($normalized['keyword_relation']); + $clone->answers = array_map( + fn(array $answer) => $tt->denormalize($answer, new ASS_AnswerMultipleResponseImage()), + $normalized['answers'] + ); + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php b/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php index d85b05d91412..09bb06fe7f91 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php @@ -18,9 +18,12 @@ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; +use ILIAS\Refinery\Transformation; /** * Class for TextSubset questions @@ -37,7 +40,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assTextSubset extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable +class assTextSubset extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable, Normalizable { public array $answers = []; public int $correctanswers = 0; @@ -755,4 +758,34 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra { return $this->getAvailableAnswers(); } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + ...$tt->normalize(parent::toNormalized($tt)), + 'text_rating' => $this->text_rating, + 'correct_answers' => $this->correctanswers, + 'answers' => $tt->normalize($this->answers), + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = parent::fromNormalized($tt)->transform($normalized); + $clone->text_rating = $tt->string($normalized['text_rating']); + $clone->correctanswers = $tt->int($normalized['correct_answers']); + $clone->answers = array_map( + fn(array $answer) => $tt->denormalize($answer, new ASS_AnswerBinaryStateImage()), + $normalized['answers'] + ); + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php b/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php index 538c8f556f9b..942a707abc73 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php @@ -1,5 +1,10 @@ getImageWebDir() . $this->getThumbPrefix() . $this->getImageFile(); } + + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(array $context): array => [ + 'position' => $this->position, + 'answertext' => $this->answertext, + 'image' => $this->imageFile ? $tt->normalize( + new QuestionImage($this->imageFile, $context['question_id'] ?? null) + ) : null, + 'correctness' => $this->correctness, + ]); + } + + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = clone $this; + $clone->position = $tt->int($normalized['position']); + $clone->answertext = $tt->nullableString($normalized['answertext']); + $clone->imageFile = $tt->denormalize($normalized['image'], QuestionImage::class)?->getFilename(); + $clone->correctness = $tt->int($normalized['correctness']); + + //TODO: imageFSDir, imageWebDir, thumbPrefix ? + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php index 0d8222ef76e3..2f65ab116890 100755 --- a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php +++ b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php @@ -17,6 +17,11 @@ *********************************************************************/ declare(strict_types=1); +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; +use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; /** * Class represents an ordering element for assOrderingQuestion @@ -25,7 +30,7 @@ * @version $Id$ * @package Modules/TestQuestionPool */ -class ilAssOrderingElement +class ilAssOrderingElement implements Normalizable { public const EXPORT_IDENT_PROPERTY_SEPARATOR = '_'; @@ -440,4 +445,39 @@ public function withContent(string $content): self $clone->content = $content; return $clone; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(array $options): array => [ + 'id' => $tt->normalize(new Id($this->id, 'ordering')), + 'random_identifier' => $this->random_identifier, + 'solution_identifier' => $this->solution_identifier, + 'position' => $this->position, + 'indentation' => $this->indentation, + 'content' => $this->content ? $tt->normalize( + new QuestionImage($this->content, $options['question_id'] ?? null) + ) : null, + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = $this->withRandomIdentifier($tt->int($normalized['random_identifier'])) + ->withSolutionIdentifier($tt->int($normalized['solution_identifier'])) + ->withPosition($tt->int($normalized['position'])) + ->withIndentation($tt->int($normalized['indentation'])); + + $clone->setContent($tt->denormalize($normalized['content'], QuestionImage::class)?->getFilename()); + $clone->setId($tt->denormalize($normalized['id'], Id::class)->getId()); + + return $clone; + }); + } } diff --git a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionLifecycle.php b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionLifecycle.php index 5273eec999b3..7b43c435e033 100755 --- a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionLifecycle.php +++ b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionLifecycle.php @@ -16,12 +16,16 @@ * *********************************************************************/ +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\Refinery\Transformation; + /** * Class ilAssQuestionLifecycle * @author Björn Heyser * @package components\ILIAS/TestQuestionPool */ -class ilAssQuestionLifecycle +class ilAssQuestionLifecycle implements Normalizable { public const DRAFT = 'draft'; public const REVIEW = 'review'; @@ -153,4 +157,24 @@ public static function getDraftInstance(): self return $lifecycle; } + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + 'identifier' => $this->getIdentifier(), + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation( + fn(array $normalized) => self::getInstance($normalized['identifier']) + ); + } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/SuggestedSolutionNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/SuggestedSolutionNormalizer.php new file mode 100644 index 000000000000..5cefef06f6aa --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/SuggestedSolutionNormalizer.php @@ -0,0 +1,139 @@ + + */ +#[Normalizes(SuggestedSolution::class)] +class SuggestedSolutionNormalizer implements Normalizer +{ + public function __construct( + private readonly Transformations $tt, + ) { + } + + /** + * @inheritDoc + */ + public function normalize($value): array|float|bool|int|string|null + { + if (!$value instanceof SuggestedSolution) { + throw new NormalizingException('Invalid value', $value); + } + + $normalized = [ + 'id' => $this->tt->normalize(new Id($value->getId(), 'suggested_solution')), + 'question_id' => $this->tt->normalize(new Id($value->getQuestionId(), 'question')), + 'subquestion_index' => $value->getSubquestionIndex(), + 'import_id' => $value->getImportId(), + 'last_update' => $this->tt->normalize($value->getLastUpdate()), + ]; + + if ($value instanceof SuggestedSolutionLink) { + $normalized['type'] = $value->getType(); + $normalized['internal_link'] = $value->getInternalLink(); + } + + if ($value instanceof SuggestedSolutionFile) { + $normalized['type'] = $value->getType(); + $normalized['title'] = $value->getTitle(); + $normalized['mime'] = $value->getMime(); + $normalized['size'] = $value->getSize(); + + $normalized['file'] = $this->tt->normalize( + new QuestionImage($value->getFilename(), $value->getQuestionId(), QuestionImage::TYPE_SOLUTION) + ); + + } + + return $normalized; + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $value, string $type): SuggestedSolution + { + if ($type !== SuggestedSolution::class && !in_array(SuggestedSolution::class, class_parents($type))) { + throw new NormalizingException("Invalid type: {$type}"); + } + + // If abstract class expected we need to lookup the concrete type from the normalized value + $denormalized_type = $this->tt->string($value['type']); + if ($type === SuggestedSolution::class) { + $type = match($denormalized_type) { + SuggestedSolution::TYPE_FILE => SuggestedSolutionFile::class, + SuggestedSolution::TYPE_LM => SuggestedSolutionLink::class, + SuggestedSolution::TYPE_LM_CHAPTER => SuggestedSolutionLink::class, + SuggestedSolution::TYPE_LM_PAGE => SuggestedSolutionLink::class, + SuggestedSolution::TYPE_GLOSARY_TERM => SuggestedSolutionLink::class, + default => throw new NormalizingException("Invalid denormalized type: {$denormalized_type}"), + }; + } + + $id = $this->tt->denormalize($value['id'], Id::class)->getId(); + $question_id = $this->tt->denormalize($value['question_id'], Id::class)->getId(); + $subquestion_index = $this->tt->int($value['subquestion_index']); + $import_id = $this->tt->string($value['import_id']); + $last_update = $this->tt->denormalize($value['last_update'], DateTimeImmutable::class); + + switch ($type) { + case SuggestedSolutionFile::class: + return new SuggestedSolutionFile( + $id, + $question_id, + $subquestion_index, + $import_id, + $last_update, + $denormalized_type, + '' + ) + ->withTitle($this->tt->string($value['title'])) + ->withFilename($this->tt->denormalize($value['file'], QuestionImage::class)->getFilename()) + ->withMime($this->tt->string($value['mime'])) + ->withSize($this->tt->int($value['size'])); + case SuggestedSolutionLink::class: + return new SuggestedSolutionLink( + $id, + $question_id, + $subquestion_index, + $import_id, + $last_update, + $denormalized_type, + $this->tt->string($value['internal_link']) + ); + default: + throw new NormalizingException("Invalid type: {$type}"); + } + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php new file mode 100644 index 000000000000..43bbd70f552c --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php @@ -0,0 +1,145 @@ + + */ +#[Normalizes(ilAssQuestionSkillAssignment::class)] +class ilAssQuestionSkillAssignmentNormalizer implements Normalizer +{ + private readonly ilDBInterface $db; + + public function __construct( + private readonly Transformations $tt, + Container $dic + ) { + $this->db = $dic->database(); + } + + /** + * @inheritDoc + */ + public function normalize($value): array|float|bool|int|string|null + { + if (!$value instanceof ilAssQuestionSkillAssignment) { + throw new NormalizingException('Invalid value', $value); + } + + $normalized = [ + 'parent_id' => $this->tt->normalize(new Id($value->getParentObjId(), 'object')), + 'question_id' => $this->tt->normalize(new Id($value->getQuestionId(), 'question')), + 'base_id' => $value->getSkillBaseId(), + 'tref_id' => $value->getSkillTrefId(), + 'original_title' => $value->getSkillTitle(), + 'original_path' => $value->getSkillPath(), + 'eval_mode' => $value->getEvalMode(), + ]; + + switch ($value->getEvalMode()) { + case ilAssQuestionSkillAssignment::EVAL_MODE_BY_QUESTION_RESULT: + $normalized['points'] = $value->getSkillPoints(); + break; + + case ilAssQuestionSkillAssignment::EVAL_MODE_BY_QUESTION_SOLUTION: + $normalized['solution_comparison_expressions'] = $this->normalizeExpressionList($value); + break; + } + return $normalized; + } + + private function normalizeExpressionList(ilAssQuestionSkillAssignment $value): array + { + $value->initSolutionComparisonExpressionList(); + + $list = []; + foreach ($value->getSolutionComparisonExpressionList()->get() as $expression) { + $list[] = [ + 'points' => $expression->getPoints(), + 'expression' => $expression->getExpression(), + 'order_index' => $expression->getOrderIndex(), + ]; + } + + return $list; + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $value, string $type): ilAssQuestionSkillAssignment + { + if ($type !== ilAssQuestionSkillAssignment::class) { + throw new NormalizingException("Invalid type for ilAssQuestionSkillAssignment: {$type}"); + } + + $assignment = new ilAssQuestionSkillAssignment($this->db); + $assignment->setParentObjId($this->tt->denormalize($value['parent_id'], Id::class)->getId()); + $assignment->setQuestionId($this->tt->denormalize($value['question_id'], Id::class)->getId()); + $assignment->setSkillBaseId($this->tt->int($value['base_id'])); + $assignment->setSkillTrefId($this->tt->int($value['tref_id'])); + $assignment->setSkillTitle($this->tt->string($value['original_title'])); + $assignment->setSkillPath($this->tt->string($value['original_path'])); + $assignment->setEvalMode($this->tt->string($value['eval_mode'])); + $assignment->initSolutionComparisonExpressionList(); + + switch ($assignment->getEvalMode()) { + case ilAssQuestionSkillAssignment::EVAL_MODE_BY_QUESTION_RESULT: + $assignment->setSkillPoints($this->tt->int($value['points'])); + break; + + case ilAssQuestionSkillAssignment::EVAL_MODE_BY_QUESTION_SOLUTION: + $list = $assignment->getSolutionComparisonExpressionList(); + foreach ($value['solution_comparison_expressions'] as $normalized) { + $list->add($this->denormalizeExpression($normalized, $assignment)); + } + break; + } + + return $assignment; + } + + private function denormalizeExpression( + array $normalized, + ilAssQuestionSkillAssignment $assignment + ): ilAssQuestionSolutionComparisonExpression { + $expression = new ilAssQuestionSolutionComparisonExpression(); + $expression->setQuestionId($assignment->getQuestionId()); + $expression->setSkillBaseId($assignment->getSkillBaseId()); + $expression->setSkillTrefId($assignment->getSkillTrefId()); + + $expression->setOrderIndex($this->tt->int($normalized['order_index'])); + $expression->setExpression($this->tt->string($normalized['expression'])); + $expression->setPoints($this->tt->int($normalized['points'])); + + return $expression; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilObjQuestionPoolNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilObjQuestionPoolNormalizer.php new file mode 100644 index 000000000000..d4cfb429eadd --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilObjQuestionPoolNormalizer.php @@ -0,0 +1,65 @@ + + */ +#[Normalizes(ilObjQuestionPool::class)] +class ilObjQuestionPoolNormalizer extends IlObjectNormalizer implements Normalizer +{ + /** + * @inheritDoc + */ + public function normalize($value): array|float|bool|int|string|null + { + if (!$value instanceof ilObjQuestionPool) { + throw new NormalizingException('Invalid value', $value); + } + + $normalized = parent::normalize($value); + $normalized['skill_service_enabled'] = $value->isSkillServiceEnabled(); + + return $normalized; + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $value, string $type): ilObjQuestionPool + { + if ($type !== ilObjQuestionPool::class) { + throw new NormalizingException("Invalid type for ilObjQuestionPool: {$type}"); + } + + /** @var ilObjQuestionPool $object */ + $object = parent::denormalize($value, ilObjQuestionPool::class); + $object->setSkillServiceEnabled($this->tt->bool($value['skill_service_enabled'])); + + return $object; + } +} From 56e7fc0a1b76b3762eb7c9c6263795d0f5301167 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Thu, 9 Apr 2026 12:45:45 +0200 Subject: [PATCH 03/66] feat(qpl): implement question pool export and integrate core export components --- .../classes/class.ilQuestionpoolExport.php | 186 -------------- .../class.ilTestQuestionPoolExporter.php | 147 ++++++------ .../src/ExportImport/Envelopes/Feedback.php | 84 +++++++ .../Pipes/CollectQuestionImages.php | 93 +++++++ .../ExportImport/QuestionPoolCollector.php | 226 ++++++++++++++++++ .../src/ExportImport/QuestionPoolExporter.php | 206 ++++++++++++++++ .../TestQuestionPool/src/QuestionPoolDIC.php | 15 ++ 7 files changed, 694 insertions(+), 263 deletions(-) delete mode 100755 components/ILIAS/TestQuestionPool/classes/class.ilQuestionpoolExport.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/Feedback.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolCollector.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilQuestionpoolExport.php b/components/ILIAS/TestQuestionPool/classes/class.ilQuestionpoolExport.php deleted file mode 100755 index acaf7943deae..000000000000 --- a/components/ILIAS/TestQuestionPool/classes/class.ilQuestionpoolExport.php +++ /dev/null @@ -1,186 +0,0 @@ - -* -* @version $Id$ -* -* @ingroup components\ILIASTestQuestionPool -*/ -class ilQuestionpoolExport -{ - public ilErrorHandling $err; // error object - public ilDBInterface $db; // database object - public ILIAS $ilias; // ilias object - public string $inst_id; // installation id - public ilLanguage $lng; - - private string $export_dir = ''; - private string $subdir = ''; - private string $filename = ''; - private string $zipfilename = ''; - private string $qti_filename = ''; - private ilXmlWriter $xml; - - /** - * Constructor - * @access public - */ - public function __construct( - protected ilObjQuestionPool $qpl_obj, - protected string $mode = "xml", - protected ?array $questions = null - ) { - global $DIC; - $this->err = $DIC['ilErr']; - $this->db = $DIC['ilDB']; - $this->ilias = $DIC['ilias']; - $this->lng = $DIC['lng']; - - if (!is_array($this->questions)) { - $this->questions = $this->qpl_obj->getAllQuestionIds(); - } - - $this->inst_id = IL_INST_ID; - $date = time(); - - $this->qpl_obj->createExportDirectory(); - switch ($this->mode) { - case "xml": - $this->export_dir = $this->qpl_obj->getExportDirectory('xml'); - $this->subdir = $date . "__" . $this->inst_id . "__" . - "qpl" . "_" . $this->qpl_obj->getId(); - $this->filename = $this->subdir . ".xml"; - $this->qti_filename = $date . "__" . $this->inst_id . "__" . - "qti" . "_" . $this->qpl_obj->getId() . ".xml"; - break; - case "xlsx": - $this->export_dir = $this->qpl_obj->getExportDirectory('xlsx'); - $this->filename = $date . "__" . $this->inst_id . "__" . - "qpl" . "_" . $this->qpl_obj->getId() . ".xlsx"; - $this->zipfilename = $date . "__" . $this->inst_id . "__" . - "qpl" . "_" . $this->qpl_obj->getId() . ".zip"; - break; - default: - $this->export_dir = $this->qpl_obj->getExportDirectory('zip'); - $this->subdir = $date . "__" . $this->inst_id . "__" . - "qpl" . "_" . $this->qpl_obj->getId(); - $this->filename = $this->subdir . ".xml"; - $this->qti_filename = $date . "__" . $this->inst_id . "__" . - "qti" . "_" . $this->qpl_obj->getId() . ".xml"; - break; - } - } - - public function getInstId() - { - return $this->inst_id; - } - - - /** - * build export file (complete zip file) - */ - public function buildExportFile(): string - { - switch ($this->mode) { - case "xlsx": - return $this->buildExportFileXLSX(); - case "xml": - default: - return $this->buildExportFileXML(); - } - } - - /** - * build xml export file - */ - public function buildExportFileXML(): string - { - global $DIC; - $ilBench = $DIC['ilBench']; - - $ilBench->start("QuestionpoolExport", "buildExportFile"); - - $this->xml = new ilXmlWriter(); - $this->xml->xmlSetDtdDef(""); - $this->xml->xmlSetGenCmt("Export of ILIAS Test Questionpool " . - $this->qpl_obj->getId() . " of installation " . $this->inst_id); - $this->xml->xmlHeader(); - - ilFileUtils::makeDir($this->export_dir . "/" . $this->subdir); - ilFileUtils::makeDir($this->export_dir . "/" . $this->subdir . "/objects"); - - $expDir = $this->qpl_obj->getExportDirectory(); - ilFileUtils::makeDirParents($expDir); - - $expLog = new ilLog($expDir, "export.log"); - $expLog->delete(); - $expLog->setLogFormat(""); - $expLog->write(date("[y-m-d H:i:s] ") . "Start Export"); - - $qti_file = fopen($this->export_dir . "/" . $this->subdir . "/" . $this->qti_filename, "w"); - fwrite($qti_file, $this->qpl_obj->questionsToXML($this->questions)); - fclose($qti_file); - - $ilBench->start("QuestionpoolExport", "buildExportFile_getXML"); - $this->qpl_obj->objectToXmlWriter( - $this->xml, - $this->inst_id, - $this->export_dir . "/" . $this->subdir, - $expLog, - $this->questions - ); - $ilBench->stop("QuestionpoolExport", "buildExportFile_getXML"); - - $ilBench->start("QuestionpoolExport", "buildExportFile_dumpToFile"); - $this->xml->xmlDumpFile($this->export_dir . "/" . $this->subdir . "/" . $this->filename, false); - $ilBench->stop("QuestionpoolExport", "buildExportFile_dumpToFile"); - - $ilBench->start("QuestionpoolExport", "buildExportFile_saveAdditionalMobs"); - $this->exportXHTMLMediaObjects($this->export_dir . "/" . $this->subdir); - $ilBench->stop("QuestionpoolExport", "buildExportFile_saveAdditionalMobs"); - - $ilBench->start("QuestionpoolExport", "buildExportFile_zipFile"); - ilFileUtils::zip($this->export_dir . "/" . $this->subdir, $this->export_dir . "/" . $this->subdir . ".zip"); - - $ilBench->stop("QuestionpoolExport", "buildExportFile_zipFile"); - - $expLog->write(date("[y-m-d H:i:s] ") . "Finished Export"); - $ilBench->stop("QuestionpoolExport", "buildExportFile"); - - return $this->export_dir . "/" . $this->subdir . ".zip"; - } - - public function exportXHTMLMediaObjects($a_export_dir): void - { - foreach ($this->questions as $question_id) { - $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $question_id); - foreach ($mobs as $mob) { - if (ilObjMediaObject::_exists($mob)) { - $mob_obj = new ilObjMediaObject($mob); - $mob_obj->exportFiles($a_export_dir); - unset($mob_obj); - } - } - } - } -} diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php index 9ad04c2ce93b..8d4536da9a38 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php @@ -16,6 +16,12 @@ * *********************************************************************/ +use ILIAS\Data\ObjectId; +use ILIAS\TestQuestionPool\ExportImport\Foundation\ExportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\SimpleXMLSerializer; +use ILIAS\TestQuestionPool\ExportImport\QuestionPoolExporter; +use ILIAS\TestQuestionPool\QuestionPoolDIC; + /** * Used for container export with tests * @@ -25,105 +31,54 @@ */ class ilTestQuestionPoolExporter extends ilXmlExporter { - private $ds; + private QuestionPoolExporter $exporter; /** - * Initialisation + * @var array $batches */ + private array $batches = []; + + public function init(): void { + $this->exporter = QuestionPoolDIC::dic()['exportimport.exporter']; } /** - * Overwritten for qpl - * @param string $a_obj_type - * @param int $a_obj_id - * @param string $a_export_type - */ - public static function lookupExportDirectory(string $a_obj_type, int $a_obj_id, string $a_export_type = 'xml', string $a_entity = ""): string - { - if ($a_export_type == 'xml') { - return ilFileUtils::getDataDir() . "/qpl_data" . "/qpl_" . $a_obj_id . "/export_zip"; - } - return ilFileUtils::getDataDir() . "/qpl_data" . "/qpl_" . $a_obj_id . "/export_" . $a_export_type; - } - - - /** - * Get xml representation - * @param string entity - * @param string schema version - * @param string id - * @return string xml string + * Returns the final XML content for one question pool. + * + * This method is called after `getXmlExportTailDependencies()`. At this point the export writer and export + * directory are available, so the prepared batch can be written to disk and finalized. */ public function getXmlRepresentation(string $a_entity, string $a_schema_version, string $a_id): string { - $qpl = new ilObjQuestionPool($a_id, false); - $qpl->loadFromDb(); - - $qpl_exp = new ilQuestionpoolExport($qpl, 'xml'); - $qpl_exp->buildExportFile(); + if ($a_entity !== 'qpl') { + throw new InvalidArgumentException("Invalid entity for question pool export: {$a_entity}"); + } - global $DIC; /* @var ILIAS\DI\Container $DIC */ - $DIC['ilLog']->info(': Created zip file'); - return ''; // sagt mjansen + return $this->finalizeExport((int) $a_id)->getContent(); } /** - * Get tail dependencies - * @param string entity - * @param string target release - * @param array ids - * @return array array of array with keys "component", entity", "ids" + * Collects export tail dependencies for one or more question pools. + * + * The export framework calls this method before `getXmlRepresentation()`. Therefore this method only prepares and + * processes the export batch in memory and caches the context, because writer and export directory are not yet + * initialized here. */ public function getXmlExportTailDependencies(string $a_entity, string $a_target_release, array $a_ids): array { - if ($a_entity == 'qpl') { - $deps = []; - - $taxIds = $this->getDependingTaxonomyIds($a_ids); - - if (count($taxIds)) { - $deps[] = [ - 'component' => 'components/ILIAS/Taxonomy', - 'entity' => 'tax', - 'ids' => $taxIds - ]; - } - - $md_ids = []; - foreach ($a_ids as $id) { - $md_ids[] = $id . ':0:qpl'; - } - if ($md_ids !== []) { - $deps[] = [ - 'component' => 'components/ILIAS/MetaData', - 'entity' => 'md', - 'ids' => $md_ids - ]; - } - - return $deps; + if ($a_entity !== 'qpl') { + throw new InvalidArgumentException("Invalid entity for question pool export: {$a_entity}"); } - return parent::getXmlExportTailDependencies($a_entity, $a_target_release, $a_ids); - } - - /** - * @param array $testObjIds - * @return array $taxIds - */ - private function getDependingTaxonomyIds($poolObjIds): array - { - $taxIds = []; - - foreach ($poolObjIds as $poolObjId) { - foreach (ilObjTaxonomy::getUsageOfObject($poolObjId) as $taxId) { - $taxIds[$taxId] = $taxId; - } + $dependencies = []; + foreach ($a_ids as $id) { + $context = $this->processExport((int) $id); + $dependencies = array_merge($dependencies, $context->getDependencies()); } - return $taxIds; + return $dependencies; } /** @@ -143,4 +98,42 @@ public function getValidSchemaVersions(string $a_entity): array "max" => ""] ]; } + + /** + * Prepares and processes a question pool export in memory. The resulting context is cached per pool and reused + * across calls. + */ + private function processExport(int $pool_id): ExportContext + { + if (isset($this->batches[$pool_id])) { + return $this->batches[$pool_id]; + } + + $context = $this->exporter->prepare( + new ObjectId($pool_id), + $this->exp->getExportConfigs() + ); + + $context = $this->exporter->process( + $context, + new SimpleXMLSerializer()->open('memory') + ); + + $this->batches[$pool_id] = $context; + return $context; + } + + /** + * Finalizes a prepared export context and writes it to the export directory. + */ + private function finalizeExport(int $pool_id): ExportContext + { + $context = $this->processExport($pool_id); + + return $this->exporter->write( + $context, + $this->exp->getExportWriter(), + $this->exp->getPathToComponentExpDirInContainer() + ); + } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/Feedback.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/Feedback.php new file mode 100644 index 000000000000..7450284c25d0 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/Feedback.php @@ -0,0 +1,84 @@ + */ + private array $specific_feedback = [], + ) { + } + + public function getGenericUncompleted(): string + { + return $this->generic_uncompleted; + } + + public function getGenericCompleted(): string + { + return $this->generic_completed; + } + + /** + * @return list + */ + public function getSpecificFeedback(): array + { + return $this->specific_feedback; + } + + /** + * @inheritDoc + */ + public function toArray(Transformations $tt): array + { + return [ + 'question_id' => $tt->normalize($this->question_id), + 'generic_uncompleted' => $this->generic_uncompleted, + 'generic_completed' => $this->generic_completed, + 'specific' => $this->specific_feedback, + ]; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $value, Transformations $tt): static + { + return new self( + $tt->denormalize($value['question_id'], Id::class), + $value['generic_uncompleted'], + $value['generic_completed'], + $value['specific'], + ); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php new file mode 100644 index 000000000000..5b82ef56272d --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php @@ -0,0 +1,93 @@ + $files + */ + private array $files = []; + + public function __construct( + private readonly Factory $uuid_factory, + private readonly ObjectId $pool_id, + ) { + $this->question_files = new QuestionFiles(); + } + + /** + * @inheritDoc + */ + public function handle(mixed $passable, \Closure $next): mixed + { + if ($passable instanceof NormalizeCarry && $passable->value instanceof QuestionImage) { + $this->handleNormalization($passable->value); + } + + return $next($passable); + } + + private function handleNormalization(QuestionImage $envelope): void + { + // Build the absolute source path + $base_dir = $this->question_files->buildImagePath( + $envelope->getQuestionId(), + $this->pool_id->toInt() + ); + + if ($envelope->getType() === QuestionImage::TYPE_SOLUTION) { + $base_dir = str_replace('images/', 'solution/', $base_dir); + } + + $source_path = $base_dir . $envelope->getFilename(); + + // Generate a unique ID for the image and set it on the envelope and the relative target path + $id = $this->uuid_factory->uuid4(); + $envelope->setId($id->toString()); + + $extension = pathinfo($envelope->getFilename(), PATHINFO_EXTENSION); + $target_path = $id->toString() . '.' . $extension; + + $this->files[] = ['from' => $source_path, 'to' => $target_path]; + } + + /** + * @return list + */ + public function getFiles(): array + { + return $this->files; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolCollector.php b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolCollector.php new file mode 100644 index 000000000000..ecba542b2c15 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolCollector.php @@ -0,0 +1,226 @@ + $questions */ + private ?array $questions = null; + private ?ilObjQuestionPool $pool_object = null; + private ?ilAssQuestionSkillAssignmentList $skill_assignments = null; + + public function __construct( + private readonly GeneralQuestionPropertiesRepository $question_repository, + private readonly ilDBInterface $db, + private readonly ObjectId $pool_id + ) { + } + + /** + * Get the ID of the question pool. + * + * @return ObjectId + */ + public function getPoolId(): ObjectId + { + return $this->pool_id; + } + + /** + * Get the object of the question pool. It will be loaded from the database if not already loaded. + */ + public function getObject(): ilObjQuestionPool + { + if ($this->pool_object === null) { + $this->pool_object = new ilObjQuestionPool($this->pool_id->toInt(), false); + $this->pool_object->read(); + } + + return $this->pool_object; + } + + /** + * Collect the question properties for all questions in the question pool. + * + * @return array + */ + public function getQuestionProperties(): array + { + if ($this->questions === null) { + $this->questions = $this->question_repository->getForParentObjectId($this->pool_id->toInt()); + } + return $this->questions; + } + + /** + * Collect the question objects for all questions in the question pool. + * + * @return Generator + */ + public function getQuestionObjects(): Generator + { + foreach ($this->getQuestionProperties() as $question) { + yield assQuestion::instantiateQuestion($question->getQuestionId()); + } + } + + /* + Units + */ + + /** + * Collect the unit categories and their units for all formula questions in the question pool. + * + * @return Generator<\assFormulaQuestionUnit|\assFormulaQuestionUnitCategory> + */ + public function getUnits(): Generator + { + foreach ($this->getQuestionProperties() as $question) { + if ($question->getClassName() === 'assFormulaQuestion') { + $repository = new ilUnitConfigurationRepository($question->getQuestionId()); + yield from $repository->getCategorizedUnits(); + } + } + } + + /* + Feedback + */ + + /** + * Collect the feedback content for a question and return it as a Feedback transfer object. + */ + public function getFeedback(assQuestion $question): Feedback + { + $feedback = new Feedback( + new Id($question->getId(), 'question'), + $question->feedbackOBJ->getGenericFeedbackExportPresentation($question->getId(), false), + $question->feedbackOBJ->getGenericFeedbackExportPresentation($question->getId(), true), + $this->loadSpecificFeedback($question), + ); + + return $feedback; + } + + private function loadSpecificFeedback(assQuestion $question): array + { + // Skip if specific feedback is not available or supported by the question type. + if ( + !$question->feedbackOBJ instanceof ilAssMultiOptionQuestionFeedback || + !$question->feedbackOBJ->isSpecificAnswerFeedbackAvailable($question->getId()) + ) { + return []; + } + + // Cloze question type specific feedback uses the identifier list to load the answer-specific feedback. + if ($question->feedbackOBJ instanceof ilAssClozeTestFeedback) { + $feedback_list = new ilAssSpecificFeedbackIdentifierList(); + $feedback_list->load($question->getId()); + + $feedback = []; + foreach ($feedback_list as $identifier) { + $feedback[$identifier->getAnswerIndex()] = [ + 'answer_index' => $identifier->getAnswerIndex(), + 'question_index' => $identifier->getQuestionIndex(), + 'feedback' => $question->feedbackOBJ->getSpecificAnswerFeedbackExportPresentation( + $question->getId(), + $identifier->getQuestionIndex(), + $identifier->getAnswerIndex() + ), + ]; + } + return $feedback; + } + + // Other question types with multiple answer options share the same approach + foreach (array_keys($question->feedbackOBJ->getAnswerOptionsByAnswerIndex()) as $answer_index) { + $feedback[$answer_index] = [ + 'answer_index' => $answer_index, + 'question_index' => 0, + 'feedback' => $question->feedbackOBJ->getSpecificAnswerFeedbackExportPresentation( + $question->getId(), + 0, + $answer_index + ), + ]; + } + return $feedback; + } + + /* + Skill Assignments + */ + + /** + * @return array<\ilAssQuestionSkillAssignment> + */ + public function getSkillAssignments(): array + { + if ($this->skill_assignments === null) { + $this->skill_assignments = new ilAssQuestionSkillAssignmentList($this->db); + $this->skill_assignments->setParentObjId($this->pool_id->toInt()); + $this->skill_assignments->loadFromDb(); + $this->skill_assignments->loadAdditionalSkillData(); + } + + $assignments = []; + foreach ($this->getQuestionProperties() as $question) { + $assignments = array_merge( + $assignments, + $this->skill_assignments->getAssignmentsByQuestionId($question->getQuestionId()) + ); + } + + return $assignments; + } + + /* + CO Page & Media Objects + */ + + public function getQuestionPageIds(): array + { + $question_page_ids = []; + foreach ($this->getQuestionObjects() as $question) { + $question_page_ids[] = "qpl:{$question->getId()}"; + } + return $question_page_ids; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php new file mode 100644 index 000000000000..441d942f6b05 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php @@ -0,0 +1,206 @@ +prepare($pool_id, $config); + $context = $this->process($context, $serializer); + return $this->write($context, $writer, $export_dir); + } + + /** + * Prepares the export context by creating the transformations and the question image pipe. It returns the export + * context which is used to share the context between the prepare, process and write steps. + */ + public function prepare(ObjectId $pool_id, ExportConfig $config): ExportContext + { + $question_image_pipe = new CollectQuestionImages( + new UUIDFactory(), + $pool_id + ); + + $transformations = $this->builder->withAdditionalPipes([$question_image_pipe]) + ->create(); + + return new ExportContext($pool_id, $config, $transformations); + } + + /** + * Normalizes the question pool object and its questions and writes them to the serializer. It also collects the + * dependencies of the export. + */ + public function process(ExportContext $context, Serializer $serializer): ExportContext + { + $context->setSerializer($serializer); + $tt = $context->getTransformations(); + + $collector = new QuestionPoolCollector( + $this->question_repository, + $this->db, + $context->getPoolId() + ); + + $serializer->group( + 'general', + fn() => $this->exportObject($collector, $tt, $serializer, $context) + ); + $serializer->group( + 'units', + fn() => $this->exportUnits($collector, $tt, $serializer) + ); + $serializer->group( + 'questions', + fn() => $this->exportQuestions($collector, $tt, $serializer, $context) + ); + $serializer->group( + 'skill_assignments', + fn() => $this->exportSkillAssignments($collector, $tt, $serializer) + ); + + return $context; + } + + /** + * Finalizes the export by copying the question images to the export directory and returning the export context. + */ + public function write(ExportContext $export, ExportWriter $writer, string $export_dir): ExportContext + { + // Copy the question images to the export directory + $question_image_pipe = $export->getTransformations()->context(CollectQuestionImages::class); + foreach ($question_image_pipe->getFiles() as $file) { + $writer->writeFileByFilePath($file['from'], "{$export_dir}/" . $file['to']); + } + + return $export; + } + + + protected function exportObject( + QuestionPoolCollector $collector, + Transformations $transformations, + Serializer $serializer, + ExportContext $export + ): void { + $serializer->append('object', $transformations->normalize($collector->getObject())); + + $obj_id = $collector->getPoolId()->toInt(); + + $export->addDependency('components/ILIAS/ILIASObject', 'common', [$obj_id]); + $export->addDependency('components/ILIAS/MetaData', 'qpl', ["{$obj_id}:0:qpl"]); + $export->addDependency( + 'components/ILIAS/Taxonomy', + 'tax', + $this->taxonomy->getUsageOfObject($obj_id) + ); + } + + protected function exportUnits( + QuestionPoolCollector $collector, + Transformations $transformations, + Serializer $serializer + ): void { + $categories = []; + + foreach ($collector->getUnits() as $item) { + if($item instanceof assFormulaQuestionUnitCategory) { + $categories[$item->getId()] = $transformations->normalize($item); + } + + if($item instanceof assFormulaQuestionUnit) { + $categories[$item->getCategory()]['units'][] = $transformations->normalize($item); + } + } + + foreach($categories as $category) { + $serializer->append('category', $category); + } + } + + protected function exportQuestions( + QuestionPoolCollector $collector, + Transformations $transformations, + Serializer $serializer, + ExportContext $export + ): void { + foreach ($collector->getQuestionObjects() as $question) { + $serializer->append('question', [ + ... $transformations->normalize($question), + 'feedback' => $transformations->normalize( + $collector->getFeedback($question) + ) + ]); + + $export->addDependency('components/ILIAS/COPage', 'pg', ["qpl:{$question->getId()}"]); + } + } + + protected function exportSkillAssignments( + QuestionPoolCollector $collector, + Transformations $transformations, + Serializer $serializer, + ): void { + foreach ($collector->getSkillAssignments() as $assignment) { + $serializer->append('skill_assignment', $transformations->normalize($assignment)); + } + } +} diff --git a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php index f2cce4c0a366..1f32c1fbb89f 100755 --- a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php +++ b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php @@ -20,6 +20,8 @@ namespace ILIAS\TestQuestionPool; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; +use ILIAS\TestQuestionPool\ExportImport\QuestionPoolExporter; use Pimple\Container as PimpleContainer; use ILIAS\DI\Container as ILIASContainer; use ILIAS\TestQuestionPool\Questions\SuggestedSolution\SuggestedSolutionsDatabaseRepository; @@ -67,6 +69,19 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic['global_test_settings'] = static fn($c): GlobalTestSettings => (new GlobalTestSettingsRepository($DIC['ilSetting'], new \ilSetting('assessment')))->getGlobalSettings(); + $dic['exportimport.builder'] = static fn($c): Builder => + new Builder( + $DIC, + $c + ); + $dic['exportimport.exporter'] = static fn($c): QuestionPoolExporter => + new QuestionPoolExporter( + $c['exportimport.builder'], + $c['question.general_properties.repository'], + $DIC->database(), + $DIC->taxonomy()->domain() + ); + return $dic; } } From 46468f454c8855f608c77172a021706df49a6e51 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Thu, 9 Apr 2026 12:55:00 +0200 Subject: [PATCH 04/66] feat(qpl): implement question pool import and integrate user interface steps --- .../classes/class.ilObjQuestionPool.php | 257 ------------------ .../classes/class.ilObjQuestionPoolGUI.php | 167 +++++------- .../class.ilObjQuestionPoolXMLParser.php | 138 ---------- .../class.ilTestQuestionPoolImporter.php | 152 ++--------- .../ExportImport/Envelopes/QuestionImage.php | 94 +++++++ .../src/ExportImport/Import/PersistStage.php | 75 +++++ .../Import/QuestionSelectionStage.php | 181 ++++++++++++ .../Import/UploadValidationStage.php | 101 +++++++ .../src/ExportImport/QuestionPoolImporter.php | 191 +++++++++++++ .../ExportImport/SkillAssignmentsImporter.php | 131 +++++++++ .../TestQuestionPool/src/QuestionPoolDIC.php | 20 ++ lang/ilias_de.lang | 2 + lang/ilias_en.lang | 2 + 13 files changed, 890 insertions(+), 621 deletions(-) delete mode 100755 components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolXMLParser.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/QuestionImage.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/SkillAssignmentsImporter.php diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPool.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPool.php index 2aeefb63a399..2a666800da05 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPool.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPool.php @@ -35,8 +35,6 @@ class ilObjQuestionPool extends ilObject private ilComponentRepository $component_repository; private ilBenchmark $benchmark; - private array $mob_ids; - private array $file_ids; private bool $skill_service_enabled; private GeneralQuestionPropertiesRepository $questionrepository; @@ -305,246 +303,6 @@ public function getPrintviewQuestions(): array return $rows; } - /** - * @param ilXmlWriter $xmlWriter - */ - private function exportXMLSettings($xmlWriter): void - { - $xmlWriter->xmlStartTag('Settings'); - $xmlWriter->xmlElement('SkillService', null, (int) $this->isSkillServiceEnabled()); - $xmlWriter->xmlEndTag('Settings'); - } - - /** - * export pages of test to xml (see ilias_co.dtd) - * - * @param object $a_xml_writer ilXmlWriter object that receives the - * xml data - */ - public function objectToXmlWriter(ilXmlWriter &$a_xml_writer, $a_inst, $a_target_dir, &$expLog, $questions): void - { - $ilBench = $this->benchmark; - - $this->mob_ids = []; - $this->file_ids = []; - - $attrs = []; - $attrs['Type'] = 'Questionpool_Test'; - $a_xml_writer->xmlStartTag('ContentObject', $attrs); - - // MetaData - $this->exportTitleAndDescription($a_xml_writer); - - // Settings - $this->exportXMLSettings($a_xml_writer); - - // PageObjects - $expLog->write(date('[y-m-d H:i:s] ') . 'Start Export Page Objects'); - $ilBench->start('ContentObjectExport', 'exportPageObjects'); - $this->exportXMLPageObjects($a_xml_writer, $a_inst, $expLog, $questions); - $ilBench->stop('ContentObjectExport', 'exportPageObjects'); - $expLog->write(date('[y-m-d H:i:s] ') . 'Finished Export Page Objects'); - - // MediaObjects - $expLog->write(date('[y-m-d H:i:s] ') . 'Start Export Media Objects'); - $ilBench->start('ContentObjectExport', 'exportMediaObjects'); - $this->exportXMLMediaObjects($a_xml_writer, $a_inst, $a_target_dir, $expLog); - $ilBench->stop('ContentObjectExport', 'exportMediaObjects'); - $expLog->write(date('[y-m-d H:i:s] ') . 'Finished Export Media Objects'); - - // FileItems - $expLog->write(date('[y-m-d H:i:s] ') . 'Start Export File Items'); - $ilBench->start('ContentObjectExport', 'exportFileItems'); - $this->exportFileItems($a_target_dir, $expLog); - $ilBench->stop('ContentObjectExport', 'exportFileItems'); - $expLog->write(date('[y-m-d H:i:s] ') . 'Finished Export File Items'); - - // skill assignments - $this->populateQuestionSkillAssignmentsXml($a_xml_writer, $questions); - - $a_xml_writer->xmlEndTag('ContentObject'); - } - - /** - * @param ilXmlWriter $a_xml_writer - * @param $questions - */ - protected function populateQuestionSkillAssignmentsXml(ilXmlWriter &$a_xml_writer, $questions): void - { - $assignmentList = new ilAssQuestionSkillAssignmentList($this->db); - $assignmentList->setParentObjId($this->getId()); - $assignmentList->loadFromDb(); - $assignmentList->loadAdditionalSkillData(); - - $skillQuestionAssignmentExporter = new ilAssQuestionSkillAssignmentExporter(); - $skillQuestionAssignmentExporter->setXmlWriter($a_xml_writer); - $skillQuestionAssignmentExporter->setQuestionIds($questions); - $skillQuestionAssignmentExporter->setAssignmentList($assignmentList); - $skillQuestionAssignmentExporter->export(); - } - - public function exportTitleAndDescription(ilXmlWriter &$a_xml_writer): void - { - $a_xml_writer->xmlElement('Title', null, $this->getTitle()); - $a_xml_writer->xmlElement('Description', null, $this->getDescription()); - } - - public function modifyExportIdentifier($a_tag, $a_param, $a_value) - { - if ($a_tag == 'Identifier' && $a_param == 'Entry') { - $a_value = ilUtil::insertInstIntoID($a_value); - } - - return $a_value; - } - - /** - * export page objects to xml (see ilias_co.dtd) - * - * @param object $a_xml_writer ilXmlWriter object that receives the - * xml data - */ - public function exportXMLPageObjects(&$a_xml_writer, $a_inst, &$expLog, $questions): void - { - $ilBench = $this->benchmark; - - foreach ($questions as $question_id) { - $ilBench->start('ContentObjectExport', 'exportPageObject'); - $expLog->write(date('[y-m-d H:i:s] ') . 'Page Object ' . $question_id); - - $attrs = []; - $a_xml_writer->xmlStartTag('PageObject', $attrs); - - // export xml to writer object - $ilBench->start('ContentObjectExport', 'exportPageObject_XML'); - $page_object = new ilAssQuestionPage($question_id); - $page_object->buildDom(); - $page_object->insertInstIntoIDs($a_inst); - $mob_ids = $page_object->collectMediaObjects(false); - $file_ids = ilPCFileList::collectFileItems($page_object, $page_object->getDomDoc()); - $xml = $page_object->getXMLFromDom(false, false, false, '', true); - $xml = str_replace('&', '&', $xml); - $a_xml_writer->appendXML($xml); - $page_object->freeDom(); - unset($page_object); - $ilBench->stop('ContentObjectExport', 'exportPageObject_XML'); - - $ilBench->start("ContentObjectExport", "exportPageObject_CollectMedia"); - foreach ($mob_ids as $mob_id) { - $this->mob_ids[$mob_id] = $mob_id; - } - $ilBench->stop('ContentObjectExport', 'exportPageObject_CollectMedia'); - - // collect all file items - $ilBench->start('ContentObjectExport', 'exportPageObject_CollectFileItems'); - //$file_ids = $page_obj->getFileItemIds(); - foreach ($file_ids as $file_id) { - $this->file_ids[$file_id] = $file_id; - } - $ilBench->stop('ContentObjectExport', 'exportPageObject_CollectFileItems'); - - $a_xml_writer->xmlEndTag("PageObject"); - - $ilBench->stop('ContentObjectExport', 'exportPageObject'); - } - } - - public function exportXMLMediaObjects(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog): void - { - foreach ($this->mob_ids as $mob_id) { - $expLog->write(date('[y-m-d H:i:s] ') . 'Media Object ' . $mob_id); - if (ilObjMediaObject::_exists((int) $mob_id)) { - $target_dir = $a_target_dir . DIRECTORY_SEPARATOR . 'objects' - . DIRECTORY_SEPARATOR . 'il_' . IL_INST_ID . '_mob_' . $mob_id; - ilFileUtils::createDirectory($target_dir); - $media_obj = new ilObjMediaObject((int) $mob_id); - $media_obj->exportXML($a_xml_writer, (int) $a_inst); - foreach ($media_obj->getMediaItems() as $item) { - $stream = $item->getLocationStream(); - file_put_contents($target_dir . DIRECTORY_SEPARATOR . $item->getLocation(), $stream); - $stream->close(); - } - unset($media_obj); - } - } - } - - /** - * export files of file itmes - * - */ - public function exportFileItems($target_dir, &$expLog): void - { - foreach ($this->file_ids as $file_id) { - $expLog->write(date("[y-m-d H:i:s] ") . "File Item " . $file_id); - $file_dir = $target_dir . '/objects/il_' . IL_INST_ID . '_file_' . $file_id; - ilFileUtils::makeDir($file_dir); - $file_obj = new ilObjFile((int) $file_id, false); - $source_file = $file_obj->getFile($file_obj->getVersion()); - if (!is_file($source_file)) { - $source_file = $file_obj->getFile(); - } - if (is_file($source_file)) { - copy($source_file, $file_dir . '/' . $file_obj->getFileName()); - } - unset($file_obj); - } - } - - /** - * creates data directory for export files - * (data_dir/qpl_data/qpl_/export, depending on data - * directory that is set in ILIAS setup/ini) - */ - public function createExportDirectory(): void - { - $qpl_data_dir = ilFileUtils::getDataDir() . '/qpl_data'; - ilFileUtils::makeDir($qpl_data_dir); - if (!is_writable($qpl_data_dir)) { - $this->error->raiseError( - 'Questionpool Data Directory (' . $qpl_data_dir - . ') not writeable.', - $this->error->FATAL - ); - } - - // create learning module directory (data_dir/lm_data/lm_) - $qpl_dir = $qpl_data_dir . '/qpl_' . $this->getId(); - ilFileUtils::makeDir($qpl_dir); - if (!@is_dir($qpl_dir)) { - $this->error->raiseError('Creation of Questionpool Directory failed.', $this->error->FATAL); - } - // create Export subdirectory (data_dir/lm_data/lm_/Export) - ilFileUtils::makeDir($this->getExportDirectory('xlsx')); - if (!@is_dir($this->getExportDirectory('xlsx'))) { - $this->error->raiseError('Creation of Export Directory failed.', $this->error->FATAL); - } - ilFileUtils::makeDir($this->getExportDirectory('zip')); - if (!@is_dir($this->getExportDirectory('zip'))) { - $this->error->raiseError('Creation of Export Directory failed.', $this->error->FATAL); - } - } - - /** - * get export directory of questionpool - */ - public function getExportDirectory($type = ''): string - { - switch ($type) { - case 'xml': - $export_dir = ilExport::_getExportDirectory($this->getId(), $type, $this->getType()); - break; - case 'xlsx': - case 'zip': - $export_dir = ilFileUtils::getDataDir() . "/qpl_data/qpl_{$this->getId()}/export_{$type}"; - break; - default: - $export_dir = ilFileUtils::getDataDir() . '/qpl_data' . '/qpl_' . $this->getId() . '/export'; - break; - } - return $export_dir; - } - /** * Retrieve an array containing all question ids of the questionpool * @@ -599,15 +357,6 @@ public function checkQuestionParent(int $question_id): bool return (bool) $row['cnt']; } - /** - * get array of (two) new created questions for - * import id - */ - public function getImportMapping(): array - { - return []; - } - /** * Returns a QTI xml representation of a list of questions * @@ -1242,10 +991,4 @@ public static function isSkillManagementGloballyActivated(): ?bool return self::$isSkillManagementGloballyActivated; } - - public function fromXML(?string $xml_file): void - { - $parser = new ilObjQuestionPoolXMLParser($this, $xml_file); - $parser->startParsing(); - } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php index 965620232e13..ae9c98d7cf38 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php @@ -20,6 +20,15 @@ use ILIAS\Skill\Service\SkillUsageService; use ILIAS\TestQuestionPool\QuestionPoolDIC; +use ILIAS\TestQuestionPool\Import\TestQuestionsImportTrait; +use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; +use ILIAS\TestQuestionPool\ExportImport\Import\QuestionSelectionStage; +use ILIAS\TestQuestionPool\ExportImport\Import\PersistStage; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportStageRunner; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResult; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResultType; use ILIAS\TestQuestionPool\RequestDataCollector; use ILIAS\TestQuestionPool\Questions\Presentation\QuestionTable; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; @@ -32,9 +41,7 @@ use ILIAS\UI\URLBuilderToken; use ILIAS\Data\Factory as DataFactory; use ILIAS\GlobalScreen\Services as GlobalScreen; -use ILIAS\Filesystem\Stream\Streams; use ILIAS\Filesystem\Util\Archive\Archives; -use ILIAS\TestQuestionPool\Import\TestQuestionsImportTrait; use ILIAS\FileUpload\MimeType; use ILIAS\UI\Component\Modal\RoundTrip as RoundTripModal; use ILIAS\Style\Content\Service as ContentStyle; @@ -95,6 +102,7 @@ class ilObjQuestionPoolGUI extends ilObjectGUI implements ilCtrlBaseClassInterfa protected RequestDataCollector $request_data_collector; protected GeneralQuestionPropertiesRepository $questionrepository; protected GlobalTestSettings $global_test_settings; + protected ImportSessionRepository $import_session_repository; public function __construct() { @@ -120,7 +128,8 @@ public function __construct() $local_dic = QuestionPoolDIC::dic(); $this->request_data_collector = $local_dic['request_data_collector']; $this->questionrepository = $local_dic['question.general_properties.repository']; - $this->global_test_settings = $local_dic['global_test_settings']; + $this->global_test_settings = $local_dic['global_test_settings'];; + $this->import_session_repository = $local_dic['exportimport.session']; parent::__construct('', $this->request_data_collector->getRefId(), true, false); @@ -733,62 +742,6 @@ public function download_paragraphObject(): void exit; } - public function importVerifiedFileObject(): void - { - if ($this->creation_mode - && !$this->checkPermissionBool('create', '', $this->request_data_collector->string('new_type')) - || !$this->creation_mode - && !$this->checkPermissionBool('read', '', $this->object->getType())) { - $this->redirectAfterMissingWrite(); - return; - } - - $file_to_import = ilSession::get('path_to_import_file'); - list($subdir, $importdir, $xmlfile, $qtifile) = $this->buildImportDirectoriesFromImportFile($file_to_import); - - $new_obj = new ilObjQuestionPool(0, true); - $new_obj->setType($this->request_data_collector->raw('new_type')); - $new_obj->setTitle('dummy'); - $new_obj->setDescription('questionpool import'); - $new_obj->create(true); - $new_obj->createReference(); - $new_obj->putInTree($this->request_data_collector->getRefId()); - $new_obj->setPermissions($this->request_data_collector->getRefId()); - - $selected_questions = $this->retrieveSelectedQuestionsFromImportQuestionsSelectionForm( - 'importVerifiedFile', - $importdir, - $qtifile, - $this->request - ); - - if (is_file($importdir . DIRECTORY_SEPARATOR . 'manifest.xml')) { - $this->importQuestionPoolWithValidManifest( - $new_obj, - $selected_questions, - $file_to_import - ); - } else { - $this->importQuestionsFromQtiFile( - $new_obj, - $selected_questions, - $qtifile, - $importdir, - $xmlfile - ); - - $new_obj->fromXML($xmlfile); - - $new_obj->update(); - $new_obj->saveToDb(); - } - $this->cleanupAfterImport($importdir); - - $this->tpl->setOnScreenMessage('success', $this->lng->txt('object_imported'), true); - $this->ctrl->setParameterByClass(self::class, 'ref_id', $new_obj->getRefId()); - $this->ctrl->redirectByClass(self::class); - } - public function importVerifiedQuestionsFileObject(): void { $file_to_import = ilSession::get('path_to_import_file'); @@ -879,18 +832,6 @@ function ($vs): bool { )->withSubmitLabel($this->lng->txt('import')); } - private function importQuestionPoolWithValidManifest( - ilObjQuestionPool $obj, - array $selected_questions, - string $file_to_import - ): void { - ilSession::set('qpl_import_selected_questions', $selected_questions); - $imp = new ilImport($this->request_data_collector->getRefId()); - $map = $imp->getMapping(); - $map->addMapping('components/ILIAS/TestQuestionPool', 'qpl', 'new_id', (string) $obj->getId()); - $imp->importObject($obj, $file_to_import, basename($file_to_import), 'qpl', 'components/ILIAS/TestQuestionPool', true); - } - private function importQuestionsFromQtiFile( ilObjQuestionPool $obj, array $selected_questions, @@ -1369,49 +1310,77 @@ protected function importQuestionsFile(string $path_to_uploaded_file_in_temp_dir exit; } + + protected function importFile(string $file_to_import, string $path_to_uploaded_file_in_temp_dir): void { - list($subdir, $importdir, $xmlfile, $qtifile) = $this->buildImportDirectoriesFromImportFile($file_to_import); + $this->import_session_repository->clear(); - $options = (new ILIAS\Filesystem\Util\Archive\UnzipOptions()) - ->withZipOutputPath($this->getImportTempDirectory()); + $context = new ImportContext(['file_to_import' => $file_to_import]); + $this->import_session_repository->setContext($context); + $this->import_session_repository->setCurrentStageIndex(0); - $unzip = $this->archives->unzip(Streams::ofResource(fopen($file_to_import, 'r')), $options); - $unzip->extract(); + $this->ctrl->redirectByClass(self::class, 'processImport'); + } - if (!file_exists($qtifile)) { - ilFileUtils::delDir($importdir); - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('cannot_find_xml'), true); + public function processImportObject(): void + { + $permission = $this->creation_mode ? 'create' : 'read'; + if (!$this->checkPermissionBool($permission, '', $this->object->getType())) { + $this->redirectAfterMissingWrite(); return; } - ilSession::set('path_to_import_file', $file_to_import); - ilSession::set('path_to_uploaded_file_in_temp_dir', $path_to_uploaded_file_in_temp_dir); + $runner = $this->buildImportStageRunner(); + $result = $runner->run($this->request); - $this->ctrl->setParameterByClass(self::class, 'new_type', $this->type); - $form = $this->buildImportQuestionsSelectionForm( - 'importVerifiedFile', - $importdir, - $qtifile, - $path_to_uploaded_file_in_temp_dir - ); + match ($result->type) { + StageResultType::INTERACT => $this->renderImportStage($runner, $result), + StageResultType::ADVANCE => $this->ctrl->redirectByClass(self::class, 'processImport'), + StageResultType::ERROR => $this->renderImportError($runner, $result), + StageResultType::COMPLETE => $this->renderImportSuccess($runner, $result), + }; + } - if ($form === null) { - return; - } + private function buildImportStageRunner(): ImportStageRunner + { + $form_action = $this->ctrl->getFormActionByClass(self::class, 'processImport'); - $panel = $this->ui_factory->panel()->standard( - $this->lng->txt('import_qpl'), + return new ImportStageRunner( [ - $this->ui_factory->legacy()->content($this->lng->txt('qpl_import_verify_found_questions')), - $form - ] + new UploadValidationStage($this->archives, $this->lng, 'components/ILIAS/TestQuestionPool'), + new QuestionSelectionStage($this->lng, $this->component_factory, $this->ui_factory, $form_action), + new PersistStage($this->lng, $this->request_data_collector, $this->import_session_repository), + ], + $this->import_session_repository, + ); + } + + private function renderImportStage(ImportStageRunner $runner, StageResult $result): void + { + $workflow = $runner->buildWorkflow($this->ui_factory, $this->lng->txt('import')); + $this->tpl->setContent( + $this->ui_renderer->render([$workflow, ...$result->components]) ); - $this->tpl->setContent($this->ui_renderer->render($panel)); - $this->tpl->printToStdout(); - exit; } + private function renderImportError(ImportStageRunner $runner, StageResult $result): void + { + $runner->reset(); + $this->tpl->setOnScreenMessage('failure', $result->error_message, true); + $this->ctrl->redirectByClass(self::class, self::DEFAULT_CMD); + } + + private function renderImportSuccess(ImportStageRunner $runner, StageResult $result): void + { + $runner->reset(); + $this->tpl->setOnScreenMessage('success', $this->lng->txt('object_imported'), true); + $this->ctrl->setParameter($this, 'ref_id', $result->context->get('pool_obj_id')); + $this->ctrl->redirectByClass(self::class, self::DEFAULT_CMD); + } + + + public function addLocatorItems(): void { $ilLocator = $this->locator; diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolXMLParser.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolXMLParser.php deleted file mode 100755 index ee4ab378571e..000000000000 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolXMLParser.php +++ /dev/null @@ -1,138 +0,0 @@ - - * @version $Id$ - * - * @package components\ILIAS/Test - */ -class ilObjQuestionPoolXMLParser extends ilSaxParser -{ - private \ilObjQuestionPool $poolOBJ; - - private $inSettingsTag; - - private $inMetaDataTag; - private $inMdGeneralTag; - private bool $title_processed = false; - private bool $description_processed = false; - private string $cdata = ""; - - /** - * @param ilObjQuestionPool $poolOBJ - * @param $xmlFile - */ - public function __construct(ilObjQuestionPool $poolOBJ, ?string $xmlFile) - { - $this->poolOBJ = $poolOBJ; - - $this->inSettingsTag = false; - $this->inMetaDataTag = false; - $this->inMdGeneralTag = false; - - parent::__construct($xmlFile); - } - - public function setHandlers($a_xml_parser): void - { - xml_set_element_handler($a_xml_parser, $this->handlerBeginTag(...), $this->handlerEndTag(...)); - xml_set_character_data_handler($a_xml_parser, $this->handlerCharacterData(...)); - } - - public function handlerBeginTag($xmlParser, $tagName, $tagAttributes): void - { - switch ($tagName) { - case 'MetaData': - $this->inMetaDataTag = true; - break; - - case 'General': - if ($this->inMetaDataTag) { - $this->inMdGeneralTag = true; - } - break; - - case 'Title': - case 'Description': - $this->cdata = ''; - break; - - case 'Settings': - $this->inSettingsTag = true; - break; - - case 'NavTaxonomy': - case 'SkillService': - if ($this->inSettingsTag) { - $this->cdata = ''; - } - break; - } - } - - public function handlerEndTag($xmlParser, $tagName): void - { - switch ($tagName) { - case 'MetaData': - $this->inMetaDataTag = false; - break; - - case 'General': - if ($this->inMetaDataTag) { - $this->inMdGeneralTag = false; - } - break; - - case 'Title': - if (!$this->title_processed) { - $this->poolOBJ->setTitle($this->cdata); - $this->title_processed = true; - $this->cdata = ''; - } - break; - - case 'Description': - if (!$this->description_processed) { - $this->poolOBJ->setDescription($this->cdata); - $this->description_processed = true; - $this->cdata = ''; - } - break; - - case 'Settings': - $this->inSettingsTag = false; - break; - - case 'SkillService': - $this->poolOBJ->setSkillServiceEnabled((bool) $this->cdata); - $this->cdata = ''; - break; - } - } - - public function handlerCharacterData($xmlParser, $charData): void - { - if ($charData != "\n") { - // Replace multiple tabs with one space - $charData = preg_replace("/\t+/", " ", $charData); - - $this->cdata .= $charData; - } - } -} diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php index 78ae5f10e618..15420a6c6900 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php @@ -18,9 +18,11 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\Import\TestQuestionsImportTrait; +use ILIAS\Data\ReferenceId; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\SimpleXMLDeserializer; +use ILIAS\TestQuestionPool\ExportImport\QuestionPoolImporter; use ILIAS\TestQuestionPool\QuestionPoolDIC; -use ILIAS\TestQuestionPool\RequestDataCollector; /** * Importer class for question pools @@ -32,17 +34,16 @@ class ilTestQuestionPoolImporter extends ilXmlImporter { - use TestQuestionsImportTrait; - - private ilObjQuestionPool $pool_obj; - protected readonly RequestDataCollector $request_data_collector; + protected readonly ImportSessionRepository $session; + protected readonly QuestionPoolImporter $importer; public function __construct() { parent::__construct(); $local_dic = QuestionPoolDIC::dic(); - $this->request_data_collector = $local_dic['request_data_collector']; + $this->session = $local_dic['exportimport.session']; + $this->importer = $local_dic['exportimport.importer']; } public function importXmlRepresentation( @@ -51,58 +52,15 @@ public function importXmlRepresentation( string $a_xml, ilImportMapping $a_mapping ): void { - global $DIC; - // Container import => pool object already created - if (($new_id = $a_mapping->getMapping('components/ILIAS/Container', 'objs', $a_id)) !== null) { - $new_obj = ilObjectFactory::getInstanceByObjId((int) $new_id, false); - $new_obj->getObjectProperties()->storePropertyIsOnline($new_obj->getObjectProperties()->getPropertyIsOnline()->withOffline()); // sets Question pools to always online - - $selected_questions = []; - [$importdir, $xmlfile, $qtifile] = $this->buildImportDirectoriesFromContainerImport( - $this->getImportDirectory() - ); - } elseif (($new_id = $a_mapping->getMapping('components/ILIAS/TestQuestionPool', 'qpl', 'new_id')) !== null) { - $new_obj = ilObjectFactory::getInstanceByObjId((int) $new_id, false); - - $selected_questions = ilSession::get('qpl_import_selected_questions'); - [$subdir, $importdir, $xmlfile, $qtifile] = $this->buildImportDirectoriesFromImportFile( - ilSession::get('path_to_import_file') - ); - ilSession::clear('qpl_import_selected_questions'); - } else { - // Shouldn't happen - $DIC['ilLog']->info('non container and no tax mapping, perhaps old qpl export'); - return; - } - - if (!file_exists($xmlfile)) { - $DIC['ilLog']->info('Cannot find xml definition: ' . $xmlfile); - return; - } - if (!file_exists($qtifile)) { - $DIC['ilLog']->info('Cannot find qti definition: ' . $qtifile); - return; - } - - $this->pool_obj = $new_obj; - - $new_obj->fromXML($xmlfile); - - $qpl_new = $this->request_data_collector->string('qpl_new'); - - // set another question pool name (if possible) - if ($qpl_new !== '') { - $new_obj->setTitle($qpl_new); - } - - $new_obj->update(); - $new_obj->saveToDb(); - - // FIXME: Copied from ilObjQuestionPoolGUI::importVerifiedFileObject - // TODO: move all logic to ilObjQuestionPoolGUI::importVerifiedFile and call - // this method from ilObjQuestionPoolGUI and ilTestImporter + $result = $this->importer->import( + new SimpleXMLDeserializer()->open($a_xml), + $a_mapping, + new ReferenceId($a_mapping->getTargetId()), + $this->session->getContext(), + ); + $this->session->setContext($result); + return; - $DIC['ilLog']->info('xml file: ' . $xmlfile . ', qti file:' . $qtifile); $qtiParser = new ilQTIParser( $importdir, @@ -120,58 +78,20 @@ public function importXmlRepresentation( ); $questionPageParser->setQuestionMapping($qtiParser->getImportMapping()); $questionPageParser->startParsing(); - - foreach ($qtiParser->getImportMapping() as $k => $v) { - $old_question_id = substr($k, strpos($k, 'qst_') + strlen('qst_')); - $new_question_id = (string) $v['pool']; // yes, this is the new question id ^^ - - $a_mapping->addMapping( - 'components/ILIAS/Taxonomy', - 'tax_item', - "qpl:quest:{$old_question_id}", - $new_question_id - ); - - $a_mapping->addMapping( - 'components/ILIAS/Taxonomy', - 'tax_item_obj_id', - "qpl:quest:{$old_question_id}", - (string) $new_obj->getId() - ); - - $a_mapping->addMapping( - 'components/ILIAS/TestQuestionPool', - 'quest', - $old_question_id, - $new_question_id - ); - } - - $this->importQuestionSkillAssignments($xmlfile, $a_mapping, $new_obj->getId()); - - $a_mapping->addMapping('components/ILIAS/TestQuestionPool', 'qpl', $a_id, (string) $new_obj->getId()); - $a_mapping->addMapping( - 'components/ILIAS/MetaData', - 'md', - $a_id . ':0:qpl', - $new_obj->getId() . ':0:qpl' - ); - - - $new_obj->saveToDb(); } - /** - * Final processing - * @param ilImportMapping $a_mapping - * @return void - */ public function finalProcessing(ilImportMapping $a_mapping): void { - $maps = $a_mapping->getMappingsOfEntity('components/ILIAS/TestQuestionPool', 'qpl'); - foreach ($maps as $old => $new) { + $qpl_mappings = $a_mapping->getMappingsOfEntity('components/ILIAS/TestQuestionPool', 'qpl'); + + foreach ($qpl_mappings as $old => $new) { if ($old !== 'new_id' && (int) $old > 0) { - $new_tax_ids = $a_mapping->getMapping('components/ILIAS/Taxonomy', 'tax_usage_of_obj', (string) $old); + $new_tax_ids = $a_mapping->getMapping( + 'components/ILIAS/Taxonomy', + 'tax_usage_of_obj', + (string) $old + ); + if ($new_tax_ids !== null) { $tax_ids = explode(':', $new_tax_ids); foreach ($tax_ids as $tid) { @@ -181,26 +101,4 @@ public function finalProcessing(ilImportMapping $a_mapping): void } } } - - protected function importQuestionSkillAssignments($xmlFile, ilImportMapping $mappingRegistry, $targetParentObjId): void - { - $parser = new ilAssQuestionSkillAssignmentXmlParser($xmlFile); - $parser->startParsing(); - - $importer = new ilAssQuestionSkillAssignmentImporter(); - $importer->setTargetParentObjId($targetParentObjId); - $importer->setImportInstallationId($this->getInstallId()); - $importer->setImportMappingRegistry($mappingRegistry); - $importer->setImportMappingComponent('components/ILIAS/TestQuestionPool'); - $importer->setImportAssignmentList($parser->getAssignmentList()); - - $importer->import(); - - if ($importer->getFailedImportAssignmentList()->assignmentsExist()) { - $qsaImportFails = new ilAssQuestionSkillAssignmentImportFails($targetParentObjId); - $qsaImportFails->registerFailedImports($importer->getFailedImportAssignmentList()); - - $this->pool_obj->getObjectProperties()->storePropertyIsOnline($this->pool_obj->getObjectProperties()->getPropertyIsOnline()->withOffline()); - } - } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/QuestionImage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/QuestionImage.php new file mode 100644 index 000000000000..a766438425eb --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/QuestionImage.php @@ -0,0 +1,94 @@ +filename; + } + + public function getQuestionId(): ?int + { + return $this->question_id; + } + + public function getType(): int + { + return $this->type; + } + + public function getId(): string + { + return $this->id; + } + + public function setId(string $id): static + { + $this->id = $id; + return $this; + } + + /** + * @inheritDoc + */ + public function toArray(Transformations $tt): array + { + return [ + 'filename' => $this->filename, + 'question_id' => $this->question_id, + 'type' => $this->type, + 'id' => $this->id, + ]; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $value, Transformations $tt): static + { + return new self( + $value['filename'], + $tt->int($value['question_id']), + $tt->int($value['type']), + $value['id'] + ); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php new file mode 100644 index 000000000000..4a0a23f215ad --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php @@ -0,0 +1,75 @@ +lng->txt('qpl_import_step_persist'); + } + + public function getDescription(): string + { + return ''; + } + + public function process(ImportContext $context, ServerRequestInterface $request): StageResult + { + $importer = new ilImport($this->request_data_collector->getRefId()); + $importer->importObject( + null, + $context->get('file_to_import'), + basename($context->get('file_to_import')), + 'qpl', + 'components/ILIAS/TestQuestionPool', + true, + ); + + // Context is updated by the QuestionPoolImporter so we need to reload it + return StageResult::complete($this->session->getContext()); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php new file mode 100644 index 000000000000..e7cabc668d30 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php @@ -0,0 +1,181 @@ + \ilQTIItem::QT_ORDERING, + 'KPRIM CHOICE QUESTION' => \ilQTIItem::QT_KPRIM_CHOICE, + 'LONG MENU QUESTION' => \ilQTIItem::QT_LONG_MENU, + 'SINGLE CHOICE QUESTION' => \ilQTIItem::QT_MULTIPLE_CHOICE_SR, + 'MULTIPLE CHOICE QUESTION' => \ilQTIItem::QT_MULTIPLE_CHOICE_MR, + 'MATCHING QUESTION' => \ilQTIItem::QT_MATCHING, + 'CLOZE QUESTION' => \ilQTIItem::QT_CLOZE, + 'IMAGE MAP QUESTION' => \ilQTIItem::QT_IMAGEMAP, + 'TEXT QUESTION' => \ilQTIItem::QT_TEXT, + 'NUMERIC QUESTION' => \ilQTIItem::QT_NUMERIC, + 'TEXTSUBSET QUESTION' => \ilQTIItem::QT_TEXTSUBSET + ]; + + public function __construct( + private readonly Language $lng, + private readonly ilComponentFactory $component_factory, + private readonly UIFactory $ui_factory, + private readonly string $form_action, + ) { + } + + public function getIdentifier(): string + { + return 'question_selection'; + } + + public function getLabel(): string + { + return $this->lng->txt('qpl_import_step_select'); + } + + public function getDescription(): string + { + return ''; + } + + public function process(ImportContext $context, ServerRequestInterface $request): StageResult + { + if ($context->has('selectable_questions')) { + $options = []; + foreach ($context->get('selectable_questions') as $question) { + $options[$question] = $question; + } + + $data = $this->buildSelectQuestionsForm($options) + ->withRequest($request) + ->getData(); + + if (isset($data['selected_questions'])) { + return StageResult::advance($context->with('selected_questions', $data['selected_questions'])); + } + } + + if (!$context->has('import_file')) { + return StageResult::error($context, $this->lng->txt('qpl_import_file_not_found')); + } + + $options = []; + $deserializer = new SimpleXMLDeserializer()->open(file_get_contents($context->get('import_file'))); + $deserializer->addHandler('questions', function (array $questions) use (&$options): void { + foreach ($questions as $question) { + if (!isset($question['title']) || !isset($question['type'])) { + continue; + } + + $raw_id = $question['id']; + $id = is_array($raw_id) ? (string) ($raw_id['id'] ?? '') : (string) $raw_id; + $options[$id] = "{$question['title']} ({$this->getLabelForQuestionType($question['type'])})"; + } + }); + $deserializer->process(); + + if ($options === []) { + return StageResult::error($context, $this->lng->txt('qpl_import_no_items')); + } + + $panel = $this->ui_factory->panel()->standard( + $this->lng->txt('import_qpl'), + [ + $this->ui_factory->legacy()->content($this->lng->txt('qpl_import_verify_found_questions')), + $this->buildSelectQuestionsForm($options) + ] + ); + + return StageResult::interact( + $context->with('selectable_questions', array_keys($options)), + [$panel] + ); + } + + /** + * @return list + */ + public static function getSelectedQuestions(ImportContext $context): array + { + return array_map('intval', $context->get('selected_questions', [])); + } + + private function buildSelectQuestionsForm(array $options): Form + { + $input = $this->ui_factory->input()->field()->multiSelect( + $this->lng->txt('questions'), + $options + )->withValue(array_keys($options)); + + $form = $this->ui_factory->input()->container()->form()->standard( + $this->form_action, + ['selected_questions' => $input] + )->withSubmitLabel($this->lng->txt('import')); + + return $form; + } + + private function getLabelForQuestionType(string $type): string + { + if ($this->lng->exists($type)) { + return $this->lng->txt($type); + } + + /** + * @todo Remove with ILIAS 12: This is here for backward compatibility. + * As we support the import of a previous version this should go with + * ILIAS 11, but being generous: ILIAS 12 it is. + */ + if (array_key_exists($type, $this->old_export_question_types)) { + return $this->lng->txt($this->old_export_question_types[$type]); + } + return $this->getLabelForPluginQuestionTypes($type); + } + + private function getLabelForPluginQuestionTypes(string $type): string + { + foreach ($this->component_factory->getActivePluginsInSlot('qst') as $pl) { + if ($pl->getQuestionType() === $type) { + return $pl->getQuestionTypeTranslation(); + } + } + return $type; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php new file mode 100644 index 000000000000..0cb137a55fc6 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php @@ -0,0 +1,101 @@ +lng->txt('upload'); + } + + public function getDescription(): string + { + return ''; + } + + public function process(ImportContext $context, ServerRequestInterface $request): StageResult + { + $file_to_import = $context->get('file_to_import'); + if ( + $file_to_import === null + || !is_file($file_to_import) + || !str_ends_with(strtolower($file_to_import), '.zip') + ) { + return StageResult::error($context, $this->lng->txt('obj_import_file_error')); + } + + $subdir = basename($file_to_import, '.zip'); + $import_base_dir = self::IMPORT_TEMP_DIR . DIRECTORY_SEPARATOR . $subdir; + + $options = (new UnzipOptions())->withZipOutputPath(self::IMPORT_TEMP_DIR); + $unzip = $this->archives->unzip(Streams::ofResource(fopen($file_to_import, 'r')), $options); + $unzip->extract(); + + $manifest = new ilManifestParser($import_base_dir . DIRECTORY_SEPARATOR . 'manifest.xml'); + $export_file = array_find( + $manifest->getExportFiles(), + fn($file): bool => $file['component'] === $this->component + ); + + if ($export_file === null) { + return StageResult::error($context, $this->lng->txt('obj_import_file_error')); + } + + return StageResult::advance( + $context->with('import_file', $import_base_dir . DIRECTORY_SEPARATOR . $export_file['path']) + ->with('install_id', $manifest->getInstallId()) + ); + } + + public static function getInstallId(ImportContext $context): int + { + return intval($context->get('install_id')); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php new file mode 100644 index 000000000000..5b3d36b3d4d9 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php @@ -0,0 +1,191 @@ +builder->withAdditionalPipes(append: [$id_mapping_pipe])->create(); + + $selected_questions = QuestionSelectionStage::getSelectedQuestions($context); + + $deserializer->addHandler( + 'general', + function (array $objects) use ($tt, $mapping, $parent_id, &$context): void { + $new_pool_id = $this->importQuestionPool( + array_pop($objects), + $tt, + $mapping, + $parent_id + ); + $context = $context->with('pool_obj_id', $new_pool_id); + } + ); + + $deserializer->addHandler( + 'questions', + function (array $questions) use ($tt, $mapping, $selected_questions): void { + foreach ($questions as $question) { + $this->importQuestion( + $question, + $tt, + $mapping, + $selected_questions + ); + } + } + ); + + $deserializer->addHandler( + 'skill_assignments', + function (array $assignments) use ($tt, &$context): void { + $result = $this->skill_importer->import( + $assignments, + UploadValidationStage::getInstallId($context), + $tt, + ); + $context = $context->with('skill_assignments', $result); + } + ); + + $deserializer->process(); + + return $context; + } + + protected function importQuestionPool( + array $normalized, + Transformations $transformations, + ilImportMapping $mapping, + ReferenceId $parent_id + ): int { + $pool_object = $transformations->denormalize($normalized, ilObjQuestionPool::class); + $old_pool_id = $pool_object->getId(); + + $pool_object->setTitle('Imported'); //TODO: Remove after testing + $new_pool_id = $pool_object->create(true); + $pool_object->getObjectProperties()->storePropertyIsOnline( + $pool_object->getObjectProperties()->getPropertyIsOnline()->withOffline() + ); + $pool_object->saveToDb(); + + $pool_object->createReference(); + $pool_object->putInTree($parent_id->toInt()); + $pool_object->setPermissions($parent_id->toInt()); + + $mapping->addMapping(self::COMPONENT, 'qpl', (string) $old_pool_id, (string) $new_pool_id); + $mapping->addMapping(self::COMPONENT, 'object', (string) $old_pool_id, (string) $new_pool_id); + $mapping->addMapping('components/ILIAS/MetaData', 'md', "{$old_pool_id}:0:qpl", "{$new_pool_id}:0:qpl"); + + return $new_pool_id; + } + + protected function importQuestion( + array $normalized, + Transformations $transformations, + ilImportMapping $mapping, + array $selected_questions + ): void { + $question_class = $normalized['type']; + if (!class_exists($question_class)) { + throw new \InvalidArgumentException("Question class {$question_class} does not exist"); + } + + /** @var assQuestion $question */ + $question = $transformations->denormalize($normalized, new $question_class()); + $old_question_id = $question->getId(); + if (!in_array($old_question_id, $selected_questions)) { + return; + } + + $feedback_class = $question::getFeedbackClassNameByQuestionType($question->getQuestionType()); + $question->feedbackOBJ = new $feedback_class($question, $this->ctrl, $this->database, $this->language); + + $new_question_id = $question->createNewQuestion(false); + $question->saveToDb(); + + $mapping->addMapping(self::COMPONENT, 'question', (string) $old_question_id, (string) $new_question_id); + $mapping->addMapping('components/ILIAS/Taxonomy', 'tax_item', "qpl:quest:{$old_question_id}", (string) $new_question_id); + $mapping->addMapping('components/ILIAS/Taxonomy', 'tax_item_obj_id', "qpl:quest:{$old_question_id}", (string) $question->getObjId()); + $mapping->addMapping('components/ILIAS/COPage', 'pg', "qpl:{$old_question_id}", "qpl:{$new_question_id}"); + + $feedback = $transformations->denormalize($normalized['feedback'], Feedback::class); + $this->importFeedback($feedback, $question); + } + + protected function importFeedback(Feedback $feedback, assQuestion $question): void + { + $question_id = $question->getId(); + $question->feedbackOBJ->importGenericFeedback($question_id, false, $feedback->getGenericUncompleted()); + $question->feedbackOBJ->importGenericFeedback($question_id, true, $feedback->getGenericCompleted()); + + foreach ($feedback->getSpecificFeedback() as $specific_feedback) { + $question->feedbackOBJ->importSpecificAnswerFeedback( + $question_id, + (int) $specific_feedback['question_index'], + (int) $specific_feedback['answer_index'], + $specific_feedback['feedback'] + ); + } + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/SkillAssignmentsImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/SkillAssignmentsImporter.php new file mode 100644 index 000000000000..a22364815c72 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/SkillAssignmentsImporter.php @@ -0,0 +1,131 @@ +> $normalized_assignments + * @return array{failed: list, success: list} + */ + public function import( + array $normalized_assignments, + int $import_install_id, + Transformations $transformations, + ): array { + $result = ['failed' => [], 'success' => []]; + + foreach ($normalized_assignments as $item) { + $assignment = $transformations->denormalize($item, ilAssQuestionSkillAssignment::class); + + $skill_data = $this->getSkillIdMapping( + $assignment->getSkillBaseId(), + $assignment->getSkillTrefId(), + $import_install_id + ); + if ($skill_data === null) { + $result['failed'][] = $this->buildResultData($assignment); + continue; + } + + // Map imported skill ids to local skill ids. Question id and object id are already replaced in the id + // mapping pipe. + $assignment->setSkillBaseId($skill_data['skill_id']); + $assignment->setSkillTrefId($skill_data['tref_id']); + + $assignment->initSolutionComparisonExpressionList(); + foreach ($assignment->getSolutionComparisonExpressionList()->get() as $expression) { + $expression->setSkillBaseId($assignment->getSkillBaseId()); + $expression->setSkillTrefId($assignment->getSkillTrefId()); + } + + $assignment->saveToDb(); + $assignment->saveComparisonExpressions(); + + $this->skill_usage_service->addUsage( + $assignment->getParentObjId(), + $assignment->getSkillBaseId(), + $assignment->getSkillTrefId() + ); + + $result['success'][] = $this->buildResultData($assignment); + } + + return $result; + } + + protected function getSkillIdMapping(int $skill_base_id, int $skill_tref_id, int $import_install_id): ?array + { + if ($import_install_id === $this->local_install_id) { + return [ + 'skill_id' => $skill_base_id, + 'tref_id' => $skill_tref_id, + ]; + } + + $found_skill_data = $this->skill_repo->getCommonSkillIdForImportId( + $import_install_id, + $skill_base_id, + $skill_tref_id + ); + + $skill_data = current($found_skill_data); + if (!is_array($skill_data) || !isset($skill_data['skill_id']) || !isset($skill_data['tref_id'])) { + return null; + } + + return $skill_data; + } + + /** + * @return ImportResultData + */ + protected function buildResultData(ilAssQuestionSkillAssignment $assignment): array + { + return [ + 'skill_id' => $assignment->getSkillBaseId(), + 'tref_id' => $assignment->getSkillTrefId(), + 'title' => $assignment->getSkillTitle(), + 'path' => $assignment->getSkillPath(), + ]; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php index 1f32c1fbb89f..425580ba50fa 100755 --- a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php +++ b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php @@ -21,7 +21,10 @@ namespace ILIAS\TestQuestionPool; use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\QuestionPoolExporter; +use ILIAS\TestQuestionPool\ExportImport\QuestionPoolImporter; +use ILIAS\TestQuestionPool\ExportImport\SkillAssignmentsImporter; use Pimple\Container as PimpleContainer; use ILIAS\DI\Container as ILIASContainer; use ILIAS\TestQuestionPool\Questions\SuggestedSolution\SuggestedSolutionsDatabaseRepository; @@ -82,6 +85,23 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC->taxonomy()->domain() ); + $dic['exportimport.session'] = static fn($c): ImportSessionRepository => + new ImportSessionRepository('qpl'); + $dic['exportimport.skill_assignments_importer'] = static fn($c): SkillAssignmentsImporter => + new SkillAssignmentsImporter( + $DIC->skills()->internal()->repo()->getTreeRepo(), + $DIC->skills()->usage(), + (int) $DIC->settings()->get('inst_id', '0') + ); + $dic['exportimport.importer'] = static fn($c): QuestionPoolImporter => + new QuestionPoolImporter( + $c['exportimport.builder'], + $DIC->ctrl(), + $DIC->database(), + $DIC->language(), + $c['exportimport.skill_assignments_importer'] + ); + return $dic; } } diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index c22c2c7cd160..3a95e2343cab 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -1067,6 +1067,8 @@ assessment#:#qpl_imagemap_preview_missing#:#ILIAS konnte keine Vorschaudatei mit assessment#:#qpl_import_create_new_qpl#:#In einen neuen Fragenpool importieren assessment#:#qpl_import_no_items#:#Fehler: die Importdatei enthält keine Fragen! assessment#:#qpl_import_non_ilias_files#:#Fehler: Die Importdatei enthält QTI-Dateien, die nicht von einem ILIAS-System erstellt wurden. Bitte kontaktieren Sie das ILIAS-Team, um einen Importfilter für Ihr QTI-Dateiformat zu bekommen. +assessment#:#qpl_import_step_persist#:#Fragenpool in ILIAS importieren +assessment#:#qpl_import_step_select#:#Fragen auswählen assessment#:#qpl_import_verify_found_questions#:#ILIAS hat die folgenden Fragen in der Importdatei gefunden. Bitte wählen Sie die Fragen aus, die Sie importieren wollen. assessment#:#qpl_lac_desc_brackets#:#Klammerung assessment#:#qpl_lac_desc_compare_answer_exist#:#Vergleiche ob für eine Frage/Lücke einer Frage keine Antwort gegeben wurde diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index d73220fcc889..4af32130fd4b 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -1067,6 +1067,8 @@ assessment#:#qpl_imagemap_preview_missing#:#ILIAS could not create the temporary assessment#:#qpl_import_create_new_qpl#:#Import the questions in a new question pool assessment#:#qpl_import_no_items#:#Error: The import file contains no questions! assessment#:#qpl_import_non_ilias_files#:#Error: The import file contains QTI files which are not created by an ILIAS system. Please contact the ILIAS team to get in import filter for your QTI file format. +assessment#:#qpl_import_step_persist#:#Import question pool into ILIAS +assessment#:#qpl_import_step_select#:#Select questions to import assessment#:#qpl_import_verify_found_questions#:#ILIAS found the following questions in the import file. Please select the questions you want to import. assessment#:#qpl_lac_desc_brackets#:#Brackets assessment#:#qpl_lac_desc_compare_answer_exist#:#Compare if there is an Answer for a Question/Gap From 8558928a25e64a8b4eedf6e970967fde1d8119bc Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Thu, 9 Apr 2026 15:21:41 +0200 Subject: [PATCH 05/66] fix(qpl): support copage import --- .../class.ilTestQuestionPoolImporter.php | 64 ++++++++++++++----- .../src/ExportImport/QuestionPoolImporter.php | 1 + 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php index 15420a6c6900..27cae121f9b1 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php @@ -59,28 +59,58 @@ public function importXmlRepresentation( $this->session->getContext(), ); $this->session->setContext($result); - return; + } + public function finalProcessing(ilImportMapping $a_mapping): void + { + $this->finalizeQuestionPages($a_mapping); + $this->finalizeTaxonomyUsage($a_mapping); + } - $qtiParser = new ilQTIParser( - $importdir, - $qtifile, - ilQTIParser::IL_MO_PARSE_QTI, - $new_obj->getId(), - $selected_questions - ); - $qtiParser->startParsing(); + /** + * Finalize the imported question pages by replacing the old question ids with the new question ids. + */ + private function finalizeQuestionPages(ilImportMapping $a_mapping): void + { + $page_mappings = $a_mapping->getMappingsOfEntity('components/ILIAS/COPage', 'pg'); - $questionPageParser = new ilQuestionPageParser( - $new_obj, - $xmlfile, - $importdir - ); - $questionPageParser->setQuestionMapping($qtiParser->getImportMapping()); - $questionPageParser->startParsing(); + foreach ($page_mappings as $old => $new) { + if (!preg_match('/^qpl:(\d+)$/', $old, $old_matches)) { + continue; + } + $old_question_id = $old_matches[1]; + + if (!preg_match('/^qpl:(\d+)$/', $new, $new_matches)) { + continue; + } + $new_question_id = $new_matches[1]; + + $page = new ilAssQuestionPage((int) $new_question_id); + $xml = preg_replace( + '/il_\d+_qst_' . preg_quote($old_question_id, '/') . '\b/', + "il__qst_{$new_question_id}", + $page->getXMLContent() + ); + if ($xml === null) { + continue; + } + $page->setXMLContent($xml); + + $parent_obj_id = $a_mapping->getMapping( + 'components/ILIAS/TestQuestionPool', + 'question_assignment', + $new_question_id + ); + if ($parent_obj_id !== null) { + $page->setParentId((int) $parent_obj_id); + } + + $page->updateFromXML(); + unset($page); + } } - public function finalProcessing(ilImportMapping $a_mapping): void + private function finalizeTaxonomyUsage(ilImportMapping $a_mapping): void { $qpl_mappings = $a_mapping->getMappingsOfEntity('components/ILIAS/TestQuestionPool', 'qpl'); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php index 5b3d36b3d4d9..e96b6f314ac2 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php @@ -165,6 +165,7 @@ protected function importQuestion( $question->saveToDb(); $mapping->addMapping(self::COMPONENT, 'question', (string) $old_question_id, (string) $new_question_id); + $mapping->addMapping(self::COMPONENT, 'question_assignment', (string) $new_question_id, (string) $question->getObjId()); $mapping->addMapping('components/ILIAS/Taxonomy', 'tax_item', "qpl:quest:{$old_question_id}", (string) $new_question_id); $mapping->addMapping('components/ILIAS/Taxonomy', 'tax_item_obj_id', "qpl:quest:{$old_question_id}", (string) $question->getObjId()); $mapping->addMapping('components/ILIAS/COPage', 'pg', "qpl:{$old_question_id}", "qpl:{$new_question_id}"); From 5477595717e0b5b4ac128a05d0477bff82dabed8 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Fri, 10 Apr 2026 09:58:32 +0200 Subject: [PATCH 06/66] fix(qpl): formula unit and categories import --- .../classes/class.assFormulaQuestionUnit.php | 4 +- .../ExportImport/QuestionPoolCollector.php | 34 +++++++--- .../src/ExportImport/QuestionPoolExporter.php | 40 +++--------- .../src/ExportImport/QuestionPoolImporter.php | 62 ++++++++++++++++++- 4 files changed, 96 insertions(+), 44 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php index aecd91246e3f..085204feb2fe 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php @@ -176,7 +176,7 @@ public function toNormalized(Transformations $tt): Transformation 'factor' => $this->factor, 'category_id' => $tt->normalize(new Id($this->category, 'unit_category')), 'sequence' => $this->sequence, - 'baseunit' => $this->baseunit, + 'baseunit' => $tt->normalize(new Id($this->baseunit, 'unit')), 'baseunit_title' => $this->baseunit_title, ]); } @@ -193,7 +193,7 @@ public function fromNormalized(Transformations $tt): Transformation $clone->factor = $tt->float($normalized['factor']); $clone->category = $tt->denormalize($normalized['category_id'], Id::class)->getId(); $clone->sequence = $tt->int($normalized['sequence']); - $clone->baseunit = $tt->int($normalized['baseunit']); + $clone->baseunit = $tt->denormalize($normalized['baseunit'], Id::class)->getId(); $clone->baseunit_title = $tt->string($normalized['baseunit_title']); return $clone; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolCollector.php b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolCollector.php index ecba542b2c15..860cfb630b0a 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolCollector.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolCollector.php @@ -20,6 +20,8 @@ namespace ILIAS\TestQuestionPool\ExportImport; +use assFormulaQuestionUnit; +use assFormulaQuestionUnitCategory; use assQuestion; use Generator; use ilAssQuestionSkillAssignmentList; @@ -106,18 +108,34 @@ public function getQuestionObjects(): Generator */ /** - * Collect the unit categories and their units for all formula questions in the question pool. - * - * @return Generator<\assFormulaQuestionUnit|\assFormulaQuestionUnitCategory> + * Get all unit categories and units for a formula question. + * + * @return array{categories: list, base_units: list, units: list} */ - public function getUnits(): Generator + public function getUnitsAndCategories(int $question_id): array { - foreach ($this->getQuestionProperties() as $question) { - if ($question->getClassName() === 'assFormulaQuestion') { - $repository = new ilUnitConfigurationRepository($question->getQuestionId()); - yield from $repository->getCategorizedUnits(); + $repository = new ilUnitConfigurationRepository($question_id); + $data = [ + 'categories' => [], + 'base_units' => [], + 'units' => [], + ]; + + foreach ($repository->getCategorizedUnits() as $item) { + if($item instanceof assFormulaQuestionUnitCategory) { + $data['categories'][] = $item; + } + + if($item instanceof assFormulaQuestionUnit) { + if($item->getBaseUnit() === 0 || $item->getBaseUnit() === $item->getId()) { + $data['base_units'][] = $item; + } else { + $data['units'][] = $item; + } } } + + return $data; } /* diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php index 441d942f6b05..71d6821a2942 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php @@ -20,8 +20,7 @@ namespace ILIAS\TestQuestionPool\ExportImport; -use assFormulaQuestionUnit; -use assFormulaQuestionUnitCategory; +use assFormulaQuestion; use ilDBInterface; use ILIAS\Data\ObjectId; use ILIAS\Data\UUID\Factory as UUIDFactory; @@ -31,11 +30,9 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Serializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\ExportContext; -use ILIAS\Questions\Units\Repository as UnitsRepository; use ILIAS\Taxonomy\DomainService as Taxonomy; use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; -use SebastianBergmann\CodeCoverage\Report\Xml\Unit; /** * Orchestrates the export of a question pool. It uses the Builder to create a pipeline of transformations that are used @@ -104,10 +101,6 @@ public function process(ExportContext $context, Serializer $serializer): ExportC 'general', fn() => $this->exportObject($collector, $tt, $serializer, $context) ); - $serializer->group( - 'units', - fn() => $this->exportUnits($collector, $tt, $serializer) - ); $serializer->group( 'questions', fn() => $this->exportQuestions($collector, $tt, $serializer, $context) @@ -154,28 +147,6 @@ protected function exportObject( ); } - protected function exportUnits( - QuestionPoolCollector $collector, - Transformations $transformations, - Serializer $serializer - ): void { - $categories = []; - - foreach ($collector->getUnits() as $item) { - if($item instanceof assFormulaQuestionUnitCategory) { - $categories[$item->getId()] = $transformations->normalize($item); - } - - if($item instanceof assFormulaQuestionUnit) { - $categories[$item->getCategory()]['units'][] = $transformations->normalize($item); - } - } - - foreach($categories as $category) { - $serializer->append('category', $category); - } - } - protected function exportQuestions( QuestionPoolCollector $collector, Transformations $transformations, @@ -183,13 +154,18 @@ protected function exportQuestions( ExportContext $export ): void { foreach ($collector->getQuestionObjects() as $question) { - $serializer->append('question', [ + $normalized = [ ... $transformations->normalize($question), 'feedback' => $transformations->normalize( $collector->getFeedback($question) ) - ]); + ]; + + if ($question instanceof assFormulaQuestion) { + $normalized['formula_data'] = $transformations->normalize($collector->getUnitsAndCategories($question->getId())); + } + $serializer->append('question', $normalized); $export->addDependency('components/ILIAS/COPage', 'pg', ["qpl:{$question->getId()}"]); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php index e96b6f314ac2..24fba228ccbf 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php @@ -20,6 +20,9 @@ namespace ILIAS\TestQuestionPool\ExportImport; +use assFormulaQuestion; +use assFormulaQuestionUnit; +use assFormulaQuestionUnitCategory; use assQuestion; use ilCtrl; use ilDBInterface; @@ -29,12 +32,14 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Deserializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\IdMappingPipe; use ILIAS\TestQuestionPool\ExportImport\Envelopes\Feedback; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionSelectionStage; use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; use ilImportMapping; use ilObjQuestionPool; +use ilUnitConfigurationRepository; /** * Orchestrates the import of a question pool. It uses the Builder to create a pipeline of transformations that are used @@ -158,18 +163,25 @@ protected function importQuestion( return; } + // Initialize feedback object to prevent error when saving the question $feedback_class = $question::getFeedbackClassNameByQuestionType($question->getQuestionType()); $question->feedbackOBJ = new $feedback_class($question, $this->ctrl, $this->database, $this->language); + // Create new question and store basic question properties $new_question_id = $question->createNewQuestion(false); - $question->saveToDb(); - $mapping->addMapping(self::COMPONENT, 'question', (string) $old_question_id, (string) $new_question_id); $mapping->addMapping(self::COMPONENT, 'question_assignment', (string) $new_question_id, (string) $question->getObjId()); $mapping->addMapping('components/ILIAS/Taxonomy', 'tax_item', "qpl:quest:{$old_question_id}", (string) $new_question_id); $mapping->addMapping('components/ILIAS/Taxonomy', 'tax_item_obj_id', "qpl:quest:{$old_question_id}", (string) $question->getObjId()); $mapping->addMapping('components/ILIAS/COPage', 'pg', "qpl:{$old_question_id}", "qpl:{$new_question_id}"); + if ($question instanceof assFormulaQuestion) { + $this->importFormulaQuestion($normalized, $question, $transformations, $mapping, ); + } + + // Save question-specific properties + $question->saveToDb(); + $feedback = $transformations->denormalize($normalized['feedback'], Feedback::class); $this->importFeedback($feedback, $question); } @@ -189,4 +201,50 @@ protected function importFeedback(Feedback $feedback, assQuestion $question): vo ); } } + + protected function importFormulaQuestion( + array $normalized, + assFormulaQuestion $question, + Transformations $transformations, + ilImportMapping $mapping, + ): void { + $formula = $normalized['formula_data']; + $repository = new ilUnitConfigurationRepository($question->getId()); + + // First, import the unit categories which are referenced by the units + foreach ($formula['categories'] as $normalized_category) { + $category = $transformations->denormalize($normalized_category, new assFormulaQuestionUnitCategory()); + $old_category_id = $category->getId(); + + $repository->saveNewUnitCategory($category); + $mapping->addMapping(self::COMPONENT, 'unit_category', (string) $old_category_id, (string) $category->getId()); + } + + // Ensure base units are imported first so they can be referenced by the units. The mapping pipe will ensure + // that the category id, question id and base unit id are mapped to the new ids. + $normalized_units = array_merge($formula['base_units'], $formula['units']); + foreach ($normalized_units as $normalized_unit) { + $old_unit_id = $transformations->denormalize($normalized_unit['id'], Id::class)->getId(); + + $unit = new assFormulaQuestionUnit(); + $repository->createNewUnit($unit); + $mapping->addMapping(self::COMPONENT, 'unit', (string) $old_unit_id, (string) $unit->getId()); + + $unit = $transformations->denormalize($normalized_unit, $unit); + $repository->saveUnit($unit); + } + + // The question object is denormalized again to ensure the new unit ids are set in the variables and results. + $new_question = $transformations->denormalize($normalized, $question); + + $question->clearVariables(); + foreach ($new_question->getVariables() as $variable) { + $question->addVariable($variable); + } + + $question->clearResults(); + foreach ($new_question->getResults() as $result) { + $question->addResult($result); + } + } } From 07f6230968a61b6c0754c60da47683d57659446a Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Mon, 13 Apr 2026 11:59:04 +0200 Subject: [PATCH 07/66] feat(qpl): introduce question image file import --- .../Pipes/CollectQuestionImages.php | 46 +++++++++++++++---- .../src/ExportImport/QuestionPoolImporter.php | 37 ++++++++++++++- .../src/Questions/Files/QuestionFiles.php | 5 ++ 3 files changed, 78 insertions(+), 10 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php index 5b82ef56272d..b32beb3e1d88 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php @@ -23,6 +23,8 @@ use ILIAS\Data\ObjectId; use ILIAS\Data\UUID\Factory; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Pipe; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\DenormalizeCarry; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\NormalizeCarry; use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; use ILIAS\TestQuestionPool\Questions\Files\QuestionFiles; @@ -40,6 +42,11 @@ class CollectQuestionImages implements Pipe */ private array $files = []; + /** + * @var array $envelopes + */ + private array $envelopes = []; + public function __construct( private readonly Factory $uuid_factory, private readonly ObjectId $pool_id, @@ -56,20 +63,20 @@ public function handle(mixed $passable, \Closure $next): mixed $this->handleNormalization($passable->value); } + if ($passable instanceof DenormalizeCarry && $passable->expected === QuestionImage::class) { + $this->handleDenormalization($passable); + } + return $next($passable); } private function handleNormalization(QuestionImage $envelope): void { - // Build the absolute source path - $base_dir = $this->question_files->buildImagePath( - $envelope->getQuestionId(), - $this->pool_id->toInt() - ); - - if ($envelope->getType() === QuestionImage::TYPE_SOLUTION) { - $base_dir = str_replace('images/', 'solution/', $base_dir); - } + $pool_id = $this->pool_id->toInt(); + + $base_dir = $envelope->getType() === QuestionImage::TYPE_ANSWER + ? $this->question_files->buildImagePath($envelope->getQuestionId(), $pool_id) + : $this->question_files->buildSolutionPath($envelope->getQuestionId(), $pool_id); $source_path = $base_dir . $envelope->getFilename(); @@ -83,6 +90,19 @@ private function handleNormalization(QuestionImage $envelope): void $this->files[] = ['from' => $source_path, 'to' => $target_path]; } + private function handleDenormalization(DenormalizeCarry $passable): void + { + $envelope = $passable->result(); + if (!$envelope instanceof QuestionImage) { + throw new NormalizingException('Expected question image envelope, got ' . get_debug_type($envelope)); + } + + $extension = pathinfo($envelope->getFilename(), PATHINFO_EXTENSION); + $path = $envelope->getId() . '.' . $extension; + + $this->envelopes[$path] = $envelope; + } + /** * @return list */ @@ -90,4 +110,12 @@ public function getFiles(): array { return $this->files; } + + /** + * @return array + */ + public function getEnvelopes(): array + { + return $this->envelopes; + } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php index 24fba228ccbf..ec98b3fd5dea 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php @@ -27,7 +27,10 @@ use ilCtrl; use ilDBInterface; use ILIAS\Data\ReferenceId; +use ILIAS\Data\ObjectId; +use ILIAS\Data\UUID\Factory; use ILIAS\Language\Language; +use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Deserializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; @@ -37,6 +40,8 @@ use ILIAS\TestQuestionPool\ExportImport\Envelopes\Feedback; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionSelectionStage; use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; +use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; +use ILIAS\TestQuestionPool\Questions\Files\QuestionFiles; use ilImportMapping; use ilObjQuestionPool; use ilUnitConfigurationRepository; @@ -70,7 +75,8 @@ public function import( ImportContext $context ): ImportContext { $id_mapping_pipe = new IdMappingPipe($mapping, self::COMPONENT); - $tt = $this->builder->withAdditionalPipes(append: [$id_mapping_pipe])->create(); + $images_pipe = new CollectQuestionImages(new Factory(), new ObjectId(0)); + $tt = $this->builder->withAdditionalPipes(append: [$id_mapping_pipe, $images_pipe])->create(); $selected_questions = QuestionSelectionStage::getSelectedQuestions($context); @@ -115,6 +121,9 @@ function (array $assignments) use ($tt, &$context): void { $deserializer->process(); + // Copy the question images from the temporary import directory to the question pool directory + $this->importQuestionImages($mapping, $context, $images_pipe); + return $context; } @@ -247,4 +256,30 @@ protected function importFormulaQuestion( $question->addResult($result); } } + + protected function importQuestionImages( + ilImportMapping $mapping, + ImportContext $context, + CollectQuestionImages $pipe, + ): void { + $import_dir = dirname($context->get('import_file')) . DIRECTORY_SEPARATOR . 'expDir_1'; + + $question_files = new QuestionFiles(); + foreach ($pipe->getEnvelopes() as $from_path => $envelope) { + $question_id = $mapping->getMapping('components/ILIAS/TestQuestionPool', 'question', (string) $envelope->getQuestionId()); + if (!$question_id) { + continue; + } + + $base_dir = $envelope->getType() === QuestionImage::TYPE_ANSWER + ? $question_files->buildImagePath($question_id, $context->get('pool_obj_id')) + : $question_files->buildSolutionPath($question_id, $context->get('pool_obj_id')); + + if (!file_exists($base_dir)) { + mkdir($base_dir, 0755, true); + } + + copy($import_dir . DIRECTORY_SEPARATOR . $from_path, $base_dir . $envelope->getFilename()); + } + } } diff --git a/components/ILIAS/TestQuestionPool/src/Questions/Files/QuestionFiles.php b/components/ILIAS/TestQuestionPool/src/Questions/Files/QuestionFiles.php index 9c515f468ae3..0740c6e5f5b8 100755 --- a/components/ILIAS/TestQuestionPool/src/Questions/Files/QuestionFiles.php +++ b/components/ILIAS/TestQuestionPool/src/Questions/Files/QuestionFiles.php @@ -90,4 +90,9 @@ public function buildImagePath($questionId, $parentObjectId): string { return CLIENT_WEB_DIR . '/assessment/' . $parentObjectId . '/' . $questionId . '/images/'; } + + public function buildSolutionPath($questionId, $parentObjectId): string + { + return CLIENT_WEB_DIR . '/assessment/' . $parentObjectId . '/' . $questionId . '/solution/'; + } } From b63c37c1b6773693ae498c0eb42939658c968d2c Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Mon, 13 Apr 2026 16:24:31 +0200 Subject: [PATCH 08/66] feat(qpl): introduce import cleanup stage --- .../classes/class.ilObjQuestionPoolGUI.php | 8 +- .../Importing/ImportStageRunner.php | 16 +++- .../src/ExportImport/Import/CleanupStage.php | 80 +++++++++++++++++++ .../Import/UploadValidationStage.php | 1 + 4 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php index ae9c98d7cf38..171bb9aedae3 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php @@ -19,6 +19,7 @@ declare(strict_types=1); use ILIAS\Skill\Service\SkillUsageService; +use ILIAS\TestQuestionPool\ExportImport\Import\CleanupStage; use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\TestQuestionPool\Import\TestQuestionsImportTrait; use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; @@ -128,7 +129,8 @@ public function __construct() $local_dic = QuestionPoolDIC::dic(); $this->request_data_collector = $local_dic['request_data_collector']; $this->questionrepository = $local_dic['question.general_properties.repository']; - $this->global_test_settings = $local_dic['global_test_settings'];; + $this->global_test_settings = $local_dic['global_test_settings']; + ; $this->import_session_repository = $local_dic['exportimport.session']; parent::__construct('', $this->request_data_collector->getRefId(), true, false); @@ -1353,6 +1355,7 @@ private function buildImportStageRunner(): ImportStageRunner new PersistStage($this->lng, $this->request_data_collector, $this->import_session_repository), ], $this->import_session_repository, + new CleanupStage() ); } @@ -1366,14 +1369,11 @@ private function renderImportStage(ImportStageRunner $runner, StageResult $resul private function renderImportError(ImportStageRunner $runner, StageResult $result): void { - $runner->reset(); $this->tpl->setOnScreenMessage('failure', $result->error_message, true); - $this->ctrl->redirectByClass(self::class, self::DEFAULT_CMD); } private function renderImportSuccess(ImportStageRunner $runner, StageResult $result): void { - $runner->reset(); $this->tpl->setOnScreenMessage('success', $this->lng->txt('object_imported'), true); $this->ctrl->setParameter($this, 'ref_id', $result->context->get('pool_obj_id')); $this->ctrl->redirectByClass(self::class, self::DEFAULT_CMD); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php index c31a51d505b2..29ba3497b506 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php @@ -20,6 +20,7 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Importing; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ImportStage; use ILIAS\UI\Component\Listing\Workflow\Linear; use ILIAS\UI\Component\Listing\Workflow\Step; use ILIAS\UI\Factory as UIFactory; @@ -36,6 +37,7 @@ class ImportStageRunner public function __construct( private readonly array $stages, private readonly ImportSessionRepository $session, + private readonly ?ImportStage $final_stage = null, ) { } @@ -68,12 +70,16 @@ public function run(ServerRequestInterface $request): StageResult return $result; case StageResultType::ERROR: + $this->session->setContext($result->context); + $this->reset($request); + return $result; + case StageResultType::INTERACT: $this->session->setContext($result->context); return $result; case StageResultType::COMPLETE: - $this->session->clear(); + $this->reset($request); return $result; } @@ -113,10 +119,14 @@ public function buildWorkflow(UIFactory $ui, string $title): Linear } /** - * Reset the import stage session. + * Reset the import stage session. If a final stage is set, it will be processed. */ - public function reset(): void + public function reset(ServerRequestInterface $request): void { + if ($this->final_stage) { + $this->final_stage->process($this->session->getContext(), $request); + } + $this->session->clear(); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php new file mode 100644 index 000000000000..e02a2b04a961 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php @@ -0,0 +1,80 @@ +get('file_to_import'); + if ($file_to_import !== null) { + $temp_dir = dirname($file_to_import); + if (file_exists($temp_dir) && is_dir($temp_dir)) { + $this->removeDirectory($temp_dir); + } + } + + $import_base_dir = $context->get('import_base_dir'); + if (file_exists($import_base_dir) && is_dir($import_base_dir)) { + $this->removeDirectory($import_base_dir); + } + + return StageResult::complete($context); + } + + private function removeDirectory(string $path): void + { + $files = array_diff(scandir($path), ['.', '..']); + foreach ($files as $file) { + if (is_dir("$path/$file")) { + $this->removeDirectory("$path/$file"); + } else { + unlink("$path/$file"); + } + } + + rmdir($path); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php index 0cb137a55fc6..155c53229e99 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php @@ -90,6 +90,7 @@ public function process(ImportContext $context, ServerRequestInterface $request) return StageResult::advance( $context->with('import_file', $import_base_dir . DIRECTORY_SEPARATOR . $export_file['path']) + ->with('import_base_dir', $import_base_dir) ->with('install_id', $manifest->getInstallId()) ); } From 3d3228a1510a8c439d74be9f3c10ac5cc0aab6bb Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Mon, 13 Apr 2026 16:38:10 +0200 Subject: [PATCH 09/66] refactor(qpl): remove questions import in question pool --- .../classes/class.ilObjQuestionPoolGUI.php | 210 +----------------- 1 file changed, 1 insertion(+), 209 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php index 171bb9aedae3..4f45c6373b3d 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php @@ -744,133 +744,6 @@ public function download_paragraphObject(): void exit; } - public function importVerifiedQuestionsFileObject(): void - { - $file_to_import = ilSession::get('path_to_import_file'); - - if (mb_substr($file_to_import, -3) === 'xml') { - $importdir = dirname($file_to_import); - $selected_questions = $this->retrieveSelectedQuestionsFromImportQuestionsSelectionForm( - 'importVerifiedQuestionsFile', - $importdir, - $file_to_import, - $this->request - ); - $this->importQuestionsFromQtiFile( - $this->getObject(), - $selected_questions, - $file_to_import, - $importdir - ); - } else { - list($subdir, $importdir, $xmlfile, $qtifile) = $this->buildImportDirectoriesFromImportFile($file_to_import); - $selected_questions = $this->retrieveSelectedQuestionsFromImportQuestionsSelectionForm( - 'importVerifiedQuestionsFile', - $importdir, - $qtifile, - $this->request - ); - if (is_file($importdir . DIRECTORY_SEPARATOR . 'manifest.xml')) { - $this->importQuestionPoolWithValidManifest( - $this->getObject(), - $selected_questions, - $file_to_import - ); - } else { - $this->importQuestionsFromQtiFile( - $this->getObject(), - $selected_questions, - $qtifile, - $importdir, - $xmlfile - ); - } - } - - $this->cleanupAfterImport($importdir); - - $this->tpl->setOnScreenMessage('success', $this->lng->txt('object_imported'), true); - $this->questionsObject(); - } - - public function uploadQuestionsImportObject(): void - { - $import_questions_modal = $this->buildImportQuestionsModal()->withRequest($this->request); - $data = $import_questions_modal->getData(); - if ($data === null) { - $this->questionsObject( - $import_questions_modal->withOnLoad( - $import_questions_modal->getShowSignal() - ) - ); - return; - } - $path_to_imported_file_in_temp_dir = $data['import_file'][0]; - $this->importQuestionsFile($path_to_imported_file_in_temp_dir); - } - - private function buildImportQuestionsModal(): RoundTripModal - { - $constraint = $this->refinery->custom()->constraint( - function ($vs): bool { - if ($vs === []) { - return false; - } - return true; - }, - $this->lng->txt('msg_no_files_selected') - ); - - $file_upload_input = $this->ui_factory->input()->field() - ->file(new \QuestionPoolImportUploadHandlerGUI(), $this->lng->txt('import_file')) - ->withAcceptedMimeTypes(self::SUPPORTED_IMPORT_MIME_TYPES) - ->withMaxFiles(1) - ->withAdditionalTransformation($constraint); - return $this->ui_factory->modal()->roundtrip( - $this->lng->txt('import'), - [], - ['import_file' => $file_upload_input], - $this->ctrl->getFormActionByClass(self::class, 'uploadQuestionsImport') - )->withSubmitLabel($this->lng->txt('import')); - } - - private function importQuestionsFromQtiFile( - ilObjQuestionPool $obj, - array $selected_questions, - string $qtifile, - string $importdir, - string $xmlfile = '' - ): void { - $qti_parser = new ilQTIParser( - $importdir, - $qtifile, - ilQTIParser::IL_MO_PARSE_QTI, - $obj->getId(), - $selected_questions - ); - $qti_parser->startParsing(); - - if ($xmlfile === '') { - return; - } - - $cont_parser = new ilQuestionPageParser( - $obj, - $xmlfile, - $importdir - ); - $cont_parser->setQuestionMapping($qti_parser->getImportMapping()); - $cont_parser->startParsing(); - } - - private function cleanupAfterImport(string $importdir): void - { - ilFileUtils::delDir($importdir); - $this->deleteUploadedImportFile(ilSession::get('path_to_uploaded_file_in_temp_dir')); - ilSession::clear('path_to_import_file'); - ilSession::clear('path_to_uploaded_file_in_temp_dir'); - } - public function createQuestionObject(): void { $form = $this->buildQuestionCreationForm()->withRequest($this->request); @@ -1001,24 +874,10 @@ public function exportQuestions(array $ids): void } } - protected function renoveImportFailsObject(): void - { - $qsaImportFails = new ilAssQuestionSkillAssignmentImportFails($this->object->getId()); - $qsaImportFails->deleteRegisteredImportFails(); - - $this->ctrl->redirectByClass( - [ - ilRepositoryGUI::class, - self::class, - ilInfoScreenGUI::class - ] - ); - } - /** * list questions of question pool */ - public function questionsObject(?RoundTripModal $import_questions_modal = null): void + public function questionsObject(): void { if (!$this->access->checkAccess("read", "", $this->request_data_collector->getRefId())) { $this->infoScreenForward(); @@ -1048,17 +907,6 @@ public function questionsObject(?RoundTripModal $import_questions_modal = null): ); $this->toolbar->addComponent($btn); - if ($import_questions_modal === null) { - $import_questions_modal = $this->buildImportQuestionsModal(); - } - - $btn_import = $this->ui_factory->button()->standard( - $this->lng->txt('import'), - $import_questions_modal->getShowSignal() - ); - $this->toolbar->addComponent($btn_import); - $out[] = $this->ui_renderer->render($import_questions_modal); - if (ilSession::get('qpl_clipboard') != null && count(ilSession::get('qpl_clipboard'))) { $btn_paste = $this->ui_factory->button()->standard( $this->lng->txt('paste'), @@ -1258,61 +1106,6 @@ public function editQuestionForTestObject(): void $this->ctrl->redirectByClass(ilAssQuestionPreviewGUI::class, 'show'); } - protected function importQuestionsFile(string $path_to_uploaded_file_in_temp_dir): void - { - if (!$this->temp_file_system->hasDir($path_to_uploaded_file_in_temp_dir) - || ($files = $this->temp_file_system->listContents($path_to_uploaded_file_in_temp_dir)) === [] - || mb_stripos($files[0]->getPath(), 'tst') !== false) { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('obj_import_file_error'), true); - $this->ctrl->redirectByClass(self::class, self::DEFAULT_CMD); - } - - $file_to_import = $this->import_temp_directory . DIRECTORY_SEPARATOR . $files[0]->getPath(); - $qtifile = $file_to_import; - $importdir = dirname($file_to_import); - - - if ($this->temp_file_system->getMimeType($files[0]->getPath()) === MimeType::APPLICATION__ZIP) { - $options = (new ILIAS\Filesystem\Util\Archive\UnzipOptions()) - ->withZipOutputPath($this->getImportTempDirectory()); - $unzip = $this->archives->unzip($this->temp_file_system->readStream($files[0]->getPath()), $options); - $unzip->extract(); - list($subdir, $importdir, $xmlfile, $qtifile) = $this->buildImportDirectoriesFromImportFile($file_to_import); - } - if (!file_exists($qtifile)) { - ilFileUtils::delDir($importdir); - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('cannot_find_xml'), true); - $this->questionsObject(); - return; - } - - ilSession::set('path_to_import_file', $file_to_import); - ilSession::set('path_to_uploaded_file_in_temp_dir', $path_to_uploaded_file_in_temp_dir); - - $form = $this->buildImportQuestionsSelectionForm( - 'importVerifiedQuestionsFile', - $importdir, - $qtifile, - $path_to_uploaded_file_in_temp_dir - ); - - if ($form === null) { - return; - } - - $panel = $this->ui_factory->panel()->standard( - $this->lng->txt('import_question'), - [ - $this->ui_factory->legacy()->content($this->lng->txt('qpl_import_verify_found_questions')), - $form - ] - ); - $this->tpl->setContent($this->ui_renderer->render($panel)); - $this->tpl->printToStdout(); - exit; - } - - protected function importFile(string $file_to_import, string $path_to_uploaded_file_in_temp_dir): void { @@ -1380,7 +1173,6 @@ private function renderImportSuccess(ImportStageRunner $runner, StageResult $res } - public function addLocatorItems(): void { $ilLocator = $this->locator; From 9ea17b2a5c4dc26b79b6d49fc0921917c8338580 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Tue, 14 Apr 2026 10:20:57 +0200 Subject: [PATCH 10/66] fix(foundation): rename export context parameter --- .../src/ExportImport/Foundation/ExportContext.php | 6 +++--- .../src/ExportImport/QuestionPoolExporter.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/ExportContext.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/ExportContext.php index b5e38b014d9f..ecef26b222f0 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/ExportContext.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/ExportContext.php @@ -41,15 +41,15 @@ class ExportContext private array $dependencies = []; public function __construct( - private ObjectId $pool_id, + private ObjectId $object_id, private ExportConfig $config, private Transformations $transformations, ) { } - public function getPoolId(): ObjectId + public function getObjectId(): ObjectId { - return $this->pool_id; + return $this->object_id; } public function getConfig(): ExportConfig diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php index 71d6821a2942..0cd53b5f4e0f 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php @@ -94,7 +94,7 @@ public function process(ExportContext $context, Serializer $serializer): ExportC $collector = new QuestionPoolCollector( $this->question_repository, $this->db, - $context->getPoolId() + $context->getObjectId() ); $serializer->group( From 2fbbfff3f586e7a4ae5b23c5603fef591242777f Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Wed, 15 Apr 2026 09:40:02 +0200 Subject: [PATCH 11/66] fix(qpl): support legacy question pool import --- .../classes/class.ilObjQuestionPoolGUI.php | 2 + .../class.ilObjQuestionPoolXMLParser.php | 135 ++++++++++++++ .../class.ilTestQuestionPoolImporter.php | 38 +++- ...class.ilTestQuestionPoolLegacyImporter.php | 170 ++++++++++++++++++ .../Foundation/Contracts/ImportStage.php | 10 +- .../Importing/ImportStageRunner.php | 4 + .../src/ExportImport/Import/CleanupStage.php | 8 +- .../Import/DetectLegacyImportStage.php | 70 ++++++++ .../src/ExportImport/Import/PersistStage.php | 4 +- .../Import/QuestionSelectionStage.php | 65 +++++-- .../Import/UploadValidationStage.php | 4 +- 11 files changed, 471 insertions(+), 39 deletions(-) create mode 100755 components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolXMLParser.php create mode 100755 components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php index 4f45c6373b3d..c1d2bd668912 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php @@ -20,6 +20,7 @@ use ILIAS\Skill\Service\SkillUsageService; use ILIAS\TestQuestionPool\ExportImport\Import\CleanupStage; +use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\TestQuestionPool\Import\TestQuestionsImportTrait; use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; @@ -1144,6 +1145,7 @@ private function buildImportStageRunner(): ImportStageRunner return new ImportStageRunner( [ new UploadValidationStage($this->archives, $this->lng, 'components/ILIAS/TestQuestionPool'), + new DetectLegacyImportStage(), new QuestionSelectionStage($this->lng, $this->component_factory, $this->ui_factory, $form_action), new PersistStage($this->lng, $this->request_data_collector, $this->import_session_repository), ], diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolXMLParser.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolXMLParser.php new file mode 100755 index 000000000000..bc2632ef42f5 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolXMLParser.php @@ -0,0 +1,135 @@ +poolOBJ = $poolOBJ; + + $this->inSettingsTag = false; + $this->inMetaDataTag = false; + $this->inMdGeneralTag = false; + + parent::__construct($xmlFile); + } + + public function setHandlers($a_xml_parser): void + { + xml_set_element_handler($a_xml_parser, $this->handlerBeginTag(...), $this->handlerEndTag(...)); + xml_set_character_data_handler($a_xml_parser, $this->handlerCharacterData(...)); + } + + public function handlerBeginTag($xmlParser, $tagName, $tagAttributes): void + { + switch ($tagName) { + case 'MetaData': + $this->inMetaDataTag = true; + break; + + case 'General': + if ($this->inMetaDataTag) { + $this->inMdGeneralTag = true; + } + break; + + case 'Title': + case 'Description': + $this->cdata = ''; + break; + + case 'Settings': + $this->inSettingsTag = true; + break; + + case 'NavTaxonomy': + case 'SkillService': + if ($this->inSettingsTag) { + $this->cdata = ''; + } + break; + } + } + + public function handlerEndTag($xmlParser, $tagName): void + { + switch ($tagName) { + case 'MetaData': + $this->inMetaDataTag = false; + break; + + case 'General': + if ($this->inMetaDataTag) { + $this->inMdGeneralTag = false; + } + break; + + case 'Title': + if (!$this->title_processed) { + $this->poolOBJ->setTitle($this->cdata); + $this->title_processed = true; + $this->cdata = ''; + } + break; + + case 'Description': + if (!$this->description_processed) { + $this->poolOBJ->setDescription($this->cdata); + $this->description_processed = true; + $this->cdata = ''; + } + break; + + case 'Settings': + $this->inSettingsTag = false; + break; + + case 'SkillService': + $this->poolOBJ->setSkillServiceEnabled((bool) $this->cdata); + $this->cdata = ''; + break; + } + } + + public function handlerCharacterData($xmlParser, $charData): void + { + if ($charData != "\n") { + // Replace multiple tabs with one space + $charData = preg_replace("/\t+/", " ", $charData); + + $this->cdata .= $charData; + } + } +} diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php index 27cae121f9b1..bb9be798b3db 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php @@ -21,48 +21,68 @@ use ILIAS\Data\ReferenceId; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\SimpleXMLDeserializer; +use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; use ILIAS\TestQuestionPool\ExportImport\QuestionPoolImporter; use ILIAS\TestQuestionPool\QuestionPoolDIC; -/** - * Importer class for question pools - * - * @author Helmut Schottmüller - * @version $Id$ - * @ingroup components\ILIASLearningModule - */ - class ilTestQuestionPoolImporter extends ilXmlImporter { protected readonly ImportSessionRepository $session; protected readonly QuestionPoolImporter $importer; + protected readonly ilTestQuestionPoolLegacyImporter $legacy_importer; public function __construct() { parent::__construct(); + $this->legacy_importer = new ilTestQuestionPoolLegacyImporter(); $local_dic = QuestionPoolDIC::dic(); $this->session = $local_dic['exportimport.session']; $this->importer = $local_dic['exportimport.importer']; } + public function init(): void + { + $this->legacy_importer->setImport($this->getImport()); + $this->legacy_importer->setImportDirectory($this->getImportDirectory()); + $this->legacy_importer->init(); + } + public function importXmlRepresentation( string $a_entity, string $a_id, string $a_xml, ilImportMapping $a_mapping ): void { + // Check if forward to legacy importer is needed + $context = $this->session->getContext(); + if (DetectLegacyImportStage::isLegacyImport($context)) { + $this->legacy_importer->setInstallId($this->getInstallId()); + $this->legacy_importer->setInstallUrl($this->getInstallUrl()); + $this->legacy_importer->setSchemaVersion($this->getSchemaVersion()); + $this->legacy_importer->setSkipEntities($this->getSkipEntities()); + $this->legacy_importer->importXmlRepresentation($a_entity, $a_id, $a_xml, $a_mapping); + return; + } + $result = $this->importer->import( new SimpleXMLDeserializer()->open($a_xml), $a_mapping, new ReferenceId($a_mapping->getTargetId()), - $this->session->getContext(), + $context, ); $this->session->setContext($result); } public function finalProcessing(ilImportMapping $a_mapping): void { + // Check if forward to legacy importer is needed + $context = $this->session->getContext(); + if (DetectLegacyImportStage::isLegacyImport($context)) { + $this->legacy_importer->finalProcessing($a_mapping); + return; + } + $this->finalizeQuestionPages($a_mapping); $this->finalizeTaxonomyUsage($a_mapping); } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php new file mode 100755 index 000000000000..921d9958f542 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php @@ -0,0 +1,170 @@ +session = $local_dic['exportimport.session']; + $this->request_data_collector = $local_dic['request_data_collector']; + } + + public function importXmlRepresentation( + string $a_entity, + string $a_id, + string $a_xml, + ilImportMapping $a_mapping + ): void { + $this->pool_obj = new ilObjQuestionPool(0, true); + $this->pool_obj->setType('qpl'); + $this->pool_obj->setTitle('dummy'); + $this->pool_obj->setDescription('questionpool import'); + $this->pool_obj->create(true); + $this->pool_obj->createReference(); + $this->pool_obj->putInTree($this->request_data_collector->getRefId()); + $this->pool_obj->setPermissions($this->request_data_collector->getRefId()); + + $a_mapping->addMapping('components/ILIAS/TestQuestionPool', 'qpl', $a_id, (string) $this->pool_obj->getId()); + + $context = $this->session->getContext(); + $import_base_dir = $context->get('import_base_dir'); + $xml_file = $context->get('xml_file'); + $context = $context->with('pool_obj_id', $this->pool_obj->getId()); + $this->session->setContext($context); + + $qpl_parser = new ilObjQuestionPoolXMLParser( + $this->pool_obj, + $xml_file + ); + $qpl_parser->startParsing(); + + // set another question pool name (if possible) + $qpl_new = $this->request_data_collector->string('qpl_new'); + if ($qpl_new !== '') { + $this->pool_obj->setTitle($qpl_new); + } + + $this->pool_obj->update(); + $this->pool_obj->saveToDb(); + + $qti_parser = new ilQTIParser( + $import_base_dir, + $context->get('qti_file'), + ilQTIParser::IL_MO_PARSE_QTI, + $this->pool_obj->getId(), + $context->get('selected_questions') + ); + $qti_parser->startParsing(); + + $page_parser = new ilQuestionPageParser( + $this->pool_obj, + $xml_file, + $import_base_dir + ); + $page_parser->setQuestionMapping($qti_parser->getImportMapping()); + $page_parser->startParsing(); + + foreach ($qti_parser->getImportMapping() as $k => $v) { + $old_question_id = substr($k, strpos($k, 'qst_') + strlen('qst_')); + $new_question_id = (string) $v['pool']; // yes, this is the new question id ^^ + + $a_mapping->addMapping( + 'components/ILIAS/Taxonomy', + 'tax_item', + "qpl:quest:{$old_question_id}", + $new_question_id + ); + + $a_mapping->addMapping( + 'components/ILIAS/Taxonomy', + 'tax_item_obj_id', + "qpl:quest:{$old_question_id}", + (string) $this->pool_obj->getId() + ); + + $a_mapping->addMapping( + 'components/ILIAS/TestQuestionPool', + 'quest', + $old_question_id, + $new_question_id + ); + } + + $this->importQuestionSkillAssignments($xml_file, $a_mapping, $this->pool_obj->getId()); + + $a_mapping->addMapping( + 'components/ILIAS/MetaData', + 'md', + "{$a_id}:0:qpl", + "{$this->pool_obj->getId()}:0:qpl" + ); + + $this->pool_obj->saveToDb(); + } + + public function finalProcessing(ilImportMapping $a_mapping): void + { + $maps = $a_mapping->getMappingsOfEntity('components/ILIAS/TestQuestionPool', 'qpl'); + foreach ($maps as $old => $new) { + if ($old !== 'new_id' && (int) $old > 0) { + $new_tax_ids = $a_mapping->getMapping('components/ILIAS/Taxonomy', 'tax_usage_of_obj', (string) $old); + if ($new_tax_ids !== null) { + $tax_ids = explode(':', $new_tax_ids); + foreach ($tax_ids as $tid) { + ilObjTaxonomy::saveUsage((int) $tid, (int) $new); + } + } + } + } + } + + protected function importQuestionSkillAssignments($xmlFile, ilImportMapping $mappingRegistry, $targetParentObjId): void + { + $parser = new ilAssQuestionSkillAssignmentXmlParser($xmlFile); + $parser->startParsing(); + + $importer = new ilAssQuestionSkillAssignmentImporter(); + $importer->setTargetParentObjId($targetParentObjId); + $importer->setImportInstallationId($this->getInstallId()); + $importer->setImportMappingRegistry($mappingRegistry); + $importer->setImportMappingComponent('components/ILIAS/TestQuestionPool'); + $importer->setImportAssignmentList($parser->getAssignmentList()); + + $importer->import(); + + if ($importer->getFailedImportAssignmentList()->assignmentsExist()) { + $fails = new ilAssQuestionSkillAssignmentImportFails($targetParentObjId); + $fails->registerFailedImports($importer->getFailedImportAssignmentList()); + + $this->pool_obj->getObjectProperties()->storePropertyIsOnline($this->pool_obj->getObjectProperties()->getPropertyIsOnline()->withOffline()); + } + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ImportStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ImportStage.php index 2dcb1fa9ff3d..bdb78f8e391d 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ImportStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ImportStage.php @@ -36,14 +36,16 @@ interface ImportStage public function getIdentifier(): string; /** - * Get the label of the stage which will be displayed in the workflow UI. + * Get the label of the stage which will be displayed in the workflow UI. If null, the stage will not be displayed + * in the workflow UI. */ - public function getLabel(): string; + public function getLabel(): ?string; /** - * Get the description of the stage which will be displayed in the workflow UI. + * Get the description of the stage which will be displayed in the workflow UI. If null, the stage will not be + * displayed in the workflow UI. */ - public function getDescription(): string; + public function getDescription(): ?string; /** * Process the current stage. On the first invocation the stage should return `StageResult::interact()` with the UI diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php index 29ba3497b506..3a4ba77dd938 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php @@ -95,6 +95,10 @@ public function buildWorkflow(UIFactory $ui, string $title): Linear $active_index = $this->session->getCurrentStageIndex(); foreach ($this->stages as $i => $stage) { + if ($stage->getLabel() === null) { + continue; + } + $step = $ui->listing()->workflow()->step( $stage->getLabel(), $stage->getDescription() diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php index e02a2b04a961..fbd99c94b976 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php @@ -36,14 +36,14 @@ public function getIdentifier(): string return 'cleanup'; } - public function getLabel(): string + public function getLabel(): ?string { - return ''; + return null; } - public function getDescription(): string + public function getDescription(): ?string { - return ''; + return null; } public function process(ImportContext $context, ServerRequestInterface $request): StageResult diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php new file mode 100644 index 000000000000..da921c6afd50 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php @@ -0,0 +1,70 @@ +get('import_base_dir'); + $import_name = basename($import_base_dir); + + $xml_file = $import_base_dir . DIRECTORY_SEPARATOR . $import_name . '.xml'; + $qti_file = $import_base_dir . DIRECTORY_SEPARATOR . str_replace('_qpl_', '_qti_', $import_name) . '.xml'; + + if (!file_exists($qti_file) || !file_exists($xml_file)) { + return StageResult::advance($context); + } + + return StageResult::advance( + $context->with('qti_file', $qti_file) + ->with('xml_file', $xml_file) + ); + } + + public static function isLegacyImport(ImportContext $context): bool + { + return $context->has('qti_file') && $context->has('xml_file'); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php index 4a0a23f215ad..83ca3be6e343 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php @@ -47,12 +47,12 @@ public function getIdentifier(): string return 'persist'; } - public function getLabel(): string + public function getLabel(): ?string { return $this->lng->txt('qpl_import_step_persist'); } - public function getDescription(): string + public function getDescription(): ?string { return ''; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php index e7cabc668d30..e7fd2b79bb9c 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php @@ -26,8 +26,6 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResult; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\SimpleXMLDeserializer; -use ILIAS\TestQuestionPool\QuestionPoolDIC; -use ILIAS\TestQuestionPool\Questions\GeneralQuestionProperties; use ILIAS\UI\Component\Input\Container\Form\Form; use ILIAS\UI\Factory as UIFactory; use Psr\Http\Message\ServerRequestInterface; @@ -65,12 +63,12 @@ public function getIdentifier(): string return 'question_selection'; } - public function getLabel(): string + public function getLabel(): ?string { return $this->lng->txt('qpl_import_step_select'); } - public function getDescription(): string + public function getDescription(): ?string { return ''; } @@ -96,20 +94,9 @@ public function process(ImportContext $context, ServerRequestInterface $request) return StageResult::error($context, $this->lng->txt('qpl_import_file_not_found')); } - $options = []; - $deserializer = new SimpleXMLDeserializer()->open(file_get_contents($context->get('import_file'))); - $deserializer->addHandler('questions', function (array $questions) use (&$options): void { - foreach ($questions as $question) { - if (!isset($question['title']) || !isset($question['type'])) { - continue; - } - - $raw_id = $question['id']; - $id = is_array($raw_id) ? (string) ($raw_id['id'] ?? '') : (string) $raw_id; - $options[$id] = "{$question['title']} ({$this->getLabelForQuestionType($question['type'])})"; - } - }); - $deserializer->process(); + $options = DetectLegacyImportStage::isLegacyImport($context) + ? $this->readQuestionsFromQTI($context) + : $this->readQuestions($context); if ($options === []) { return StageResult::error($context, $this->lng->txt('qpl_import_no_items')); @@ -137,6 +124,48 @@ public static function getSelectedQuestions(ImportContext $context): array return array_map('intval', $context->get('selected_questions', [])); } + private function readQuestions(ImportContext $context): array + { + $options = []; + + $deserializer = new SimpleXMLDeserializer()->open(file_get_contents($context->get('import_file'))); + $deserializer->addHandler('questions', function (array $questions) use (&$options): void { + foreach ($questions as $question) { + if (!isset($question['title']) || !isset($question['type'])) { + continue; + } + + $raw_id = $question['id']; + $id = is_array($raw_id) ? (string) ($raw_id['id'] ?? '') : (string) $raw_id; + $options[$id] = "{$question['title']} ({$this->getLabelForQuestionType($question['type'])})"; + } + }); + $deserializer->process(); + + return $options; + } + + /** + * @deprecated This method is only used for legacy imports and will be removed with further ILIAS versions. + */ + private function readQuestionsFromQTI(ImportContext $context): array + { + $parser = new \ilQTIParser( + $context->get('import_base_dir'), + $context->get('qti_file'), + \ilQTIParser::IL_MO_VERIFY_QTI, + 0 + ); + $parser->startParsing(); + + $options = []; + foreach ($parser->getFoundItems() as $item) { + $options[$item['ident']] = "{$item['title']} ({$this->getLabelForQuestionType($item['type'])})"; + } + + return $options; + } + private function buildSelectQuestionsForm(array $options): Form { $input = $this->ui_factory->input()->field()->multiSelect( diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php index 155c53229e99..28e602c7cd87 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php @@ -50,12 +50,12 @@ public function getIdentifier(): string return 'upload_and_validate'; } - public function getLabel(): string + public function getLabel(): ?string { return $this->lng->txt('upload'); } - public function getDescription(): string + public function getDescription(): ?string { return ''; } From db59c00f1c16db9cd628e0df946f82c7ce1d53b1 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Thu, 16 Apr 2026 11:44:54 +0200 Subject: [PATCH 12/66] refactor(foundation,qpl): introduce export bridge to enhance dependency management --- .../class.ilTestQuestionPoolExporter.php | 111 +++------- .../Foundation/Bridge/ExportState.php | 190 ++++++++++++++++++ .../Foundation/Bridge/ExportStep.php | 29 +++ .../Foundation/Bridge/StateHolder.php | 59 ++++++ .../Foundation/Bridge/XmlExporterBridge.php | 114 +++++++++++ .../Foundation/Contracts/Exporter.php | 66 ++++++ .../ExportImport/Foundation/ExportContext.php | 108 ---------- .../src/ExportImport/QuestionPoolExporter.php | 148 ++++++++------ .../TestQuestionPool/src/QuestionPoolDIC.php | 5 + 9 files changed, 585 insertions(+), 245 deletions(-) create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportStep.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/StateHolder.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Exporter.php delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/ExportContext.php diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php index 8d4536da9a38..ba4e24df7c65 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php @@ -16,39 +16,30 @@ * *********************************************************************/ -use ILIAS\Data\ObjectId; -use ILIAS\TestQuestionPool\ExportImport\Foundation\ExportContext; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\SimpleXMLSerializer; -use ILIAS\TestQuestionPool\ExportImport\QuestionPoolExporter; +use ILIAS\Export\ExportHandler\Factory as ExportHandler; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\XmlExporterBridge; use ILIAS\TestQuestionPool\QuestionPoolDIC; -/** - * Used for container export with tests - * - * @author Helmut Schottmüller - * @version $Id$ - * @ingroup components\ILIASTest - */ class ilTestQuestionPoolExporter extends ilXmlExporter { - private QuestionPoolExporter $exporter; - - /** - * @var array $batches - */ - private array $batches = []; - + use XmlExporterBridge; public function init(): void { - $this->exporter = QuestionPoolDIC::dic()['exportimport.exporter']; + global $DIC; + $local_dic = QuestionPoolDIC::dic(); + + $this->export_handler = new ExportHandler(); + $this->state_holder = $local_dic['exportimport.state_holder']; + $this->exporter = $local_dic['exportimport.exporter']; + $this->logger = $DIC->logger()->qpl()->getLogger(); } /** - * Returns the final XML content for one question pool. + * Returns the final XML content for the question pool. * * This method is called after `getXmlExportTailDependencies()`. At this point the export writer and export - * directory are available, so the prepared batch can be written to disk and finalized. + * directory are available, so the preprocessed export can be written to disk and returned as xml. */ public function getXmlRepresentation(string $a_entity, string $a_schema_version, string $a_id): string { @@ -56,15 +47,14 @@ public function getXmlRepresentation(string $a_entity, string $a_schema_version, throw new InvalidArgumentException("Invalid entity for question pool export: {$a_entity}"); } - return $this->finalizeExport((int) $a_id)->getContent(); + return $this->finalizeExport()->getContent(); } /** - * Collects export tail dependencies for one or more question pools. + * Collects export tail dependencies for the question pool. * * The export framework calls this method before `getXmlRepresentation()`. Therefore this method only prepares and - * processes the export batch in memory and caches the context, because writer and export directory are not yet - * initialized here. + * processes the export in memory using the export state. The export state is created if it does not exist yet. */ public function getXmlExportTailDependencies(string $a_entity, string $a_target_release, array $a_ids): array { @@ -72,68 +62,33 @@ public function getXmlExportTailDependencies(string $a_entity, string $a_target_ throw new InvalidArgumentException("Invalid entity for question pool export: {$a_entity}"); } - $dependencies = []; - foreach ($a_ids as $id) { - $context = $this->processExport((int) $id); - $dependencies = array_merge($dependencies, $context->getDependencies()); + // If the default export option was used, the state is not initialized yet. + if ($this->state_holder->exists() === false) { + $this->initExportState( + 'components/ILIAS/TestQuestionPool', + $a_target_release, + $a_entity, + $a_ids + ); } - return $dependencies; + return $this->processExport()->getDependencies(); } /** - * Returns schema versions that the component can export to. - * ILIAS chooses the first one, that has min/max constraints which - * fit to the target release. Please put the newest on top. - * @return array + * Returns schema versions that the component can export to. ILIAS chooses the first one, that has min/max + * constraints which fit to the target release. */ public function getValidSchemaVersions(string $a_entity): array { return [ - "4.1.0" => [ - "namespace" => "http://www.ilias.de/Modules/TestQuestionPool/htlm/4_1", - "xsd_file" => "ilias_qpl_4_1.xsd", - "uses_dataset" => false, - "min" => "4.1.0", - "max" => ""] + '4.1.0' => [ + 'namespace' => 'http://www.ilias.de/Modules/TestQuestionPool/htlm/4_1', + 'xsd_file' => 'ilias_qpl_4_1.xsd', + 'uses_dataset' => false, + 'min' => '4.1.0', + 'max' => '' + ] ]; } - - /** - * Prepares and processes a question pool export in memory. The resulting context is cached per pool and reused - * across calls. - */ - private function processExport(int $pool_id): ExportContext - { - if (isset($this->batches[$pool_id])) { - return $this->batches[$pool_id]; - } - - $context = $this->exporter->prepare( - new ObjectId($pool_id), - $this->exp->getExportConfigs() - ); - - $context = $this->exporter->process( - $context, - new SimpleXMLSerializer()->open('memory') - ); - - $this->batches[$pool_id] = $context; - return $context; - } - - /** - * Finalizes a prepared export context and writes it to the export directory. - */ - private function finalizeExport(int $pool_id): ExportContext - { - $context = $this->processExport($pool_id); - - return $this->exporter->write( - $context, - $this->exp->getExportWriter(), - $this->exp->getPathToComponentExpDirInContainer() - ); - } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php new file mode 100644 index 000000000000..1afb13e04b03 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php @@ -0,0 +1,190 @@ +}> $dependencies + */ + private array $dependencies = []; + + public function __construct( + private ExportTarget $target, + private ExportConfig $config + ) { + $this->step = ExportStep::INIT; + } + + public function target(): ExportTarget + { + return $this->target; + } + + public function config(): ExportConfig + { + return $this->config; + } + + public function getStep(): ExportStep + { + return $this->step; + } + + public function setStep(ExportStep $step): void + { + $this->step = $step; + } + + public function assertStep(ExportStep $step): void + { + if ($this->step->value < $step->value) { + throw new RuntimeException("Expected step {$step->name}, but got {$this->step->name} instead"); + } + + $this->step = $step; + } + + public function logger(): Logger + { + $this->assertNotNull($this->logger, 'logger'); + return $this->logger; + } + + public function setLogger(Logger $logger): void + { + $this->logger = $logger; + } + + public function path(): ExportPath + { + $this->assertNotNull($this->path_info, 'path_info'); + return $this->path_info; + } + + public function setPathInfo(ExportPath $path_info): void + { + $this->path_info = $path_info; + } + + public function collector(): DataCollector + { + $this->assertNotNull($this->collector, 'collector'); + return $this->collector; + } + + public function setCollector(DataCollector $collector): void + { + $this->collector = $collector; + } + + public function transformations(): Transformations + { + $this->assertNotNull($this->transformations, 'transformations'); + return $this->transformations; + } + + public function setTransformations(Transformations $transformations): void + { + $this->transformations = $transformations; + } + + public function serializer(): Serializer + { + $this->assertNotNull($this->serializer, 'serializer'); + return $this->serializer; + } + + public function setSerializer(Serializer $serializer): void + { + $this->serializer = $serializer; + } + + public function writer(): ExportWriter + { + $this->assertNotNull($this->writer, 'writer'); + return $this->writer; + } + + public function setWriter(ExportWriter $writer): void + { + $this->writer = $writer; + } + + public function getDependencies(): array + { + return array_values($this->dependencies); + } + + public function addDependency(string $component, string $entity, array $ids): void + { + $key = "{$component}::{$entity}"; + + if (!isset($this->dependencies[$key])) { + $this->dependencies[$key] = [ + 'component' => $component, + 'entity' => $entity, + 'ids' => [], + ]; + } + + $this->dependencies[$key]['ids'] = array_values(array_unique(array_merge( + $this->dependencies[$key]['ids'], + $ids + ))); + } + + public function getContent(): string + { + return $this->serializer()->write(); + } + + private function assertNotNull(mixed $value, string $property): void + { + if ($value === null) { + throw new RuntimeException( + "{$property} not set. This may happen if the exporter steps are not executed in the correct order." + ); + } + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportStep.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportStep.php new file mode 100644 index 000000000000..bcbcf3f04b53 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportStep.php @@ -0,0 +1,29 @@ +export_state = new ExportState($target, $config); + return $this->export_state; + } + + public function exists(): bool + { + return $this->export_state !== null; + } + + public function get(): ExportState + { + if ($this->export_state === null) { + throw new RuntimeException('Export state not found. You need to create the state first.'); + } + return $this->export_state; + } + + public function set(ExportState $export_state): void + { + $this->export_state = $export_state; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php new file mode 100644 index 000000000000..3c652352a049 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php @@ -0,0 +1,114 @@ +state_holder->get(); + + if ($state->getStep()->value < ExportStep::PREPARE->value) { + $state->setLogger($this->logger); + $this->exporter->prepare($state); + } + + if ($state->getStep()->value < ExportStep::PROCESS->value) { + $state->setSerializer(new SimpleXMLSerializer()->open('memory')); + $this->exporter->process($state); + } + + $this->state_holder->set($state); + return $state; + } + + /** + * Finalizes the export by setting the path info and writer and calling the write step of the exporter. It performs + * the prepare and process steps if not already done. + */ + private function finalizeExport(): ExportState + { + $state = $this->processExport(); + + if ($state->getStep()->value < ExportStep::WRITE->value) { + $state->setPathInfo($this->createPathInfo()); + $state->setWriter($this->exp->getExportWriter()); + + $this->exporter->write($state); + } + + $this->state_holder->set($state); + return $state; + } + + private function initExportState( + string $component, + string $target_release, + string $type, + array $object_ids + ): ExportState { + $target = $this->export_handler->target()->handler() + ->withType($type) + ->withTargetRelease($target_release) + ->withObjectIds($object_ids) + ->withClassname(static::class) + ->withComponent($component); + + return $this->state_holder->create( + $target, + $this->export_handler->consumer()->exportConfig()->collection() + ); + } + + private function createPathInfo(): ExportPath + { + return $this->export_handler->info()->export()->path()->handler() + ->withPathToComponentDirInContainer($this->exp->getExportDirInContainer()) + ->withPathToComponentExpDirInContainer($this->exp->getPathToComponentExpDirInContainer()) + ->withSetNumber($this->exp->getSetNumber()) + ->withIsContainerExport($this->exp->isContainerExport()); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Exporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Exporter.php new file mode 100644 index 000000000000..0dcd56624270 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Exporter.php @@ -0,0 +1,66 @@ +}> $dependencies - */ - private array $dependencies = []; - - public function __construct( - private ObjectId $object_id, - private ExportConfig $config, - private Transformations $transformations, - ) { - } - - public function getObjectId(): ObjectId - { - return $this->object_id; - } - - public function getConfig(): ExportConfig - { - return $this->config; - } - - public function getTransformations(): Transformations - { - return $this->transformations; - } - - public function getSerializer(): Serializer - { - if ($this->serializer === null) { - throw new RuntimeException( - 'Serializer not set. This may happen if the exporter steps are not executed in the correct order.' - ); - } - - return $this->serializer; - } - - public function setSerializer(Serializer $serializer): void - { - $this->serializer = $serializer; - } - - public function getDependencies(): array - { - return array_values($this->dependencies); - } - - public function addDependency(string $component, string $entity, array $ids): void - { - $key = "{$component}::{$entity}"; - - if (!isset($this->dependencies[$key])) { - $this->dependencies[$key] = [ - 'component' => $component, - 'entity' => $entity, - 'ids' => [], - ]; - } - - $this->dependencies[$key]['ids'] = array_values(array_unique(array_merge( - $this->dependencies[$key]['ids'], - $ids - ))); - } - - public function getContent(): string - { - return $this->getSerializer()->write(); - } -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php index 0cd53b5f4e0f..5fd121dee14e 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php @@ -22,14 +22,15 @@ use assFormulaQuestion; use ilDBInterface; +use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\ObjectId; use ILIAS\Data\UUID\Factory as UUIDFactory; -use ILIAS\Export\ExportHandler\I\Consumer\ExportWriter\HandlerInterface as ExportWriter; -use ILIAS\Export\ExportHandler\I\Consumer\ExportConfig\CollectionInterface as ExportConfig; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportState; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportStep; use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Exporter; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Serializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\ExportContext; use ILIAS\Taxonomy\DomainService as Taxonomy; use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; @@ -39,10 +40,11 @@ * to normalize the data and then writes the normalized data to the serializer. It also copies the needed files to the * export directory. */ -class QuestionPoolExporter +class QuestionPoolExporter implements Exporter { public function __construct( private readonly Builder $builder, + private readonly DataFactory $data_factory, private readonly GeneralQuestionPropertiesRepository $question_repository, private readonly ilDBInterface $db, private readonly Taxonomy $taxonomy @@ -50,108 +52,135 @@ public function __construct( } /** - * Performs the export for a given question pool. It returns the export context which contains the serialized data - * and the dependencies of the export. + * Prepares the export by creating the transformations and the question image pipe. */ - public function export( - ObjectId $pool_id, - ExportConfig $config, - Serializer $serializer, - ExportWriter $writer, - string $export_dir - ): ExportContext { - $context = $this->prepare($pool_id, $config); - $context = $this->process($context, $serializer); - return $this->write($context, $writer, $export_dir); - } - - /** - * Prepares the export context by creating the transformations and the question image pipe. It returns the export - * context which is used to share the context between the prepare, process and write steps. - */ - public function prepare(ObjectId $pool_id, ExportConfig $config): ExportContext + public function prepare(ExportState $state): void { + $state->assertStep(ExportStep::INIT); + $state->setStep(ExportStep::PREPARE); + + $pool_id = $this->extractObjectId($state); + if ($pool_id === null) { + return; + } + + $collector = new QuestionPoolCollector( + $this->question_repository, + $this->db, + $pool_id + ); + $state->setCollector($collector); + $question_image_pipe = new CollectQuestionImages( new UUIDFactory(), $pool_id ); - $transformations = $this->builder->withAdditionalPipes([$question_image_pipe]) + $transformations = $this->builder + ->withAdditionalPipes([$question_image_pipe]) ->create(); - - return new ExportContext($pool_id, $config, $transformations); + $state->setTransformations($transformations); } /** * Normalizes the question pool object and its questions and writes them to the serializer. It also collects the * dependencies of the export. */ - public function process(ExportContext $context, Serializer $serializer): ExportContext + public function process(ExportState $state): void { - $context->setSerializer($serializer); - $tt = $context->getTransformations(); - - $collector = new QuestionPoolCollector( - $this->question_repository, - $this->db, - $context->getObjectId() - ); + $state->assertStep(ExportStep::PREPARE); + $state->setStep(ExportStep::PROCESS); - $serializer->group( + $state->serializer()->group( 'general', - fn() => $this->exportObject($collector, $tt, $serializer, $context) + fn() => $this->exportObject( + $state->collector(), + $state->transformations(), + $state->serializer(), + $state + ) ); - $serializer->group( + $state->serializer()->group( 'questions', - fn() => $this->exportQuestions($collector, $tt, $serializer, $context) + fn() => $this->exportQuestions( + $state->collector(), + $state->transformations(), + $state->serializer(), + $state + ) ); - $serializer->group( + $state->serializer()->group( 'skill_assignments', - fn() => $this->exportSkillAssignments($collector, $tt, $serializer) + fn() => $this->exportSkillAssignments( + $state->collector(), + $state->transformations(), + $state->serializer(), + ) ); - - return $context; } /** * Finalizes the export by copying the question images to the export directory and returning the export context. */ - public function write(ExportContext $export, ExportWriter $writer, string $export_dir): ExportContext + public function write(ExportState $state): void { - // Copy the question images to the export directory - $question_image_pipe = $export->getTransformations()->context(CollectQuestionImages::class); + $state->assertStep(ExportStep::PROCESS); + $state->setStep(ExportStep::WRITE); + + $export_dir = $state->path()->getPathToComponentExpDirInContainer(); + $question_image_pipe = $state->transformations()->context(CollectQuestionImages::class); + foreach ($question_image_pipe->getFiles() as $file) { - $writer->writeFileByFilePath($file['from'], "{$export_dir}/" . $file['to']); + $state->writer()->writeFileByFilePath( + $file['from'], + "{$export_dir}/" . $file['to'] + ); } - - return $export; } - protected function exportObject( + private function extractObjectId(ExportState $state): ?ObjectId + { + $target_ids = $state->target()->getObjectIds(); + + if (count($target_ids) === 0) { + $state->logger()->warning('No target object IDs found for question pool export'); + return null; + } + + if (count($target_ids) > 1) { + $state->logger()->warning( + 'Multiple target object IDs found for question pool export. Only the first one will be used.' + ); + } + + return $this->data_factory->objId(array_shift($target_ids)); + } + + private function exportObject( QuestionPoolCollector $collector, Transformations $transformations, Serializer $serializer, - ExportContext $export + ExportState $state ): void { $serializer->append('object', $transformations->normalize($collector->getObject())); $obj_id = $collector->getPoolId()->toInt(); - $export->addDependency('components/ILIAS/ILIASObject', 'common', [$obj_id]); - $export->addDependency('components/ILIAS/MetaData', 'qpl', ["{$obj_id}:0:qpl"]); - $export->addDependency( + $state->addDependency('components/ILIAS/ILIASObject', 'common', [$obj_id]); + $state->addDependency('components/ILIAS/MetaData', 'qpl', ["{$obj_id}:0:qpl"]); + $state->addDependency( 'components/ILIAS/Taxonomy', 'tax', $this->taxonomy->getUsageOfObject($obj_id) ); } - protected function exportQuestions( + private function exportQuestions( QuestionPoolCollector $collector, Transformations $transformations, Serializer $serializer, - ExportContext $export + ExportState $state ): void { foreach ($collector->getQuestionObjects() as $question) { $normalized = [ @@ -162,15 +191,16 @@ protected function exportQuestions( ]; if ($question instanceof assFormulaQuestion) { - $normalized['formula_data'] = $transformations->normalize($collector->getUnitsAndCategories($question->getId())); + $data = $collector->getUnitsAndCategories($question->getId()); + $normalized['formula_data'] = $transformations->normalize($data); } $serializer->append('question', $normalized); - $export->addDependency('components/ILIAS/COPage', 'pg', ["qpl:{$question->getId()}"]); + $state->addDependency('components/ILIAS/COPage', 'pg', ["qpl:{$question->getId()}"]); } } - protected function exportSkillAssignments( + private function exportSkillAssignments( QuestionPoolCollector $collector, Transformations $transformations, Serializer $serializer, diff --git a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php index 425580ba50fa..6ef905266324 100755 --- a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php +++ b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php @@ -20,6 +20,8 @@ namespace ILIAS\TestQuestionPool; +use ILIAS\Data\Factory as DataFactory; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\StateHolder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\QuestionPoolExporter; @@ -77,9 +79,12 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC, $c ); + $dic['exportimport.state_holder'] = static fn($c): StateHolder => + new StateHolder(); $dic['exportimport.exporter'] = static fn($c): QuestionPoolExporter => new QuestionPoolExporter( $c['exportimport.builder'], + new DataFactory(), $c['question.general_properties.repository'], $DIC->database(), $DIC->taxonomy()->domain() From 23cab6529005a656428b7dddee473d83aadc4299 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Fri, 17 Apr 2026 09:48:02 +0200 Subject: [PATCH 13/66] refactor(qpl): introduce trait to share question collection logic --- .../CollectsQuestions.php} | 81 ++++++----------- .../Export/QuestionPoolCollector.php | 88 +++++++++++++++++++ .../{ => Export}/QuestionPoolExporter.php | 4 +- .../TestQuestionPool/src/QuestionPoolDIC.php | 2 +- 4 files changed, 116 insertions(+), 59 deletions(-) rename components/ILIAS/TestQuestionPool/src/ExportImport/{QuestionPoolCollector.php => Export/CollectsQuestions.php} (74%) create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolCollector.php rename components/ILIAS/TestQuestionPool/src/ExportImport/{ => Export}/QuestionPoolExporter.php (98%) diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolCollector.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/CollectsQuestions.php similarity index 74% rename from components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolCollector.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Export/CollectsQuestions.php index 860cfb630b0a..0cfb3dde61f8 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolCollector.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/CollectsQuestions.php @@ -18,84 +18,53 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport; +namespace ILIAS\TestQuestionPool\ExportImport\Export; use assFormulaQuestionUnit; use assFormulaQuestionUnitCategory; use assQuestion; use Generator; use ilAssQuestionSkillAssignmentList; -use ilDBInterface; -use ilObjQuestionPool; use ilAssClozeTestFeedback; use ilAssMultiOptionQuestionFeedback; use ilAssSpecificFeedbackIdentifierList; +use ilDBInterface; use ILIAS\Data\ObjectId; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\DataCollector; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Envelopes\Feedback; use ILIAS\TestQuestionPool\Questions\GeneralQuestionProperties; -use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ilUnitConfigurationRepository; /** - * Collector to aggregate data from the question pool for export. + * Trait to collect questions and related data from a question pool or test object. */ -class QuestionPoolCollector implements DataCollector +trait CollectsQuestions { - /** @var array $questions */ - private ?array $questions = null; - private ?ilObjQuestionPool $pool_object = null; - private ?ilAssQuestionSkillAssignmentList $skill_assignments = null; - - public function __construct( - private readonly GeneralQuestionPropertiesRepository $question_repository, - private readonly ilDBInterface $db, - private readonly ObjectId $pool_id - ) { - } - /** - * Get the ID of the question pool. + * Get the question properties for all questions related to the target object. * - * @return ObjectId + * @return array */ - public function getPoolId(): ObjectId - { - return $this->pool_id; - } + abstract public function getQuestionProperties(): array; /** - * Get the object of the question pool. It will be loaded from the database if not already loaded. + * Get the object ID of the object that contains the questions. */ - public function getObject(): ilObjQuestionPool - { - if ($this->pool_object === null) { - $this->pool_object = new ilObjQuestionPool($this->pool_id->toInt(), false); - $this->pool_object->read(); - } - - return $this->pool_object; - } + abstract public function getObjectId(): ObjectId; /** - * Collect the question properties for all questions in the question pool. - * - * @return array + * Get the database interface. */ - public function getQuestionProperties(): array - { - if ($this->questions === null) { - $this->questions = $this->question_repository->getForParentObjectId($this->pool_id->toInt()); - } - return $this->questions; - } + abstract private function database(): ilDBInterface; + + + private ?ilAssQuestionSkillAssignmentList $skill_assignments = null; /** - * Collect the question objects for all questions in the question pool. - * - * @return Generator - */ + * Collect the question objects by instantiating the question objects. + * + * @return Generator + */ public function getQuestionObjects(): Generator { foreach ($this->getQuestionProperties() as $question) { @@ -109,7 +78,7 @@ public function getQuestionObjects(): Generator /** * Get all unit categories and units for a formula question. - * + * * @return array{categories: list, base_units: list, units: list} */ public function getUnitsAndCategories(int $question_id): array @@ -122,12 +91,12 @@ public function getUnitsAndCategories(int $question_id): array ]; foreach ($repository->getCategorizedUnits() as $item) { - if($item instanceof assFormulaQuestionUnitCategory) { + if ($item instanceof assFormulaQuestionUnitCategory) { $data['categories'][] = $item; } - if($item instanceof assFormulaQuestionUnit) { - if($item->getBaseUnit() === 0 || $item->getBaseUnit() === $item->getId()) { + if ($item instanceof assFormulaQuestionUnit) { + if ($item->getBaseUnit() === 0 || $item->getBaseUnit() === $item->getId()) { $data['base_units'][] = $item; } else { $data['units'][] = $item; @@ -212,8 +181,8 @@ private function loadSpecificFeedback(assQuestion $question): array public function getSkillAssignments(): array { if ($this->skill_assignments === null) { - $this->skill_assignments = new ilAssQuestionSkillAssignmentList($this->db); - $this->skill_assignments->setParentObjId($this->pool_id->toInt()); + $this->skill_assignments = new ilAssQuestionSkillAssignmentList($this->database()); + $this->skill_assignments->setParentObjId($this->getObjectId()->toInt()); $this->skill_assignments->loadFromDb(); $this->skill_assignments->loadAdditionalSkillData(); } @@ -225,7 +194,7 @@ public function getSkillAssignments(): array $this->skill_assignments->getAssignmentsByQuestionId($question->getQuestionId()) ); } - + return $assignments; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolCollector.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolCollector.php new file mode 100644 index 000000000000..28f91dfb8be1 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolCollector.php @@ -0,0 +1,88 @@ + $questions */ + private ?array $questions = null; + private ?ilObjQuestionPool $pool_object = null; + + public function __construct( + private readonly GeneralQuestionPropertiesRepository $question_repository, + private readonly ilDBInterface $db, + private readonly ObjectId $pool_id + ) { + } + + /** + * Get the ID of the question pool. + * + * @return ObjectId + */ + public function getObjectId(): ObjectId + { + return $this->pool_id; + } + + /** + * Get the object of the question pool. It will be loaded from the database if not already loaded. + */ + public function getObject(): ilObjQuestionPool + { + if ($this->pool_object === null) { + $this->pool_object = new ilObjQuestionPool($this->pool_id->toInt(), false); + $this->pool_object->read(); + } + + return $this->pool_object; + } + + /** + * Collect the question properties for all questions in the question pool. + * + * @return array + */ + public function getQuestionProperties(): array + { + if ($this->questions === null) { + $this->questions = $this->question_repository->getForParentObjectId($this->pool_id->toInt()); + } + return $this->questions; + } + + private function database(): ilDBInterface + { + return $this->db; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php similarity index 98% rename from components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php index 5fd121dee14e..b4d72eab3b86 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolExporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport; +namespace ILIAS\TestQuestionPool\ExportImport\Export; use assFormulaQuestion; use ilDBInterface; @@ -165,7 +165,7 @@ private function exportObject( ): void { $serializer->append('object', $transformations->normalize($collector->getObject())); - $obj_id = $collector->getPoolId()->toInt(); + $obj_id = $collector->getObjectId()->toInt(); $state->addDependency('components/ILIAS/ILIASObject', 'common', [$obj_id]); $state->addDependency('components/ILIAS/MetaData', 'qpl', ["{$obj_id}:0:qpl"]); diff --git a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php index 6ef905266324..95987154f8b0 100755 --- a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php +++ b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php @@ -24,7 +24,7 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\StateHolder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; -use ILIAS\TestQuestionPool\ExportImport\QuestionPoolExporter; +use ILIAS\TestQuestionPool\ExportImport\Export\QuestionPoolExporter; use ILIAS\TestQuestionPool\ExportImport\QuestionPoolImporter; use ILIAS\TestQuestionPool\ExportImport\SkillAssignmentsImporter; use Pimple\Container as PimpleContainer; From 67741cbaea312c50bb2a713815d64adffc6d9fda Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Mon, 20 Apr 2026 09:04:34 +0200 Subject: [PATCH 14/66] fix(test): missing parameter in Repository::getTestAttemptResult --- .../Test/src/Results/Data/Repository.php | 19 +++++++++++++++++-- .../Results/Data/TestResultRepositoryTest.php | 4 ++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/components/ILIAS/Test/src/Results/Data/Repository.php b/components/ILIAS/Test/src/Results/Data/Repository.php index 70c1499d41b4..f7168d7850d2 100644 --- a/components/ILIAS/Test/src/Results/Data/Repository.php +++ b/components/ILIAS/Test/src/Results/Data/Repository.php @@ -150,14 +150,29 @@ public function updateTestResultCache(int $active_id, ?\ilAssQuestionProcessLock return $result; } - public function getTestAttemptResult(int $active_id): ?AttemptResult + public function getTestAttemptResult(int $active_id, int $attempt): ?AttemptResult + { + $result = $this->db->queryF( + "SELECT * FROM tst_pass_result WHERE active_fi = %s AND pass = %s", + [\ilDBConstants::T_INTEGER, \ilDBConstants::T_INTEGER], + [$active_id, $attempt] + ); + return $this->toTestAttemptResult($this->db->fetchAssoc($result)); + } + + public function getTestAttemptResults(int $active_id): array { $result = $this->db->queryF( "SELECT * FROM tst_pass_result WHERE active_fi = %s", [\ilDBConstants::T_INTEGER], [$active_id] ); - return $this->toTestAttemptResult($this->db->fetchAssoc($result)); + + $results = []; + while ($row = $this->db->fetchAssoc($result)) { + $results[$row['pass']] = $this->toTestAttemptResult($row); + } + return $results; } public function updateTestAttemptResult( diff --git a/components/ILIAS/Test/tests/Results/Data/TestResultRepositoryTest.php b/components/ILIAS/Test/tests/Results/Data/TestResultRepositoryTest.php index ee733ac0754a..79e01a7588e5 100644 --- a/components/ILIAS/Test/tests/Results/Data/TestResultRepositoryTest.php +++ b/components/ILIAS/Test/tests/Results/Data/TestResultRepositoryTest.php @@ -163,7 +163,7 @@ public function testGetTestAttemptResult(array $query_result, array $expected): $this->mockGetTestPassResultQuery($query_result); $repository = $this->createInstance(); - $actual = $repository->getTestAttemptResult($query_result['active_fi']); + $actual = $repository->getTestAttemptResult($query_result['active_fi'], $query_result['pass']); $this->assertNotNull($actual); $this->assertInstanceOf(AttemptResult::class, $actual); @@ -177,7 +177,7 @@ public function testGetTestAttemptResultNotFound(): void $this->mockGetTestPassResultQuery(null); $repository = $this->createInstance(); - $actual = $repository->getTestAttemptResult(1000); + $actual = $repository->getTestAttemptResult(1000, 0); $this->assertNull($actual); } From a43edd8cfba6dd942ed6c48dcfa404974959ffad Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Tue, 21 Apr 2026 10:25:42 +0200 Subject: [PATCH 15/66] feat(test): introduce normalizing into test component --- .../ExportImport/Envelopes/ManualFeedback.php | 84 ++++++ .../src/ExportImport/Envelopes/Solution.php | 101 ++++++++ .../ExportImport/Envelopes/WorkingTime.php | 72 ++++++ .../Normalizer/AttemptResultNormalizer.php | 86 +++++++ .../Normalizer/ExportableNormalizer.php | 63 +++++ .../Normalizer/ParticipantNormalizer.php | 109 ++++++++ .../ParticipantResultNormalizer.php | 81 ++++++ .../Normalizer/ilObjTestNormalizer.php | 68 +++++ .../src/ExportImport/Pipes/CollectUserIds.php | 61 +++++ .../Test/src/ExportImport/TestCollector.php | 241 ++++++++++++++++++ .../Test/src/Participants/Participant.php | 5 + .../classes/class.assQuestion.php | 4 +- .../Normalizer/ResourceNormalizer.php | 147 +++++++++++ .../Normalizing/Pipes/CollectResources.php | 72 ++++++ 14 files changed, 1192 insertions(+), 2 deletions(-) create mode 100644 components/ILIAS/Test/src/ExportImport/Envelopes/ManualFeedback.php create mode 100644 components/ILIAS/Test/src/ExportImport/Envelopes/Solution.php create mode 100644 components/ILIAS/Test/src/ExportImport/Envelopes/WorkingTime.php create mode 100644 components/ILIAS/Test/src/ExportImport/Normalizer/AttemptResultNormalizer.php create mode 100644 components/ILIAS/Test/src/ExportImport/Normalizer/ExportableNormalizer.php create mode 100644 components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php create mode 100644 components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantResultNormalizer.php create mode 100644 components/ILIAS/Test/src/ExportImport/Normalizer/ilObjTestNormalizer.php create mode 100644 components/ILIAS/Test/src/ExportImport/Pipes/CollectUserIds.php create mode 100644 components/ILIAS/Test/src/ExportImport/TestCollector.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/ManualFeedback.php b/components/ILIAS/Test/src/ExportImport/Envelopes/ManualFeedback.php new file mode 100644 index 000000000000..5531a916bd81 --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Envelopes/ManualFeedback.php @@ -0,0 +1,84 @@ + $tt->normalize($this->active_id), + 'question_id' => $tt->normalize($this->question_id), + 'attempt' => $this->attempt, + 'feedback' => $this->feedback, + 'finalized_evaluation' => $this->finalized_evaluation, + 'finalized_timestamp' => $this->finalized_timestamp, + 'finalized_by' => $tt->normalize($this->finalized_by), + ]; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $value, Transformations $tt): static + { + return new self( + $tt->denormalize($value['active_id'], Id::class)->getId(), + $tt->denormalize($value['question_id'], Id::class)->getId(), + $tt->int($value['attempt']), + $tt->string($value['feedback']), + $tt->bool($value['finalized_evaluation']), + $tt->int($value['finalized_timestamp']), + $tt->denormalize($value['finalized_by'], Id::class)->getId(), + ); + } + + public static function fromRow(array $row): static + { + return new self( + new Id($row['active_fi'], 'participant'), + new Id($row['question_fi'], 'question'), + (int) $row['pass'], + $row['feedback'], + (bool) $row['finalized_evaluation'], + (int) $row['finalized_tstamp'], + new Id($row['finalized_by_usr_id'], 'user'), + ); + } +} diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/Solution.php b/components/ILIAS/Test/src/ExportImport/Envelopes/Solution.php new file mode 100644 index 000000000000..2bdc8d75ba32 --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Envelopes/Solution.php @@ -0,0 +1,101 @@ + $tt->normalize($this->active_id), + 'question_id' => $tt->normalize($this->question_id), + 'attempt' => $this->attempt, + 'points' => $this->points, + 'timestamp' => $this->timestamp, + 'value1' => $tt->normalize($this->value1), + 'value2' => $this->value2, + 'step' => $this->step, + 'authorized' => $this->authorized + ]; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $value, Transformations $tt): static + { + return new self( + $tt->denormalize($value['active_id'], Id::class)->getId(), + $tt->denormalize($value['question_id'], Id::class)->getId(), + $tt->int($value['attempt']), + $tt->nullableFloat($value['points']), + $tt->int($value['timestamp']), + ResourceNormalizer::isResourceIdentification($value['value1']) + ? $tt->denormalize($value['value1'], ResourceIdentification::class) + : $tt->nullableString($value['value1']), + $tt->nullableString($value['value2']), + $tt->nullableInt($value['step']), + $tt->bool($value['authorized']), + ); + } + + public static function fromRow(array $row): static + { + $value1 = $row['value1']; + if ($row['value2'] === 'rid' && is_string($value1)) { + $value1 = new ResourceIdentification($value1); + } + + return new self( + new Id($row['active_fi'], 'participant'), + new Id($row['question_fi'], 'question'), + (int) $row['pass'], + $row['points'] ? (float) $row['points'] : null, + (int) $row['tstamp'], + $value1, + $row['value2'], + $row['step'] ? (int) $row['step'] : null, + (bool) $row['authorized'] + ); + } +} diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/WorkingTime.php b/components/ILIAS/Test/src/ExportImport/Envelopes/WorkingTime.php new file mode 100644 index 000000000000..17deb3dabbd3 --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Envelopes/WorkingTime.php @@ -0,0 +1,72 @@ + $tt->normalize($this->active_id), + 'attempt' => $this->attempt, + 'started' => $this->started, + 'finished' => $this->finished, + ]; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $value, Transformations $tt): static + { + return new self( + $tt->denormalize($value['active_id'], Id::class)->getId(), + $tt->int($value['attempt']), + $tt->string($value['started']), + $tt->string($value['finished']), + ); + } + + public static function fromRow(array $row): static + { + return new self( + new Id($row['active_fi'], 'participant'), + (int) $row['pass'], + $row['started'], + $row['finished'], + ); + } +} diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/AttemptResultNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/AttemptResultNormalizer.php new file mode 100644 index 000000000000..15a1a4349b90 --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/AttemptResultNormalizer.php @@ -0,0 +1,86 @@ + + */ +#[Normalizes(AttemptResult::class)] +class AttemptResultNormalizer implements Normalizer +{ + public function __construct( + private readonly Transformations $tt, + ) { + } + + /** + * @inheritDoc + */ + public function normalize($value): array|float|bool|int|string|null + { + if (!$value instanceof AttemptResult) { + throw new NormalizingException('Invalid value', $value); + } + + return [ + 'active_id' => $this->tt->normalize(new Id($value->getActiveId(), 'participant')), + 'attempt' => $value->getAttempt(), + 'max_points' => $value->getMaxPoints(), + 'reached_points' => $value->getReachedPoints(), + 'question_count' => $value->getQuestionCount(), + 'answered_questions' => $value->getAnsweredQuestions(), + 'working_time' => $value->getWorkingTime(), + 'timestamp' => $value->getTimestamp(), + 'exam_id' => $value->getExamId(), + 'finalized_by' => $value->getFinalizedBy(), + ]; + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $value, string $type): AttemptResult + { + if ($type !== AttemptResult::class) { + throw new NormalizingException("Invalid type for AttemptResult: {$type}"); + } + + return new AttemptResult( + $this->tt->denormalize($value['active_id'], Id::class)->getId(), + $this->tt->int($value['attempt']), + $this->tt->float($value['max_points']), + $this->tt->float($value['reached_points']), + $this->tt->int($value['question_count']), + $this->tt->int($value['answered_questions']), + $this->tt->int($value['working_time']), + $this->tt->int($value['timestamp']), + $this->tt->string($value['exam_id']), + $this->tt->string($value['finalized_by']), + ); + } +} diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ExportableNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/ExportableNormalizer.php new file mode 100644 index 000000000000..1490132728dd --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/ExportableNormalizer.php @@ -0,0 +1,63 @@ + + */ +#[Normalizes(Exportable::class)] +class ExportableNormalizer implements Normalizer +{ + public function __construct( + private readonly Transformations $tt, + ) { + } + + /** + * @inheritDoc + */ + public function normalize($value): array|float|bool|int|string|null + { + if (!$value instanceof Exportable) { + throw new NormalizingException('Invalid exportable value', $value); + } + + return $value->toExport(); + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $value, string $type): Exportable + { + if (!in_array(Exportable::class, class_implements($type))) { + throw new NormalizingException('Invalid exportable type', $type); + } + + return $type::fromExport($value); + } +} diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php new file mode 100644 index 000000000000..27d6dc082585 --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php @@ -0,0 +1,109 @@ + + */ +#[Normalizes(Participant::class)] +class ParticipantNormalizer implements Normalizer +{ + public function __construct( + private readonly Transformations $tt, + ) { + } + + /** + * @inheritDoc + */ + public function normalize($value): array|float|bool|int|string|null + { + if (!$value instanceof Participant) { + throw new NormalizingException('Invalid value', $value); + } + + return [ + 'user_id' => $this->tt->normalize(new Id($value->getUserId(), 'user')), + 'active_id' => $this->tt->normalize(new Id($value->getActiveId(), 'participant')), + 'test_id' => $this->tt->normalize(new Id($value->getTestId(), 'test')), + 'anonymous_id' => $value->getAnonymousId(), + 'firstname' => $value->getFirstname(), + 'lastname' => $value->getLastname(), + 'login' => $value->getLogin(), + 'importname' => $value->getImportname(), + 'matriculation' => $value->getMatriculation(), + 'extra_time' => $value->getExtraTime(), + 'attempts' => $value->getAttempts(), + 'client_ip_from' => $value->getClientIpFrom(), + 'client_ip_to' => $value->getClientIpTo(), + 'invitation_date' => $value->getInvitationDate(), + 'submitted' => $value->getSubmitted(), + 'last_started_attempt' => $value->getLastStartedAttempt(), + 'last_finished_attempt' => $value->getLastFinishedAttempt(), + 'unfinished_attempts' => $value->hasUnfinishedAttempts(), + 'first_access' => $this->tt->normalize($value->getFirstAccess()), + 'last_access' => $this->tt->normalize($value->getLastAccess()), + 'scoring_finalized' => $value->isScoringFinalized(), + ]; + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $value, string $type): Participant + { + if ($type !== Participant::class) { + throw new NormalizingException("Invalid type for Participant: {$type}"); + } + + return new Participant( + $this->tt->denormalize($value['user_id'], Id::class)->getId(), + $this->tt->denormalize($value['active_id'], Id::class)->getId(), + $this->tt->denormalize($value['test_id'], Id::class)->getId(), + $this->tt->nullableString($value['anonymous_id']), + $this->tt->string($value['firstname']), + $this->tt->string($value['lastname']), + $this->tt->string($value['login']), + $this->tt->nullableString($value['importname']), + $this->tt->string($value['matriculation']), + $this->tt->int($value['extra_time']), + $this->tt->int($value['attempts']), + $this->tt->nullableString($value['client_ip_from']), + $this->tt->nullableString($value['client_ip_to']), + $this->tt->nullableInt($value['invitation_date']), + $this->tt->nullableBool($value['submitted']), + $this->tt->nullableInt($value['last_started_attempt']), + $this->tt->nullableInt($value['last_finished_attempt']), + $this->tt->bool($value['unfinished_attempts']), + $this->tt->denormalize($value['first_access'], DateTimeImmutable::class), + $this->tt->denormalize($value['last_access'], DateTimeImmutable::class), + $this->tt->bool($value['scoring_finalized']), + ); + } +} diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantResultNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantResultNormalizer.php new file mode 100644 index 000000000000..b31df4ca9e0a --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantResultNormalizer.php @@ -0,0 +1,81 @@ + + */ +#[Normalizes(ParticipantResult::class)] +class ParticipantResultNormalizer implements Normalizer +{ + public function __construct( + private readonly Transformations $tt, + ) { + } + + /** + * @inheritDoc + */ + public function normalize($value): array|float|bool|int|string|null + { + if (!$value instanceof ParticipantResult) { + throw new NormalizingException('Invalid value', $value); + } + + return [ + 'active_id' => $this->tt->normalize(new Id($value->getActiveId(), 'participant')), + 'attempt' => $value->getAttempt(), + 'max_points' => $value->getMaxPoints(), + 'reached_points' => $value->getReachedPoints(), + 'mark' => $this->tt->normalize($value->getMark()), + 'timestamp' => $value->getTimestamp(), + 'passed_once' => $value->isPassedOnce(), + ]; + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $value, string $type): ParticipantResult + { + if ($type !== ParticipantResult::class) { + throw new NormalizingException("Invalid type for ParticipantResult: {$type}"); + } + + return new ParticipantResult( + $this->tt->denormalize($value['active_id'], Id::class)->getId(), + $this->tt->int($value['attempt']), + $this->tt->float($value['max_points']), + $this->tt->float($value['reached_points']), + $this->tt->denormalize($value['mark'], Mark::class), + $this->tt->int($value['timestamp']), + $this->tt->bool($value['passed_once']), + ); + } +} diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ilObjTestNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/ilObjTestNormalizer.php new file mode 100644 index 000000000000..083dd5c228fa --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/ilObjTestNormalizer.php @@ -0,0 +1,68 @@ + + */ +#[Normalizes(ilObjTest::class)] +class ilObjTestNormalizer extends IlObjectNormalizer implements Normalizer +{ + /** + * @inheritDoc + */ + public function normalize($value): array|float|bool|int|string|null + { + if (!$value instanceof ilObjTest) { + throw new NormalizingException('Invalid value', $value); + } + + $normalized = parent::normalize($value); + $normalized['test_id'] = $this->tt->normalize(new Id($value->getTestId(), 'test')); + + return $normalized; + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $value, string $type): ilObjTest + { + if ($type !== ilObjTest::class) { + throw new NormalizingException("Invalid type for ilObjTest: {$type}"); + } + + /** @var ilObjTest $object */ + $object = parent::denormalize($value, ilObjTest::class); + $object->setTestId( + $this->tt->denormalize($value['test_id'], Id::class)->getId() + ); + + return $object; + } +} diff --git a/components/ILIAS/Test/src/ExportImport/Pipes/CollectUserIds.php b/components/ILIAS/Test/src/ExportImport/Pipes/CollectUserIds.php new file mode 100644 index 000000000000..5f727a3fd49e --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Pipes/CollectUserIds.php @@ -0,0 +1,61 @@ + $ids + */ + private array $ids = []; + + /** + * @inheritDoc + */ + public function handle(mixed $passable, \Closure $next): mixed + { + if ($passable instanceof NormalizeCarry && $passable->value instanceof Id) { + if ($passable->value->getObject() === 'user') { + $this->ids[$passable->value->getId()] = true; + } + } + + return $next($passable); + } + + /** + * Get all user IDs collected during normalization. + * + * @return list + */ + public function getIds(): array + { + return array_keys($this->ids); + } +} diff --git a/components/ILIAS/Test/src/ExportImport/TestCollector.php b/components/ILIAS/Test/src/ExportImport/TestCollector.php new file mode 100644 index 000000000000..dc80c6012540 --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/TestCollector.php @@ -0,0 +1,241 @@ + $questions */ + private ?array $questions = null; + private ?ilObjTest $test = null; + private ?array $participants = null; + + + public function __construct( + private readonly ParticipantRepository $participant_repository, + private readonly ResultsRepository $results_repository, + private readonly QuestionsRepository $questions_repository, + private readonly ilDBInterface $db, + private readonly ObjectId $object_id + ) { + } + + private function database(): ilDBInterface + { + return $this->db; + } + + public function getObjectId(): ObjectId + { + return $this->object_id; + } + + public function getTestId(): int + { + return $this->getObject()->getTestId(); + } + + public function getObject(): ilObjTest + { + if ($this->test === null) { + $this->test = new ilObjTest($this->object_id->toInt(), false); + } + + return $this->test; + } + + public function getSettings(): array + { + return [ + 'main' => $this->test->getMainSettings(), + 'scoring' => $this->test->getScoreSettings(), + 'marks' => $this->test->getMarkSchema(), + ]; + } + + /** + * Create a mapping of user IDs to the user identifier field specified in the test object's global settings. + * + * @param list $user_ids + * @return array{identifier: string, mapping: array} + */ + public function getUserMapping(array $user_ids): array + { + $export_identifier = $this->getObject()->getGlobalSettings()->getUserIdentifier(); + + $mapping = []; + if ($export_identifier === UserIdentifiers::USER_ID) { + foreach ($user_ids as $user_id) { + $mapping[$user_id] = $user_id; + } + } else { + $in_clause = $this->db->in('usr_id', $user_ids, false, 'integer'); + $query = $this->db->query("SELECT usr_id, {$export_identifier->value} FROM usr_data WHERE {$in_clause}"); + + foreach ($this->db->fetchAll($query) as $row) { + $mapping[$row['usr_id']] = $row[$export_identifier->value]; + } + }; + + return [ + 'identifier' => $export_identifier->value, + 'mapping' => $mapping, + ]; + } + + /* + Questions + */ + + /** + * @inheritDoc + */ + public function getQuestionProperties(): array + { + return array_map( + fn(Properties $property) => $property->getGeneralQuestionProperties(), + $this->getTestQuestionProperties() + ); + } + + /** + * @return array + */ + public function getTestQuestionProperties(): array + { + if ($this->questions === null) { + $this->questions = $this->questions_repository->getQuestionPropertiesForTest($this->getObject()); + } + return $this->questions; + } + + /* + Participants + */ + + /** + * @return Generator + */ + public function getParticipants(): Generator + { + return $this->participant_repository->getParticipants($this->getTestId()); + } + + /** + * @return list + */ + public function getParticipantsIds(): array + { + if ($this->participants === null) { + $this->participants = []; + foreach ($this->getParticipants() as $participant) { + $this->participants[] = $participant->getActiveId(); + } + } + return $this->participants; + } + + /* + Results + */ + + public function getResults(int $participant_id): array + { + return [ + 'results' => $this->results_repository->getTestResult($participant_id), + 'attempts' => $this->results_repository->getTestAttemptResults($participant_id), + 'solutions' => $this->getSolutions($participant_id), + 'working_times' => $this->getWorkingTimes($participant_id), + 'manual_feedback' => $this->getManualFeedback($participant_id), + ]; + } + + /** + * @return list + */ + public function getSolutions(int $participant_id): array + { + $query = $this->db->queryF( + "SELECT * FROM tst_solutions WHERE active_fi = %s", + [ilDBConstants::T_INTEGER], + [$participant_id] + ); + + return array_map( + fn(array $row): Solution => Solution::fromRow($row), + $this->db->fetchAll($query) + ); + } + + /** + * @return list + */ + public function getWorkingTimes(int $participant_id): array + { + $query = $this->db->queryF( + "SELECT * FROM tst_times WHERE active_fi = %s", + [ilDBConstants::T_INTEGER], + [$participant_id] + ); + + return array_map( + fn(array $row): WorkingTime => WorkingTime::fromRow($row), + $this->db->fetchAll($query) + ); + } + + /** + * @return list + */ + public function getManualFeedback(int $participant_id): array + { + $query = $this->db->queryF( + "SELECT * FROM tst_manual_fb WHERE active_fi = %s", + [ilDBConstants::T_INTEGER], + [$participant_id] + ); + + return array_map( + fn(array $row): ManualFeedback => ManualFeedback::fromRow($row), + $this->db->fetchAll($query) + ); + } +} diff --git a/components/ILIAS/Test/src/Participants/Participant.php b/components/ILIAS/Test/src/Participants/Participant.php index 1f464c83a92f..cce4de3f3ec2 100644 --- a/components/ILIAS/Test/src/Participants/Participant.php +++ b/components/ILIAS/Test/src/Participants/Participant.php @@ -145,6 +145,11 @@ public function withClientIpTo(?string $ip): self return $clone; } + public function getInvitationDate(): ?int + { + return $this->invitation_date; + } + public function isInvitedParticipant(): bool { return $this->invitation_date > 0; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php index 08101ca675f9..0abc3787d1b8 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php @@ -2970,7 +2970,7 @@ public function toNormalized(Transformations $tt): Transformation { return $tt->custom()->transformation(fn(): array => [ 'id' => $tt->normalize(new Id($this->id, 'question')), - 'pool_id' => $tt->normalize(new Id($this->obj_id, 'qpl')), + 'parent_id' => $tt->normalize(new Id($this->obj_id, 'object')), 'original_id' => $this->original_id, 'external_id' => $this->external_id, 'type' => $this->getQuestionType(), @@ -2998,7 +2998,7 @@ public function fromNormalized(Transformations $tt): Transformation return $tt->custom()->transformation(function (array $normalized) use ($tt): self { $clone = clone $this; $clone->id = $tt->denormalize($normalized['id'], Id::class)->getId(); - $clone->obj_id = $tt->denormalize($normalized['pool_id'], Id::class)->getId(); + $clone->obj_id = $tt->denormalize($normalized['parent_id'], Id::class)->getId(); $clone->original_id = $tt->nullableInt($normalized['original_id']); $clone->external_id = $tt->nullableString($normalized['external_id']); $clone->owner = $tt->int($normalized['owner']); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php new file mode 100644 index 000000000000..64e7e3dc2c89 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php @@ -0,0 +1,147 @@ + + */ +#[Normalizes(ResourceIdentification::class, StorableResource::class)] +class ResourceNormalizer implements Normalizer +{ + private const string KEY_TYPE = '_type'; + private const string TYPE_RID = 'rid'; + private const string TYPE_RESOURCE = 'resource'; + + private readonly ResourceRepository $resource_repository; + + public function __construct( + private readonly Transformations $tt, + Container $dic + ) { + $this->resource_repository = $dic[InitResourceStorage::D_REPOSITORIES]->getResourceRepository(); + } + + /** + * @inheritDoc + */ + public function normalize($value): array|float|bool|int|string|null + { + if ($value instanceof ResourceIdentification) { + return $this->normalizeIdentification($value); + } + + if ($value instanceof StorableResource) { + return $this->normalizeResource($value); + } + + throw new NormalizingException('Invalid value', $value); + } + + private function normalizeIdentification(ResourceIdentification $rid): array + { + return [ + self::KEY_TYPE => self::TYPE_RID, + 'id' => $rid->serialize(), + ]; + } + + private function normalizeResource(StorableResource $resource): array + { + return [ + self::KEY_TYPE => self::TYPE_RESOURCE, + 'resource_type' => $resource->getType()->value, + 'id' => $resource->getIdentification()->serialize(), + 'revision' => $resource->getCurrentRevision()->getVersionNumber(), + 'title' => $resource->getCurrentRevision()->getTitle(), + 'mime_type' => $resource->getCurrentRevision()->getInformation()->getMimeType(), + 'suffix' => $resource->getCurrentRevision()->getInformation()->getSuffix(), + 'creation_date' => $this->tt->normalize($resource->getCurrentRevision()->getInformation()->getCreationDate()), + ]; + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $value, string $type): ResourceIdentification|StorableResource + { + if ($type === ResourceIdentification::class) { + return $this->denormalizeIdentification($value); + } + + if ($type === StorableResource::class) { + $this->denormalizeResource($value); + } + + throw new NormalizingException('Invalid type', $type); + } + + private function denormalizeIdentification(array $value): ResourceIdentification + { + if (!self::isResourceIdentification($value)) { + throw new NormalizingException('Invalid resource identification', $value); + } + + return new ResourceIdentification($value['id']); + } + + private function denormalizeResource(array $value): StorableResource + { + if (!self::isStorableResource($value)) { + throw new NormalizingException('Invalid storable resource', $value); + } + + $id = new ResourceIdentification($value['id']); + $type = ResourceType::from($value['resource_type']); + + return $this->resource_repository->blank($id, $type); + } + + /** + * Returns true if the value is a normalized resource identification. + */ + public static function isResourceIdentification(mixed $value): bool + { + return is_array($value) + && isset($value[self::KEY_TYPE]) + && $value[self::KEY_TYPE] === self::TYPE_RID; + } + + /** + * Returns true if the value is a normalized storable resource. + */ + public static function isStorableResource(mixed $value): bool + { + return is_array($value) + && isset($value[self::KEY_TYPE]) + && $value[self::KEY_TYPE] === self::TYPE_RESOURCE; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php new file mode 100644 index 000000000000..ea9c3b4b4c8b --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php @@ -0,0 +1,72 @@ + $resources + */ + private array $resources = []; + + public function __construct( + private readonly IRSS $irss + ) { + } + + /** + * @inheritDoc + */ + public function handle(mixed $passable, \Closure $next): mixed + { + if ($passable instanceof NormalizeCarry && $passable->value instanceof ResourceIdentification) { + $this->handleNormalization($passable->value); + } + + return $next($passable); + } + + private function handleNormalization(ResourceIdentification $rid): void + { + $resource = $this->irss->manage()->getResource($rid); + + $this->resources[$rid->serialize()] = $resource; + } + + /** + * Get all resources collected during normalization. + * + * @return array + */ + public function getResources(): array + { + return $this->resources; + } +} From 97c94eee44bdeffba3fa93cf1fa3600cc8b93dc0 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Tue, 21 Apr 2026 12:47:47 +0200 Subject: [PATCH 16/66] feat(test): introduce test export classes and integrate into existing structure --- .../Test/classes/class.ilTestExportGUI.php | 58 --- .../class.ilTestExportOptionXMLRES.php | 152 -------- .../Test/classes/class.ilTestExporter.php | 182 ++-------- .../ExportOptions/XMLWithResultsOption.php | 94 +++++ .../Test/src/ExportImport/TestExporter.php | 343 ++++++++++++++++++ components/ILIAS/Test/src/TestDIC.php | 24 ++ .../Foundation/Bridge/ExportState.php | 8 +- .../Foundation/Bridge/StateHolder.php | 9 +- .../Foundation/Bridge/XmlExporterBridge.php | 6 +- 9 files changed, 513 insertions(+), 363 deletions(-) delete mode 100644 components/ILIAS/Test/classes/class.ilTestExportOptionXMLRES.php create mode 100644 components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php create mode 100644 components/ILIAS/Test/src/ExportImport/TestExporter.php diff --git a/components/ILIAS/Test/classes/class.ilTestExportGUI.php b/components/ILIAS/Test/classes/class.ilTestExportGUI.php index f01433516c0d..c2d5cb6058b9 100755 --- a/components/ILIAS/Test/classes/class.ilTestExportGUI.php +++ b/components/ILIAS/Test/classes/class.ilTestExportGUI.php @@ -20,13 +20,11 @@ use ILIAS\Test\Scoring\Manual\TestScoring; use ILIAS\Test\ExportImport\DBRepository; -use ILIAS\Test\ExportImport\ResultsExportStakeholder; use ILIAS\Test\Results\Data\Repository as TestResultsRepository; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Renderer as UIRenderer; use ILIAS\ResourceStorage\Services as IRSS; use ILIAS\Filesystem\Filesystem; -use ILIAS\Test\ExportImport\Types as ExportImportTypes; use Psr\Http\Message\ServerRequestInterface; /** @@ -56,62 +54,6 @@ public function __construct( parent::__construct($parent_gui, null); } - /** - * Create test export file - */ - public function createTestExportWithResults() - { - $this->ctrl->setParameterByClass(self::class, 'export_results', 1); - $manager = $this->export_handler->manager()->handler(); - $export_info = $manager->getExportInfoWithObject( - $this->obj, - time(), - $this->export_handler->consumer()->exportConfig()->allExportConfigs() - ); - $element = $manager->createExport( - $this->il_user->getId(), - $export_info, - '' - ); - - $file_name = $element->getIRSSInfo()->getFileName(); - $this->temp_file_system->writeStream( - $file_name, - $this->irss->consume()->stream($element->getIRSSInfo()->getResourceId())->getStream() - ); - $temp_stream = $this->temp_file_system->readStream($file_name); - $rid = $this->irss->manage()->stream( - $temp_stream, - new ResultsExportStakeholder(), - $element->getIRSSInfo()->getFileName() - ); - - $temp_stream->close(); - - $this->temp_file_system->delete($file_name); - - $this->export_repository->store( - $this->obj->getId(), - ExportImportTypes::XML_WITH_RESULTS, - $rid - ); - $this->export_options->getById('expxml')->onDeleteFiles( - $this->context, - $this->export_handler->consumer()->file()->identifier()->collection()->withElement( - $this->export_handler->consumer()->file()->identifier()->handler()->withIdentifier( - $element->getIRSSInfo()->getResourceIdSerialized() - ) - ) - ); - - $this->tpl->setOnScreenMessage( - ilGlobalTemplateInterface::MESSAGE_TYPE_SUCCESS, - $this->lng->txt("exp_file_created"), - true - ); - $this->ctrl->redirect($this, self::CMD_LIST_EXPORT_FILES); - } - public function createTestArchiveExport() { if ($this->access->checkAccess('write', '', $this->obj->getRefId())) { diff --git a/components/ILIAS/Test/classes/class.ilTestExportOptionXMLRES.php b/components/ILIAS/Test/classes/class.ilTestExportOptionXMLRES.php deleted file mode 100644 index 154460c6622a..000000000000 --- a/components/ILIAS/Test/classes/class.ilTestExportOptionXMLRES.php +++ /dev/null @@ -1,152 +0,0 @@ -lng = $DIC['lng']; - $this->irss = $DIC['resource_storage']; - $this->data_factory = new DataFactory(); - $this->repository = TestDIC::dic()['exportimport.repository']; - } - - public function getExportType(): string - { - return 'ZIP Results'; - } - - public function getExportOptionId(): string - { - return self::OPTIONS_ID; - } - - public function getSupportedRepositoryObjectTypes(): array - { - return ['tst']; - } - - public function getLabel(): string - { - $this->lng->loadLanguageModule('exp'); - $this->lng->loadLanguageModule('assessment'); - return $this->lng->txt("exp_format_dropdown-xml") . " (" . $this->lng->txt('ass_create_export_file_with_results') . ")"; - } - - public function onDeleteFiles( - ilExportHandlerConsumerContextInterface $context, - ilExportHandlerConsumerFileIdentifierCollectionInterface $file_identifiers - ): void { - $object_id = new ObjectId($context->exportObject()->getId()); - foreach ($file_identifiers as $file_identifier) { - $rid = $this->irss->manage()->find($file_identifier->getIdentifier()); - $this->repository->delete($rid); - $this->irss->manage()->remove($rid, new ResultsExportStakeholder()); - } - } - - public function onDownloadFiles( - ilExportHandlerConsumerContextInterface $context, - ilExportHandlerConsumerFileIdentifierCollectionInterface $file_identifiers - ): void { - $object_id = new ObjectId($context->exportObject()->getId()); - foreach ($file_identifiers as $file_identifier) { - $this->irss->consume()->download( - $this->irss->manage()->find($file_identifier->getIdentifier()) - )->run(); - } - } - - public function onDownloadWithLink( - ReferenceId $reference_id, - ilExportHandlerConsumerFileIdentifierInterface $file_identifier - ): void { - $this->irss->consume()->download($reference_id)->run(); - } - - public function getFiles( - ilExportHandlerConsumerContextInterface $context - ): ilExportHandlerFileInfoCollectionInterface { - return $this->buildElements( - $context, - $this->data_factory->objId($context->exportObject()->getId()) - ); - } - - public function getFileSelection( - ilExportHandlerConsumerContextInterface $context, - ilExportHandlerConsumerFileIdentifierCollectionInterface $file_identifiers - ): ilExportHandlerFileInfoCollectionInterface { - return $this->buildElements( - $context, - $this->data_factory->objId($context->exportObject()->getId()), - $file_identifiers->toStringArray() - ); - } - - public function onExportOptionSelected( - ilExportHandlerConsumerContextInterface $context - ): void { - $context->exportGUIObject()->createTestExportWithResults(); - } - - protected function buildElements( - ilExportHandlerConsumerContextInterface $context, - ObjectId $object_id, - ?array $file_identifiers = null - ): ilExportHandlerFileInfoCollectionInterface { - if ($file_identifiers === null) { - $file_identifiers = array_map( - static fn(array $v): string => $v['rid'], - $this->repository->getFor($object_id->toInt()) - ); - } - $collection_builder = $context->fileCollectionBuilder(); - foreach ($file_identifiers as $file_identifier) { - $collection_builder = $collection_builder->withResourceIdentifier( - $this->irss->manage()->find($file_identifier), - $object_id, - $this - ); - } - return $collection_builder->collection(); - } -} diff --git a/components/ILIAS/Test/classes/class.ilTestExporter.php b/components/ILIAS/Test/classes/class.ilTestExporter.php index 9244605526e9..85e741c91093 100755 --- a/components/ILIAS/Test/classes/class.ilTestExporter.php +++ b/components/ILIAS/Test/classes/class.ilTestExporter.php @@ -18,182 +18,70 @@ declare(strict_types=1); +use ILIAS\Export\ExportHandler\Factory as ExportHandler; +use ILIAS\Test\ExportImport\Types; use ILIAS\Test\TestDIC; -use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; -use ILIAS\Test\Logging\TestLogger; -use ILIAS\Test\ExportImport\Factory as ExportImportFactory; -use ILIAS\Test\ExportImport\Types as ExportImportTypes; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\XmlExporterBridge; -/** - * Used for container export with tests - * - * @author Stefan Meyer - * @version $Id$ - * @ingroup components\ILIASTest - */ class ilTestExporter extends ilXmlExporter { - private readonly ilLanguage $lng; - private readonly ExportImportFactory $export_factory; - private readonly TestLogger $logger; - private readonly ilTree $tree; - private readonly ilCtrl $ctrl; - private readonly ilComponentRepository $component_repository; - private readonly GeneralQuestionPropertiesRepository $questionrepository; + use XmlExporterBridge; - public function __construct() + public function init(): void { - global $DIC; - $this->lng = $DIC['lng']; $local_dic = TestDIC::dic(); - $this->export_factory = $local_dic['exportimport.factory']; - $this->logger = $local_dic['logging.logger']; - $this->questionrepository = $local_dic['question.general_properties.repository']; - $this->tree = $DIC['tree']; - $this->ctrl = $DIC['ilCtrl']; - $this->component_repository = $DIC['component.repository']; - parent::__construct(); + $this->export_handler = new ExportHandler(); + $this->state_holder = $local_dic['exportimport.state_holder']; + $this->exporter = $local_dic['exportimport.exporter']; + $this->logger = $local_dic['logging.logger']; } /** - * Initialisation + * Returns the final XML content for the test. + * + * This method is called after `getXmlExportTailDependencies()`. At this point the export writer and export + * directory are available, so the preprocessed export can be written to disk and returned as xml. */ - public function init(): void - { - } - public function getXmlRepresentation(string $a_entity, string $a_schema_version, string $id): string { - $parameters = $this->ctrl->getParameterArrayByClass(ilTestExportGUI::class); - $export_type = ExportImportTypes::XML; - if (!empty($parameters['export_results'])) { - $export_type = ExportImportTypes::XML_WITH_RESULTS; - $this->ctrl->clearParameterByClass(ilTestExportGUI::class, 'export_results'); + if ($a_entity !== 'tst') { + throw new InvalidArgumentException("Invalid entity for test export: {$a_entity}"); } - $tst = new ilObjTest((int) $id, false); - $tst->read(); - $zip = $this->export_factory->getExporter($tst, $export_type) - ->withExportDirInfo($this->getAbsoluteExportDirectory()) - ->write(); - $this->logger->info(__METHOD__ . ': Created zip file ' . $zip); - return ''; - } - - public function getXmlExportHeadDependencies(string $entity, string $target_release, array $ids): array - { - if ($entity === 'tst') { - $mobs = []; - $files = []; - foreach ($ids as $id) { - $tst = new ilObjTest((int) $id, false); - $tst->read(); - - $intro_page_id = $tst->getMainSettings()->getIntroductionSettings()->getIntroductionPageId(); - if ($intro_page_id !== null) { - $mobs = array_merge($mobs, ilObjMediaObject::_getMobsOfObject('tst:pg', $intro_page_id)); - $files = array_merge($files, ilObjFile::_getFilesOfObject('tst:pg', $intro_page_id)); - } - - $concluding_remarks_page_id = $tst->getMainSettings()->getFinishingSettings()->getConcludingRemarksPageId(); - if ($concluding_remarks_page_id !== null) { - $mobs = array_merge($mobs, ilObjMediaObject::_getMobsOfObject('tst:pg', $concluding_remarks_page_id)); - $files = array_merge($files, ilObjFile::_getFilesOfObject('tst:pg', $concluding_remarks_page_id)); - } - } - - return [ - [ - 'component' => 'components/ILIAS/MediaObjects', - 'entity' => 'mob', - 'ids' => $mobs - ], - [ - 'component' => 'components/ILIAS/File', - 'entity' => 'file', - 'ids' => $files - ] - ]; - } - - return parent::getXmlExportTailDependencies($entity, $target_release, $ids); + return $this->finalizeExport()->getContent(); } /** - * @param array ids - * @return array array of array with keys 'component', 'entity', 'ids' + * Collects export tail dependencies for the test. + * + * The export framework calls this method before `getXmlRepresentation()`. Therefore this method only prepares and + * processes the export in memory using the export state. The export state is created if it does not exist yet. */ public function getXmlExportTailDependencies(string $a_entity, string $a_target_release, array $a_ids): array { - if ($a_entity == 'tst') { - $deps = []; - - $tax_ids = $this->getDependingTaxonomyIds($a_ids); - - if (count($tax_ids)) { - $deps[] = [ - 'component' => 'components/ILIAS/Taxonomy', - 'entity' => 'tax', - 'ids' => $tax_ids - ]; - } - - $deps[] = [ - 'component' => 'components/ILIAS/ILIASObject', - 'entity' => 'common', - 'ids' => $a_ids - ]; - - - $md_ids = []; - foreach ($a_ids as $id) { - $md_ids[] = $id . ':0:tst'; - } - if ($md_ids !== []) { - $deps[] = [ - 'component' => 'components/ILIAS/MetaData', - 'entity' => 'md', - 'ids' => $md_ids - ]; - } - - $deps[] = [ - "component" => "components/ILIAS/Tracking", - "entity" => "lpsettings", - "ids" => $a_ids - ]; - - return $deps; + if ($a_entity !== 'tst') { + throw new InvalidArgumentException("Invalid entity for test export: {$a_entity}"); } - return parent::getXmlExportTailDependencies($a_entity, $a_target_release, $a_ids); - } - - /** - * @param array $testObjIds - * @return array $taxIds - */ - private function getDependingTaxonomyIds(array $test_obj_ids): array - { - $tax_ids = []; - - foreach ($test_obj_ids as $test_obj_id) { - foreach (ilObjTaxonomy::getUsageOfObject($test_obj_id) as $tax_id) { - $tax_ids[$tax_id] = $tax_id; - } + // If the default export option was used, the state is not initialized yet. + if ($this->state_holder->exists() === false) { + $this->initExportState( + 'components/ILIAS/Test', + $a_target_release, + $a_entity, + $a_ids, + Types::XML->value + ); } - return $tax_ids; + return $this->processExport()->getDependencies(); } /** - * Returns schema versions that the component can export to. - * ILIAS chooses the first one, that has min/max constraints which - * fit to the target release. Please put the newest on top. - * @param string $a_entity - * @return array - */ + * Returns schema versions that the component can export to. ILIAS chooses the first one, that has min/max + * constraints which fit to the target release. + */ public function getValidSchemaVersions(string $a_entity): array { return [ diff --git a/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php b/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php new file mode 100644 index 000000000000..1e84c37de047 --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php @@ -0,0 +1,94 @@ +lng = $DIC->language(); + $this->state_holder = TestDIC::dic()['exportimport.state_holder']; + } + + public function getExportType(): string + { + return 'ZIP Results'; + } + + public function getExportOptionId(): string + { + return Types::XML_WITH_RESULTS->value; + } + + public function getSupportedRepositoryObjectTypes(): array + { + return ['tst']; + } + + public function getLabel(): string + { + $this->lng->loadLanguageModule('exp'); + $this->lng->loadLanguageModule('assessment'); + + return $this->lng->txt('exp_format_dropdown-xml') . ' (' . $this->lng->txt('ass_create_export_file_with_results') . ')'; + } + + public function onExportOptionSelected(ConsumerContext $context): void + { + $handler = new ExportHandlerLocator(); + $manager = $handler->manager()->handler(); + + $export_info = $manager->getExportInfoWithObject( + $context->exportObject(), + time(), + $handler->consumer()->exportConfig()->allExportConfigs() + ); + + // Prepare export state to bridge between export option and the xml exporter + $this->state_holder->create( + $export_info->getTarget(), + $handler->consumer()->exportConfig()->allExportConfigs(), + Types::XML_WITH_RESULTS->value + ); + + // Delegate the export to the manager which will call ilTestExporter + $manager->createExport( + 1, + $export_info, + '' + ); + } +} diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php new file mode 100644 index 000000000000..34ba3623c109 --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -0,0 +1,343 @@ +assertStep(ExportStep::INIT); + $state->setStep(ExportStep::PREPARE); + + $object_id = $this->extractObjectId($state); + if ($object_id === null) { + return; + } + + $collector = new TestCollector( + $this->participant_repository, + $this->results_repository, + $this->questions_repository, + $this->db, + $object_id + ); + $state->setCollector($collector); + + $transformations = $this->builder + ->withAdditionalPipes([ + new CollectUserIds(), + new CollectQuestionImages( + new UUIDFactory(), + $object_id + ), + new CollectResources($this->irss), + ]) + ->create(); + + $state->setTransformations($transformations); + } + + private function extractObjectId(ExportState $state): ?ObjectId + { + $target_ids = $state->target()->getObjectIds(); + + if (count($target_ids) === 0) { + $state->logger()->warning('No target object IDs found for test export'); + return null; + } + + if (count($target_ids) > 1) { + $state->logger()->warning( + 'Multiple target object IDs found for test export. Only the first one will be used.' + ); + } + + return $this->data_factory->objId(array_shift($target_ids)); + } + + /** + * @inheritDoc + */ + public function process(ExportState $state): void + { + $state->assertStep(ExportStep::PREPARE); + $state->setStep(ExportStep::PROCESS); + + $state->serializer()->group( + 'general', + fn() => $this->exportObject( + $state->collector(), + $state->transformations(), + $state->serializer(), + $state + ) + ); + $state->serializer()->group( + 'settings', + fn() => $this->exportSettings( + $state->collector(), + $state->transformations(), + $state->serializer(), + $state + ) + ); + $state->serializer()->group( + 'questions', + fn() => $this->exportQuestions( + $state->collector(), + $state->transformations(), + $state->serializer(), + $state + ) + ); + $state->serializer()->group( + 'skill_assignments', + fn() => $this->exportSkillAssignments( + $state->collector(), + $state->transformations(), + $state->serializer(), + ) + ); + + if ($state->getOption() === Types::XML_WITH_RESULTS->value) { + $this->processResults($state); + } + } + + private function processResults(ExportState $state): void + { + $state->serializer()->group( + 'participants', + fn() => $this->exportParticipants( + $state->collector(), + $state->transformations(), + $state->serializer() + ) + ); + $state->serializer()->group( + 'results', + fn() => $this->exportResults( + $state->collector(), + $state->transformations(), + $state->serializer() + ) + ); + } + + /** + * @inheritDoc + */ + public function write(ExportState $state): void + { + $state->assertStep(ExportStep::PROCESS); + $state->setStep(ExportStep::WRITE); + + $export_dir = $state->path()->getPathToComponentExpDirInContainer(); + $question_image_pipe = $state->transformations()->context(CollectQuestionImages::class); + $resource_pipe = $state->transformations()->context(CollectResources::class); + + foreach ($question_image_pipe->getFiles() as $file) { + if (file_exists($file['from'])) { + $state->writer()->writeFileByFilePath( + $file['from'], + "{$export_dir}/" . $file['to'] + ); + } else { + $state->logger()->warning('Question image file not found: ' . $file['from']); + } + } + + + $resource_dir = "{$export_dir}/resources"; + if (!file_exists($resource_dir)) { + mkdir($resource_dir, 0755, true); + } + + foreach ($resource_pipe->getResources() as $id => $resource) { + $file = "{$id}.{$resource->getCurrentRevision()->getInformation()->getSuffix()}"; + $state->writer()->writeFilesByResourceId( + $id, + "{$export_dir}/{$file}" + ); + + } + + $this->exportMappings( + $state->collector(), + $state->transformations(), + $state->serializer() + ); + } + + + private function exportObject( + TestCollector $collector, + Transformations $transformations, + Serializer $serializer, + ExportState $state + ): void { + $serializer->append('object', $transformations->normalize($collector->getObject())); + + $obj_id = $collector->getObjectId()->toInt(); + $state->addDependency('components/ILIAS/ILIASObject', 'common', [$obj_id]); + $state->addDependency('components/ILIAS/MetaData', 'qpl', ["{$obj_id}:0:qpl"]); + $state->addDependency( + 'components/ILIAS/Taxonomy', + 'tax', + $this->taxonomy->getUsageOfObject($obj_id) + ); + } + + private function exportSettings( + TestCollector $collector, + Transformations $transformations, + Serializer $serializer, + ExportState $state + ): void { + $test = $collector->getObject(); + $main_settings = $test->getMainSettings(); + + $serializer->append('main', $transformations->normalize($main_settings)); + $serializer->append('scoring', $transformations->normalize($test->getScoreSettings())); + $serializer->append('marks', $transformations->normalize($test->getMarkSchema())); + + if ($intro_page_id = $main_settings->getIntroductionSettings()->getIntroductionPageId()) { + $state->addDependency('components/ILIAS/COPage', 'pg', ["tst:{$intro_page_id}"]); + } + if ($concluding_page_id = $main_settings->getFinishingSettings()->getConcludingRemarksPageId()) { + $state->addDependency('components/ILIAS/COPage', 'pg', ["tst:{$concluding_page_id}"]); + } + } + + private function exportQuestions( + TestCollector $collector, + Transformations $transformations, + Serializer $serializer, + ExportState $state + ): void { + foreach ($collector->getQuestionObjects() as $question) { + $normalized = [ + ... $transformations->normalize($question), + 'feedback' => $transformations->normalize( + $collector->getFeedback($question) + ) + ]; + + if ($question instanceof assFormulaQuestion) { + $data = $collector->getUnitsAndCategories($question->getId()); + $normalized['formula_data'] = $transformations->normalize($data); + } + + $serializer->append('question', $normalized); + $state->addDependency('components/ILIAS/COPage', 'pg', ["qpl:{$question->getId()}"]); + } + } + + private function exportSkillAssignments( + TestCollector $collector, + Transformations $transformations, + Serializer $serializer, + ): void { + foreach ($collector->getSkillAssignments() as $assignment) { + $serializer->append('skill_assignment', $transformations->normalize($assignment)); + } + } + + private function exportParticipants( + TestCollector $collector, + Transformations $transformations, + Serializer $serializer + ): void { + foreach ($collector->getParticipants() as $participant) { + $serializer->append('participant', $transformations->normalize($participant)); + } + } + + private function exportResults( + TestCollector $collector, + Transformations $transformations, + Serializer $serializer + ): void { + foreach ($collector->getParticipantsIds() as $participant_id) { + $serializer->append( + 'results', + $transformations->normalize( + $collector->getResults($participant_id) + ) + ); + } + } + + private function exportMappings( + TestCollector $collector, + Transformations $transformations, + Serializer $serializer + ): void { + $serializer->startGroup('mappings'); + + $user_ids = $transformations->context(CollectUserIds::class)->getIds(); + $serializer->append('users', $collector->getUserMapping($user_ids)); + + $resources = $transformations->context(CollectResources::class)->getResources(); + $serializer->append( + 'resources', + array_map($transformations->normalize(...), $resources) + ); + + $serializer->endGroup('mappings'); + } +} diff --git a/components/ILIAS/Test/src/TestDIC.php b/components/ILIAS/Test/src/TestDIC.php index 134b3bb69325..671bddc9f78e 100755 --- a/components/ILIAS/Test/src/TestDIC.php +++ b/components/ILIAS/Test/src/TestDIC.php @@ -21,6 +21,7 @@ namespace ILIAS\Test; use ILIAS\LegalDocuments\ConsumerToolbox\Setting; +use ILIAS\Test\ExportImport\TestExporter; use ILIAS\Test\Participants\ParticipantRepository; use ILIAS\Test\Results\Data\Repository as TestResultRepository; use ILIAS\Test\Scoring\Marks\MarkSchemaFactory; @@ -49,6 +50,8 @@ use ILIAS\Test\Questions\Properties\DatabaseRepository as TestQuestionsDatabaseRepository; use ILIAS\Test\Results\Data\Factory as ResultsDataFactory; use ILIAS\Test\Results\Presentation\Factory as ResultsPresentationFactory; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\StateHolder; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ILIAS\TestQuestionPool\RequestDataCollector as QPLRequestDataCollector; use ILIAS\Data\Factory as DataFactory; @@ -229,6 +232,27 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC['ilDB'] ); + $dic['exportimport.state_holder'] = static fn($c): StateHolder => + new StateHolder(); + + $dic['exportimport.builder'] = static fn($c): Builder => + new Builder( + $DIC, + $c + ); + + $dic['exportimport.exporter'] = static fn($c): TestExporter => + new TestExporter( + $c['exportimport.builder'], + new DataFactory(), + $DIC->database(), + $DIC->resourceStorage(), + $c['participant.repository'], + $c['results.data.repository'], + $c['questions.properties.repository'], + $DIC->taxonomy()->domain() + ); + $dic['questions.properties.repository'] = static fn($c): TestQuestionsRepository => new TestQuestionsDatabaseRepository( $DIC['ilDB'], diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php index 1afb13e04b03..5b005ca56e1d 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php @@ -51,7 +51,8 @@ class ExportState public function __construct( private ExportTarget $target, - private ExportConfig $config + private ExportConfig $config, + private string $option = '' ) { $this->step = ExportStep::INIT; } @@ -66,6 +67,11 @@ public function config(): ExportConfig return $this->config; } + public function getOption(): string + { + return $this->option; + } + public function getStep(): ExportStep { return $this->step; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/StateHolder.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/StateHolder.php index 0c9ddb9d3855..fb5dd3dcac1c 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/StateHolder.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/StateHolder.php @@ -33,9 +33,12 @@ class StateHolder { private ?ExportState $export_state = null; - public function create(ExportTarget $target, ExportConfig $config): ExportState - { - $this->export_state = new ExportState($target, $config); + public function create( + ExportTarget $target, + ExportConfig $config, + string $option = '' + ): ExportState { + $this->export_state = new ExportState($target, $config, $option); return $this->export_state; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php index 3c652352a049..f6767b577d61 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php @@ -88,7 +88,8 @@ private function initExportState( string $component, string $target_release, string $type, - array $object_ids + array $object_ids, + string $option = '' ): ExportState { $target = $this->export_handler->target()->handler() ->withType($type) @@ -99,7 +100,8 @@ private function initExportState( return $this->state_holder->create( $target, - $this->export_handler->consumer()->exportConfig()->collection() + $this->export_handler->consumer()->exportConfig()->collection(), + $option ); } From 293e37abb713a9330c618b784d1e9cc382c00f2e Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Wed, 22 Apr 2026 11:31:27 +0200 Subject: [PATCH 17/66] feat(test): introduce skill threshold export --- .../class.ilTestSkillLevelThreshold.php | 37 ++++++++++++++++++- .../Test/src/ExportImport/TestCollector.php | 22 +++++++++++ .../Test/src/ExportImport/TestExporter.php | 19 +++++++++- ...ilAssQuestionSkillAssignmentNormalizer.php | 4 +- 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/components/ILIAS/Test/classes/class.ilTestSkillLevelThreshold.php b/components/ILIAS/Test/classes/class.ilTestSkillLevelThreshold.php index 70f22450ad38..26f40ff71821 100755 --- a/components/ILIAS/Test/classes/class.ilTestSkillLevelThreshold.php +++ b/components/ILIAS/Test/classes/class.ilTestSkillLevelThreshold.php @@ -17,6 +17,10 @@ *********************************************************************/ declare(strict_types=1); +use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; /** * @author Björn Heyser @@ -24,7 +28,7 @@ * * @package components\ILIAS/Test */ -class ilTestSkillLevelThreshold +class ilTestSkillLevelThreshold implements Normalizable { /** * @var ilDBInterface @@ -214,4 +218,35 @@ public function getThreshold(): ?int { return is_numeric($this->threshold) ? (int) $this->threshold : null; } + + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(fn(): array => [ + 'id' => $tt->normalize(new Id($this->getSkillLevelId(), 'skill_level')), + 'test_id' => $tt->normalize(new Id($this->getTestId(), 'test')), + 'skill_base_id' => $tt->normalize(new Id($this->getSkillBaseId(), 'skill_base')), + 'skill_tref_id' => $tt->normalize(new Id($this->getSkillTrefId(), 'skill_tref')), + 'threshold' => $this->getThreshold(), + ]); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = clone $this; + $clone->setSkillLevelId($tt->denormalize($normalized['id'], Id::class)->getId()); + $clone->setTestId($tt->denormalize($normalized['test_id'], Id::class)->getId()); + $clone->setSkillBaseId($tt->denormalize($normalized['skill_base_id'], Id::class)->getId()); + $clone->setSkillTrefId($tt->denormalize($normalized['skill_tref_id'], Id::class)->getId()); + $clone->setThreshold($normalized['threshold']); + return $clone; + }); + } } diff --git a/components/ILIAS/Test/src/ExportImport/TestCollector.php b/components/ILIAS/Test/src/ExportImport/TestCollector.php index dc80c6012540..7f187853def1 100644 --- a/components/ILIAS/Test/src/ExportImport/TestCollector.php +++ b/components/ILIAS/Test/src/ExportImport/TestCollector.php @@ -35,6 +35,8 @@ use ILIAS\TestQuestionPool\ExportImport\Export\CollectsQuestions; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\DataCollector; use ilObjTest; +use ilTestSkillLevelThreshold; +use ilTestSkillLevelThresholdList; /** * Collector to aggregate data from the test object for export. @@ -147,6 +149,26 @@ public function getTestQuestionProperties(): array return $this->questions; } + /** + * @return list + */ + public function getSkillLevelThresholds(): array + { + $threshold_list = new ilTestSkillLevelThresholdList($this->database()); + $threshold_list->setTestId($this->getTestId()); + $threshold_list->loadFromDb(); + + $thresholds = []; + foreach ($this->getSkillAssignments() as $assignment) { + $thresholds += $threshold_list->getThesholdsOfBaseAndTrefId( + $assignment->getSkillBaseId(), + $assignment->getSkillTrefId() + ); + } + + return $thresholds; + } + /* Participants */ diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index 34ba3623c109..fb7288556fa3 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -25,7 +25,6 @@ use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\ObjectId; use ILIAS\Data\UUID\Factory as UUIDFactory; -use ILIAS\Export\ExportHandler\I\Consumer\ExportWriter\HandlerInterface as ExportWriter; use ILIAS\ResourceStorage\Services as IRSS; use ILIAS\Taxonomy\DomainService as Taxonomy; use ILIAS\Test\ExportImport\Pipes\CollectUserIds; @@ -152,6 +151,14 @@ public function process(ExportState $state): void $state->serializer(), ) ); + $state->serializer()->group( + 'skill_thresholds', + fn() => $this->exportSkillLevelThresholds( + $state->collector(), + $state->transformations(), + $state->serializer(), + ) + ); if ($state->getOption() === Types::XML_WITH_RESULTS->value) { $this->processResults($state); @@ -297,6 +304,16 @@ private function exportSkillAssignments( } } + private function exportSkillLevelThresholds( + TestCollector $collector, + Transformations $transformations, + Serializer $serializer, + ): void { + foreach ($collector->getSkillLevelThresholds() as $threshold) { + $serializer->append('skill_level_threshold', $transformations->normalize($threshold)); + } + } + private function exportParticipants( TestCollector $collector, Transformations $transformations, diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php index 43bbd70f552c..0f63fdc77378 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php @@ -57,8 +57,8 @@ public function normalize($value): array|float|bool|int|string|null $normalized = [ 'parent_id' => $this->tt->normalize(new Id($value->getParentObjId(), 'object')), 'question_id' => $this->tt->normalize(new Id($value->getQuestionId(), 'question')), - 'base_id' => $value->getSkillBaseId(), - 'tref_id' => $value->getSkillTrefId(), + 'base_id' => $this->tt->normalize(new Id($value->getSkillBaseId(), 'skill_base')), + 'tref_id' => $this->tt->normalize(new Id($value->getSkillTrefId(), 'skill_tref')), 'original_title' => $value->getSkillTitle(), 'original_path' => $value->getSkillPath(), 'eval_mode' => $value->getEvalMode(), From 22cc32858ef73a3fa5c98485850727df2a10192c Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Wed, 22 Apr 2026 13:07:25 +0200 Subject: [PATCH 18/66] fix(test): missing redirection leads to duplicate export --- .../src/ExportImport/ExportOptions/XMLWithResultsOption.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php b/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php index 1e84c37de047..0249ba8be06b 100644 --- a/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php +++ b/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php @@ -20,6 +20,7 @@ namespace ILIAS\Test\ExportImport\ExportOptions; +use ilExportGUI; use ILIAS\Export\ExportHandler\Consumer\ExportOption\BasicLegacyHandler as BasicLegacyExportOption; use ILIAS\Export\ExportHandler\Factory as ExportHandlerLocator; use ILIAS\Export\ExportHandler\I\Consumer\Context\HandlerInterface as ConsumerContext; @@ -90,5 +91,7 @@ public function onExportOptionSelected(ConsumerContext $context): void $export_info, '' ); + + $this->ctrl->redirectByClass(ilExportGUI::class, ilExportGUI::CMD_LIST_EXPORT_FILES); } } From a6a362357b49bd41090da9c38ff81dc86eeddf28 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Wed, 22 Apr 2026 13:09:02 +0200 Subject: [PATCH 19/66] fix(test): incorrect resources export --- components/ILIAS/Test/src/ExportImport/TestExporter.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index fb7288556fa3..0231ed628f37 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -208,17 +208,11 @@ public function write(ExportState $state): void } } - - $resource_dir = "{$export_dir}/resources"; - if (!file_exists($resource_dir)) { - mkdir($resource_dir, 0755, true); - } - foreach ($resource_pipe->getResources() as $id => $resource) { $file = "{$id}.{$resource->getCurrentRevision()->getInformation()->getSuffix()}"; $state->writer()->writeFilesByResourceId( $id, - "{$export_dir}/{$file}" + "{$export_dir}/resources/{$file}" ); } From 02813c045a3d6024efed226b9cc45560f8610457 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Wed, 22 Apr 2026 13:16:54 +0200 Subject: [PATCH 20/66] fix(test): include question set config in export --- ...tRandomQuestionSetSourcePoolDefinition.php | 71 +++++++++++++++++- .../QuestionSetConfigNormalizer.php | 72 +++++++++++++++++++ .../Test/src/ExportImport/TestCollector.php | 50 +++++++++++++ .../Test/src/ExportImport/TestExporter.php | 16 +++++ 4 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php diff --git a/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php b/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php index d04074bbcfbf..b4164c416cf1 100755 --- a/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php +++ b/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php @@ -18,13 +18,18 @@ declare(strict_types=1); +use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; + /** * @author Björn Heyser * @version $Id$ * * @package Modules/Test */ -class ilTestRandomQuestionSetSourcePoolDefinition +class ilTestRandomQuestionSetSourcePoolDefinition implements Normalizable { private ?int $id = null; private ?int $pool_id = null; @@ -469,4 +474,68 @@ public function getPoolInfoLabel(ilLanguage $lng): string } // ----------------------------------------------------------------------------------------------------------------- + + /** + * @inheritDoc + */ + public function toNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function () use ($tt): array { + $normalized = [ + 'id' => $tt->normalize(new Id($this->getId(), static::class)), + 'pool_id' => $tt->normalize(new Id($this->getPoolId(), 'qpl')), + 'pool_title' => $this->getPoolTitle(), + 'pool_path' => $this->getPoolPath(), + 'quest_amount' => $this->getQuestionAmount(), + 'pool_quest_count' => $this->getPoolQuestionCount(), + 'position' => $this->getSequencePosition(), + 'type_filter' => $this->getTypeFilterAsTypeTags(), + 'lifecycle_filter' => $this->getLifecycleFilter(), + 'taxonomy_filter' => [], + ]; + + foreach ($this->getOriginalTaxonomyFilter() as $tax_id => $node_ids) { + $normalized['taxonomy_filter'][] = [ + 'tax_id' => $tt->normalize(new Id($tax_id, 'tax')), + 'node_ids' => array_map( + fn($node_id) => $tt->normalize(new Id($node_id, 'tax_node')), + $node_ids + ), + ]; + } + + return $normalized; + }); + } + + /** + * @inheritDoc + */ + public function fromNormalized(Transformations $tt): Transformation + { + return $tt->custom()->transformation(function (array $normalized) use ($tt): self { + $clone = clone $this; + $clone->setId($tt->denormalize($normalized['id'], Id::class)->getId()); + $clone->setPoolId($tt->denormalize($normalized['pool_id'], Id::class)->getId()); + $clone->setPoolTitle($tt->string($normalized['pool_title'])); + $clone->setPoolPath($tt->string($normalized['pool_path'])); + $clone->setQuestionAmount($tt->nullableInt($normalized['quest_amount'])); + $clone->setPoolQuestionCount($tt->nullableInt($normalized['pool_quest_count'])); + $clone->setSequencePosition($tt->int($normalized['position'])); + $clone->setTypeFilterFromTypeTags($normalized['type_filter']); + $clone->setLifecycleFilter($normalized['lifecycle_filter']); + + $taxonomy_filter = []; + foreach ($normalized['taxonomy_filter'] as $item) { + $tax_id = $tt->denormalize($item['tax_id'], Id::class)->getId(); + $taxonomy_filter[$tax_id] = array_map( + fn($node_id) => $tt->denormalize($node_id, Id::class)->getId(), + $item['node_ids'] + ); + } + $clone->setOriginalTaxonomyFilter($taxonomy_filter); + + return $clone; + }); + } } diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php new file mode 100644 index 000000000000..e2ca52dad50d --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php @@ -0,0 +1,72 @@ + + */ +#[Normalizes(ilTestQuestionSetConfig::class)] +class QuestionSetConfigNormalizer implements Normalizer +{ + /** + * @inheritDoc + */ + public function normalize($value): array|float|bool|int|string|null + { + if ($value instanceof ilTestFixedQuestionSetConfig) { + return [ + 'type' => ilObjTest::QUESTION_SET_TYPE_FIXED, + ]; + } + + if ($value instanceof ilTestRandomQuestionSetConfig) { + return [ + 'type' => ilObjTest::QUESTION_SET_TYPE_RANDOM, + 'homogeneous' => $value->arePoolsWithHomogeneousScoredQuestionsRequired(), + 'amount_mode' => $value->getQuestionAmountConfigurationMode(), + 'amount' => $value->getQuestionAmountPerTest(), + 'sync_timestamp' => $value->getLastQuestionSyncTimestamp(), + ]; + } + + throw new NormalizingException('Invalid value', $value); + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $value, string $type): ilTestQuestionSetConfig + { + if (!in_array(ilTestQuestionSetConfig::class, class_parents($type))) { + throw new NormalizingException("Invalid type for ilTestQuestionSetConfig: {$type}"); + } + + //TODO: Implement denormalization + } +} diff --git a/components/ILIAS/Test/src/ExportImport/TestCollector.php b/components/ILIAS/Test/src/ExportImport/TestCollector.php index 7f187853def1..af4044e6aa64 100644 --- a/components/ILIAS/Test/src/ExportImport/TestCollector.php +++ b/components/ILIAS/Test/src/ExportImport/TestCollector.php @@ -21,12 +21,15 @@ namespace ILIAS\Test\ExportImport; use Generator; +use ilComponentRepository; use ilDBConstants; use ilDBInterface; use ILIAS\Data\ObjectId; +use ILIAS\Language\Language; use ILIAS\Test\ExportImport\Envelopes\ManualFeedback; use ILIAS\Test\ExportImport\Envelopes\Solution; use ILIAS\Test\ExportImport\Envelopes\WorkingTime; +use ILIAS\Test\Logging\TestLogger; use ILIAS\Test\Participants\ParticipantRepository; use ILIAS\Test\Questions\Properties\Properties; use ILIAS\Test\Questions\Properties\Repository as QuestionsRepository; @@ -34,9 +37,14 @@ use ILIAS\Test\Settings\GlobalSettings\UserIdentifiers; use ILIAS\TestQuestionPool\ExportImport\Export\CollectsQuestions; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\DataCollector; +use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ilObjTest; +use ilTestQuestionSetConfigFactory; +use ilTestRandomQuestionSetSourcePoolDefinitionFactory; +use ilTestRandomQuestionSetSourcePoolDefinitionList; use ilTestSkillLevelThreshold; use ilTestSkillLevelThresholdList; +use ilTree; /** * Collector to aggregate data from the test object for export. @@ -55,7 +63,12 @@ public function __construct( private readonly ParticipantRepository $participant_repository, private readonly ResultsRepository $results_repository, private readonly QuestionsRepository $questions_repository, + private readonly GeneralQuestionPropertiesRepository $general_questions_repository, private readonly ilDBInterface $db, + private readonly ilTree $tree, + private readonly Language $lng, + private readonly TestLogger $logger, + private readonly ilComponentRepository $component_repository, private readonly ObjectId $object_id ) { } @@ -169,6 +182,43 @@ public function getSkillLevelThresholds(): array return $thresholds; } + /** + * Get the question set config and the source pool definitions. + * + * @return array{config: \ilTestQuestionSetConfig, definitions: list<\ilTestRandomQuestionSetSourcePoolDefinition>} + */ + public function getQuestionSetConfig(): array + { + $factory = new ilTestQuestionSetConfigFactory( + $this->tree, + $this->db, + $this->lng, + $this->logger, + $this->component_repository, + $this->getObject(), + $this->general_questions_repository + ); + + $config = $factory->getQuestionSetConfig(); + + $definition_factory = new ilTestRandomQuestionSetSourcePoolDefinitionFactory( + $this->db, + $this->getObject() + ); + + $definition_list = new ilTestRandomQuestionSetSourcePoolDefinitionList( + $this->db, + $this->getObject(), + $definition_factory + ); + $definition_list->loadDefinitions(); + + return [ + 'config' => $config, + 'definitions' => iterator_to_array($definition_list), + ]; + } + /* Participants */ diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index 0231ed628f37..3534fdfb99e3 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -21,13 +21,16 @@ namespace ILIAS\Test\ExportImport; use assFormulaQuestion; +use ilComponentRepository; use ilDBInterface; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\ObjectId; use ILIAS\Data\UUID\Factory as UUIDFactory; +use ILIAS\Language\Language; use ILIAS\ResourceStorage\Services as IRSS; use ILIAS\Taxonomy\DomainService as Taxonomy; use ILIAS\Test\ExportImport\Pipes\CollectUserIds; +use ILIAS\Test\Logging\TestLogger; use ILIAS\Test\Participants\ParticipantRepository; use ILIAS\Test\Questions\Properties\Repository as QuestionsRepository; use ILIAS\Test\Results\Data\Repository as ResultsRepository; @@ -39,6 +42,8 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\CollectResources; use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; +use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; +use ilTree; class TestExporter implements Exporter { @@ -46,10 +51,15 @@ public function __construct( private readonly Builder $builder, private readonly DataFactory $data_factory, private readonly ilDBInterface $db, + private readonly ilTree $tree, + private readonly Language $lng, + private readonly TestLogger $logger, + private readonly ilComponentRepository $component_repository, private readonly IRSS $irss, private readonly ParticipantRepository $participant_repository, private readonly ResultsRepository $results_repository, private readonly QuestionsRepository $questions_repository, + private readonly GeneralQuestionPropertiesRepository $general_questions_repository, private readonly Taxonomy $taxonomy ) { } @@ -71,7 +81,12 @@ public function prepare(ExportState $state): void $this->participant_repository, $this->results_repository, $this->questions_repository, + $this->general_questions_repository, $this->db, + $this->tree, + $this->lng, + $this->logger, + $this->component_repository, $object_id ); $state->setCollector($collector); @@ -255,6 +270,7 @@ private function exportSettings( $serializer->append('main', $transformations->normalize($main_settings)); $serializer->append('scoring', $transformations->normalize($test->getScoreSettings())); $serializer->append('marks', $transformations->normalize($test->getMarkSchema())); + $serializer->append('question_set_config', $transformations->normalize($collector->getQuestionSetConfig())); if ($intro_page_id = $main_settings->getIntroductionSettings()->getIntroductionPageId()) { $state->addDependency('components/ILIAS/COPage', 'pg', ["tst:{$intro_page_id}"]); From dea657358c018f8406e1a4035b44053f2f842fff Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Thu, 23 Apr 2026 09:40:06 +0200 Subject: [PATCH 21/66] refactor: remove request dependency in import stages --- .../classes/class.ilObjQuestionPoolGUI.php | 10 ++++++++-- .../Foundation/Contracts/ImportStage.php | 3 +-- .../Foundation/Importing/ImportStageRunner.php | 13 ++++++------- .../src/ExportImport/Import/CleanupStage.php | 3 +-- .../ExportImport/Import/DetectLegacyImportStage.php | 5 ++--- .../src/ExportImport/Import/PersistStage.php | 3 +-- .../ExportImport/Import/QuestionSelectionStage.php | 8 +++++--- .../ExportImport/Import/UploadValidationStage.php | 3 +-- 8 files changed, 25 insertions(+), 23 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php index c1d2bd668912..8a27e25ca8fe 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php @@ -131,7 +131,6 @@ public function __construct() $this->request_data_collector = $local_dic['request_data_collector']; $this->questionrepository = $local_dic['question.general_properties.repository']; $this->global_test_settings = $local_dic['global_test_settings']; - ; $this->import_session_repository = $local_dic['exportimport.session']; parent::__construct('', $this->request_data_collector->getRefId(), true, false); @@ -1146,7 +1145,14 @@ private function buildImportStageRunner(): ImportStageRunner [ new UploadValidationStage($this->archives, $this->lng, 'components/ILIAS/TestQuestionPool'), new DetectLegacyImportStage(), - new QuestionSelectionStage($this->lng, $this->component_factory, $this->ui_factory, $form_action), + new QuestionSelectionStage( + $this->lng, + $this->component_factory, + $this->ui_factory, + $this->request, + $form_action, + $this->lng->txt('import_qpl') + ), new PersistStage($this->lng, $this->request_data_collector, $this->import_session_repository), ], $this->import_session_repository, diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ImportStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ImportStage.php index bdb78f8e391d..48e2b6fbd2ed 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ImportStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ImportStage.php @@ -22,7 +22,6 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResult; -use Psr\Http\Message\ServerRequestInterface; /** * A single step in a multi-stage import workflow. Each stage is called by the ImportStageRunner and returns a @@ -52,5 +51,5 @@ public function getDescription(): ?string; * components to display. On form submission it should validate the input and return `StageResult::advance()` or * `StageResult::error()`. */ - public function process(ImportContext $context, ServerRequestInterface $request): StageResult; + public function process(ImportContext $context): StageResult; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php index 3a4ba77dd938..6aa50d897985 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php @@ -24,7 +24,6 @@ use ILIAS\UI\Component\Listing\Workflow\Linear; use ILIAS\UI\Component\Listing\Workflow\Step; use ILIAS\UI\Factory as UIFactory; -use Psr\Http\Message\ServerRequestInterface; /** * Orchestrates the execution of a list of ImportStage instances. It delegates each request to the currently active @@ -45,7 +44,7 @@ public function __construct( * Run the import stage runner. It manages the state of the import process and delegates to the current stage. * It will return a StageResult that indicates the next action to take. */ - public function run(ServerRequestInterface $request): StageResult + public function run(): StageResult { $index = $this->session->getCurrentStageIndex(); $context = $this->session->getContext(); @@ -55,7 +54,7 @@ public function run(ServerRequestInterface $request): StageResult } $stage = $this->stages[$index]; - $result = $stage->process($context, $request); + $result = $stage->process($context); switch ($result->type) { case StageResultType::ADVANCE: @@ -71,7 +70,7 @@ public function run(ServerRequestInterface $request): StageResult case StageResultType::ERROR: $this->session->setContext($result->context); - $this->reset($request); + $this->reset(); return $result; case StageResultType::INTERACT: @@ -79,7 +78,7 @@ public function run(ServerRequestInterface $request): StageResult return $result; case StageResultType::COMPLETE: - $this->reset($request); + $this->reset(); return $result; } @@ -125,10 +124,10 @@ public function buildWorkflow(UIFactory $ui, string $title): Linear /** * Reset the import stage session. If a final stage is set, it will be processed. */ - public function reset(ServerRequestInterface $request): void + public function reset(): void { if ($this->final_stage) { - $this->final_stage->process($this->session->getContext(), $request); + $this->final_stage->process($this->session->getContext()); } $this->session->clear(); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php index fbd99c94b976..78bd35fd448e 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php @@ -23,7 +23,6 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ImportStage; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResult; -use Psr\Http\Message\ServerRequestInterface; /** * Final import stage that cleans up the temporary files and directories after successful import or @@ -46,7 +45,7 @@ public function getDescription(): ?string return null; } - public function process(ImportContext $context, ServerRequestInterface $request): StageResult + public function process(ImportContext $context): StageResult { $file_to_import = $context->get('file_to_import'); if ($file_to_import !== null) { diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php index da921c6afd50..e0d657763446 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php @@ -23,7 +23,6 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ImportStage; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResult; -use Psr\Http\Message\ServerRequestInterface; /** * @deprecated This stage is only used for legacy imports and will be removed with further ILIAS versions. @@ -45,13 +44,13 @@ public function getDescription(): ?string return null; } - public function process(ImportContext $context, ServerRequestInterface $request): StageResult + public function process(ImportContext $context): StageResult { $import_base_dir = $context->get('import_base_dir'); $import_name = basename($import_base_dir); $xml_file = $import_base_dir . DIRECTORY_SEPARATOR . $import_name . '.xml'; - $qti_file = $import_base_dir . DIRECTORY_SEPARATOR . str_replace('_qpl_', '_qti_', $import_name) . '.xml'; + $qti_file = $import_base_dir . DIRECTORY_SEPARATOR . str_replace(['_qpl_', '_tst_'], '_qti_', $import_name) . '.xml'; if (!file_exists($qti_file) || !file_exists($xml_file)) { return StageResult::advance($context); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php index 83ca3be6e343..36a6069fedee 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php @@ -27,7 +27,6 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResult; use ILIAS\TestQuestionPool\RequestDataCollector; use ilImport; -use Psr\Http\Message\ServerRequestInterface; /** * Final stage of the question pool import process. Imports the question pool object and all its questions and other @@ -57,7 +56,7 @@ public function getDescription(): ?string return ''; } - public function process(ImportContext $context, ServerRequestInterface $request): StageResult + public function process(ImportContext $context): StageResult { $importer = new ilImport($this->request_data_collector->getRefId()); $importer->importObject( diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php index e7fd2b79bb9c..32e3a0ad6499 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php @@ -54,7 +54,9 @@ public function __construct( private readonly Language $lng, private readonly ilComponentFactory $component_factory, private readonly UIFactory $ui_factory, + private readonly ServerRequestInterface $request, private readonly string $form_action, + private readonly string $title, ) { } @@ -73,7 +75,7 @@ public function getDescription(): ?string return ''; } - public function process(ImportContext $context, ServerRequestInterface $request): StageResult + public function process(ImportContext $context): StageResult { if ($context->has('selectable_questions')) { $options = []; @@ -82,7 +84,7 @@ public function process(ImportContext $context, ServerRequestInterface $request) } $data = $this->buildSelectQuestionsForm($options) - ->withRequest($request) + ->withRequest($this->request) ->getData(); if (isset($data['selected_questions'])) { @@ -103,7 +105,7 @@ public function process(ImportContext $context, ServerRequestInterface $request) } $panel = $this->ui_factory->panel()->standard( - $this->lng->txt('import_qpl'), + $this->title, [ $this->ui_factory->legacy()->content($this->lng->txt('qpl_import_verify_found_questions')), $this->buildSelectQuestionsForm($options) diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php index 28e602c7cd87..0e8641e8f2d1 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php @@ -28,7 +28,6 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResult; use ilManifestParser; -use Psr\Http\Message\ServerRequestInterface; /** * First stage of the question pool import pipeline. Receives the uploaded file path from the context, extracts ZIP @@ -60,7 +59,7 @@ public function getDescription(): ?string return ''; } - public function process(ImportContext $context, ServerRequestInterface $request): StageResult + public function process(ImportContext $context): StageResult { $file_to_import = $context->get('file_to_import'); if ( From a62d928d034158c78643acd5f48975bbb9474dd0 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Fri, 24 Apr 2026 09:56:21 +0200 Subject: [PATCH 22/66] refactor(qpl): create question importer class --- .../class.ilTestQuestionPoolImporter.php | 2 +- .../Import/QuestionPoolImporter.php | 147 ++++++++++++ .../QuestionsImporter.php} | 223 ++++++------------ .../{ => Import}/SkillAssignmentsImporter.php | 2 +- ...ilAssQuestionSkillAssignmentNormalizer.php | 4 +- .../TestQuestionPool/src/QuestionPoolDIC.php | 15 +- 6 files changed, 241 insertions(+), 152 deletions(-) create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php rename components/ILIAS/TestQuestionPool/src/ExportImport/{QuestionPoolImporter.php => Import/QuestionsImporter.php} (52%) rename components/ILIAS/TestQuestionPool/src/ExportImport/{ => Import}/SkillAssignmentsImporter.php (98%) diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php index bb9be798b3db..e94a008c0d3e 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php @@ -22,7 +22,7 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\SimpleXMLDeserializer; use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; -use ILIAS\TestQuestionPool\ExportImport\QuestionPoolImporter; +use ILIAS\TestQuestionPool\ExportImport\Import\QuestionPoolImporter; use ILIAS\TestQuestionPool\QuestionPoolDIC; class ilTestQuestionPoolImporter extends ilXmlImporter diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php new file mode 100644 index 000000000000..c5e967d3940e --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php @@ -0,0 +1,147 @@ +data_factory->objId(0)); + $tt = $this->builder->withAdditionalPipes(append: [$id_mapping_pipe, $images_pipe])->create(); + + $selected_questions = QuestionSelectionStage::getSelectedQuestions($context); + + $deserializer->addHandler( + 'general', + function (array $objects) use ($tt, $mapping, $parent_id, &$context): void { + $new_pool_id = $this->importQuestionPool( + array_pop($objects), + $tt, + $mapping, + $parent_id + ); + $context = $context->with('pool_obj_id', $new_pool_id); + } + ); + + $deserializer->addHandler( + 'questions', + function (array $questions) use ($tt, $mapping, $selected_questions): void { + foreach ($questions as $question) { + $this->questions_importer->importQuestion( + $question, + $tt, + $mapping, + $selected_questions + ); + } + } + ); + + $deserializer->addHandler( + 'skill_assignments', + function (array $assignments) use ($tt, &$context): void { + $result = $this->skill_importer->import( + $assignments, + UploadValidationStage::getInstallId($context), + $tt, + ); + $context = $context->with('skill_assignments', $result); + } + ); + + $deserializer->process(); + + // Copy the question images from the temporary import directory to the question pool directory + $this->questions_importer->importQuestionImages($mapping, $context, $images_pipe); + + return $context; + } + + protected function importQuestionPool( + array $normalized, + Transformations $transformations, + ilImportMapping $mapping, + ReferenceId $parent_id + ): int { + $pool_object = $transformations->denormalize($normalized, ilObjQuestionPool::class); + $old_pool_id = $pool_object->getId(); + + $pool_object->setTitle('Imported'); //TODO: Remove after testing + $new_pool_id = $pool_object->create(true); + $pool_object->getObjectProperties()->storePropertyIsOnline( + $pool_object->getObjectProperties()->getPropertyIsOnline()->withOffline() + ); + $pool_object->saveToDb(); + + $pool_object->createReference(); + $pool_object->putInTree($parent_id->toInt()); + $pool_object->setPermissions($parent_id->toInt()); + + $mapping->addMapping('components/ILIAS/TestQuestionPool', 'qpl', (string) $old_pool_id, (string) $new_pool_id); + $mapping->addMapping('components/ILIAS/TestQuestionPool', 'object', (string) $old_pool_id, (string) $new_pool_id); + $mapping->addMapping('components/ILIAS/MetaData', 'md', "{$old_pool_id}:0:qpl", "{$new_pool_id}:0:qpl"); + + return $new_pool_id; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php similarity index 52% rename from components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php index ec98b3fd5dea..c11b5af840b3 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport; +namespace ILIAS\TestQuestionPool\ExportImport\Import; use assFormulaQuestion; use assFormulaQuestionUnit; @@ -26,135 +26,29 @@ use assQuestion; use ilCtrl; use ilDBInterface; -use ILIAS\Data\ReferenceId; -use ILIAS\Data\ObjectId; -use ILIAS\Data\UUID\Factory; use ILIAS\Language\Language; use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Deserializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\IdMappingPipe; use ILIAS\TestQuestionPool\ExportImport\Envelopes\Feedback; -use ILIAS\TestQuestionPool\ExportImport\Import\QuestionSelectionStage; -use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; use ILIAS\TestQuestionPool\Questions\Files\QuestionFiles; use ilImportMapping; -use ilObjQuestionPool; use ilUnitConfigurationRepository; -/** - * Orchestrates the import of a question pool. It uses the Builder to create a pipeline of transformations that are used - * to normalize the data provided by the deserializer. It imports the question pool object and its content (questions, - * skill assignments, etc.) into the database using repository classes and legacy active record models. - */ -class QuestionPoolImporter +class QuestionsImporter { - private const string COMPONENT = 'components/ILIAS/TestQuestionPool'; - public function __construct( - private readonly Builder $builder, + private readonly string $component, + private readonly string $parent_type, private readonly ilCtrl $ctrl, private readonly ilDBInterface $database, private readonly Language $language, - private readonly SkillAssignmentsImporter $skill_importer, ) { } - /** - * Import a question pool from a deserializer instance. It will import the question pool object, questions and skill - * assignments into the database. - */ - public function import( - Deserializer $deserializer, - ilImportMapping $mapping, - ReferenceId $parent_id, - ImportContext $context - ): ImportContext { - $id_mapping_pipe = new IdMappingPipe($mapping, self::COMPONENT); - $images_pipe = new CollectQuestionImages(new Factory(), new ObjectId(0)); - $tt = $this->builder->withAdditionalPipes(append: [$id_mapping_pipe, $images_pipe])->create(); - - $selected_questions = QuestionSelectionStage::getSelectedQuestions($context); - - $deserializer->addHandler( - 'general', - function (array $objects) use ($tt, $mapping, $parent_id, &$context): void { - $new_pool_id = $this->importQuestionPool( - array_pop($objects), - $tt, - $mapping, - $parent_id - ); - $context = $context->with('pool_obj_id', $new_pool_id); - } - ); - - $deserializer->addHandler( - 'questions', - function (array $questions) use ($tt, $mapping, $selected_questions): void { - foreach ($questions as $question) { - $this->importQuestion( - $question, - $tt, - $mapping, - $selected_questions - ); - } - } - ); - - $deserializer->addHandler( - 'skill_assignments', - function (array $assignments) use ($tt, &$context): void { - $result = $this->skill_importer->import( - $assignments, - UploadValidationStage::getInstallId($context), - $tt, - ); - $context = $context->with('skill_assignments', $result); - } - ); - - $deserializer->process(); - - // Copy the question images from the temporary import directory to the question pool directory - $this->importQuestionImages($mapping, $context, $images_pipe); - - return $context; - } - - protected function importQuestionPool( - array $normalized, - Transformations $transformations, - ilImportMapping $mapping, - ReferenceId $parent_id - ): int { - $pool_object = $transformations->denormalize($normalized, ilObjQuestionPool::class); - $old_pool_id = $pool_object->getId(); - - $pool_object->setTitle('Imported'); //TODO: Remove after testing - $new_pool_id = $pool_object->create(true); - $pool_object->getObjectProperties()->storePropertyIsOnline( - $pool_object->getObjectProperties()->getPropertyIsOnline()->withOffline() - ); - $pool_object->saveToDb(); - - $pool_object->createReference(); - $pool_object->putInTree($parent_id->toInt()); - $pool_object->setPermissions($parent_id->toInt()); - - $mapping->addMapping(self::COMPONENT, 'qpl', (string) $old_pool_id, (string) $new_pool_id); - $mapping->addMapping(self::COMPONENT, 'object', (string) $old_pool_id, (string) $new_pool_id); - $mapping->addMapping('components/ILIAS/MetaData', 'md', "{$old_pool_id}:0:qpl", "{$new_pool_id}:0:qpl"); - - return $new_pool_id; - } - - protected function importQuestion( + public function importQuestion( array $normalized, Transformations $transformations, ilImportMapping $mapping, @@ -178,11 +72,7 @@ protected function importQuestion( // Create new question and store basic question properties $new_question_id = $question->createNewQuestion(false); - $mapping->addMapping(self::COMPONENT, 'question', (string) $old_question_id, (string) $new_question_id); - $mapping->addMapping(self::COMPONENT, 'question_assignment', (string) $new_question_id, (string) $question->getObjId()); - $mapping->addMapping('components/ILIAS/Taxonomy', 'tax_item', "qpl:quest:{$old_question_id}", (string) $new_question_id); - $mapping->addMapping('components/ILIAS/Taxonomy', 'tax_item_obj_id', "qpl:quest:{$old_question_id}", (string) $question->getObjId()); - $mapping->addMapping('components/ILIAS/COPage', 'pg', "qpl:{$old_question_id}", "qpl:{$new_question_id}"); + $this->storeQuestionMappings($mapping, $old_question_id, $new_question_id, $question->getObjId()); if ($question instanceof assFormulaQuestion) { $this->importFormulaQuestion($normalized, $question, $transformations, $mapping, ); @@ -195,7 +85,74 @@ protected function importQuestion( $this->importFeedback($feedback, $question); } - protected function importFeedback(Feedback $feedback, assQuestion $question): void + + public function importQuestionImages( + ilImportMapping $mapping, + ImportContext $context, + CollectQuestionImages $pipe, + ): void { + $import_dir = dirname($context->get('import_file')) . DIRECTORY_SEPARATOR . 'expDir_1'; + + $question_files = new QuestionFiles(); + foreach ($pipe->getEnvelopes() as $from_path => $envelope) { + $question_id = $mapping->getMapping($this->component, 'question', (string) $envelope->getQuestionId()); + if (!$question_id) { + continue; + } + + $base_dir = $envelope->getType() === QuestionImage::TYPE_ANSWER + ? $question_files->buildImagePath($question_id, $context->get('pool_obj_id')) + : $question_files->buildSolutionPath($question_id, $context->get('pool_obj_id')); + + if (!file_exists($base_dir)) { + mkdir($base_dir, 0755, true); + } + + copy($import_dir . DIRECTORY_SEPARATOR . $from_path, $base_dir . $envelope->getFilename()); + + //TODO: generate thumbnail + } + } + + private function storeQuestionMappings( + ilImportMapping $mapping, + int $old_question_id, + int $new_question_id, + int $parent_obj_id, + ): void { + $mapping->addMapping( + $this->component, + 'question', + (string) $old_question_id, + (string) $new_question_id + ); + $mapping->addMapping( + $this->component, + 'question_assignment', + (string) $new_question_id, + (string) $parent_obj_id + ); + $mapping->addMapping( + 'components/ILIAS/Taxonomy', + 'tax_item', + "{$this->parent_type}:quest:{$old_question_id}", + (string) $new_question_id + ); + $mapping->addMapping( + 'components/ILIAS/Taxonomy', + 'tax_item_obj_id', + "{$this->parent_type}:quest:{$old_question_id}", + (string) $parent_obj_id + ); + $mapping->addMapping( + 'components/ILIAS/COPage', + 'pg', + "{$this->parent_type}:{$old_question_id}", + "{$this->parent_type}:{$new_question_id}" + ); + } + + private function importFeedback(Feedback $feedback, assQuestion $question): void { $question_id = $question->getId(); $question->feedbackOBJ->importGenericFeedback($question_id, false, $feedback->getGenericUncompleted()); @@ -211,7 +168,7 @@ protected function importFeedback(Feedback $feedback, assQuestion $question): vo } } - protected function importFormulaQuestion( + private function importFormulaQuestion( array $normalized, assFormulaQuestion $question, Transformations $transformations, @@ -226,7 +183,7 @@ protected function importFormulaQuestion( $old_category_id = $category->getId(); $repository->saveNewUnitCategory($category); - $mapping->addMapping(self::COMPONENT, 'unit_category', (string) $old_category_id, (string) $category->getId()); + $mapping->addMapping($this->component, 'unit_category', (string) $old_category_id, (string) $category->getId()); } // Ensure base units are imported first so they can be referenced by the units. The mapping pipe will ensure @@ -237,7 +194,7 @@ protected function importFormulaQuestion( $unit = new assFormulaQuestionUnit(); $repository->createNewUnit($unit); - $mapping->addMapping(self::COMPONENT, 'unit', (string) $old_unit_id, (string) $unit->getId()); + $mapping->addMapping($this->component, 'unit', (string) $old_unit_id, (string) $unit->getId()); $unit = $transformations->denormalize($normalized_unit, $unit); $repository->saveUnit($unit); @@ -256,30 +213,4 @@ protected function importFormulaQuestion( $question->addResult($result); } } - - protected function importQuestionImages( - ilImportMapping $mapping, - ImportContext $context, - CollectQuestionImages $pipe, - ): void { - $import_dir = dirname($context->get('import_file')) . DIRECTORY_SEPARATOR . 'expDir_1'; - - $question_files = new QuestionFiles(); - foreach ($pipe->getEnvelopes() as $from_path => $envelope) { - $question_id = $mapping->getMapping('components/ILIAS/TestQuestionPool', 'question', (string) $envelope->getQuestionId()); - if (!$question_id) { - continue; - } - - $base_dir = $envelope->getType() === QuestionImage::TYPE_ANSWER - ? $question_files->buildImagePath($question_id, $context->get('pool_obj_id')) - : $question_files->buildSolutionPath($question_id, $context->get('pool_obj_id')); - - if (!file_exists($base_dir)) { - mkdir($base_dir, 0755, true); - } - - copy($import_dir . DIRECTORY_SEPARATOR . $from_path, $base_dir . $envelope->getFilename()); - } - } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/SkillAssignmentsImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php similarity index 98% rename from components/ILIAS/TestQuestionPool/src/ExportImport/SkillAssignmentsImporter.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php index a22364815c72..f0de70e409f8 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/SkillAssignmentsImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport; +namespace ILIAS\TestQuestionPool\ExportImport\Import; use ilAssQuestionSkillAssignment; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php index 0f63fdc77378..f8ea08567123 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php @@ -104,8 +104,8 @@ public function denormalize(array|float|bool|int|string|null $value, string $typ $assignment = new ilAssQuestionSkillAssignment($this->db); $assignment->setParentObjId($this->tt->denormalize($value['parent_id'], Id::class)->getId()); $assignment->setQuestionId($this->tt->denormalize($value['question_id'], Id::class)->getId()); - $assignment->setSkillBaseId($this->tt->int($value['base_id'])); - $assignment->setSkillTrefId($this->tt->int($value['tref_id'])); + $assignment->setSkillBaseId($this->tt->denormalize($value['base_id'], Id::class)->getId()); + $assignment->setSkillTrefId($this->tt->denormalize($value['tref_id'], Id::class)->getId()); $assignment->setSkillTitle($this->tt->string($value['original_title'])); $assignment->setSkillPath($this->tt->string($value['original_path'])); $assignment->setEvalMode($this->tt->string($value['eval_mode'])); diff --git a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php index 95987154f8b0..bedf56db471c 100755 --- a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php +++ b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php @@ -25,8 +25,9 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Export\QuestionPoolExporter; -use ILIAS\TestQuestionPool\ExportImport\QuestionPoolImporter; -use ILIAS\TestQuestionPool\ExportImport\SkillAssignmentsImporter; +use ILIAS\TestQuestionPool\ExportImport\Import\QuestionPoolImporter; +use ILIAS\TestQuestionPool\ExportImport\Import\QuestionsImporter; +use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter; use Pimple\Container as PimpleContainer; use ILIAS\DI\Container as ILIASContainer; use ILIAS\TestQuestionPool\Questions\SuggestedSolution\SuggestedSolutionsDatabaseRepository; @@ -98,12 +99,22 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC->skills()->usage(), (int) $DIC->settings()->get('inst_id', '0') ); + $dic['exportimport.questions_importer'] = static fn($c): QuestionsImporter => + new QuestionsImporter( + 'components/ILIAS/TestQuestionPool', + 'qpl', + $DIC->ctrl(), + $DIC->database(), + $DIC->language() + ); $dic['exportimport.importer'] = static fn($c): QuestionPoolImporter => new QuestionPoolImporter( $c['exportimport.builder'], $DIC->ctrl(), $DIC->database(), $DIC->language(), + new DataFactory(), + $c['exportimport.questions_importer'], $c['exportimport.skill_assignments_importer'] ); From 216fa7f30dceddbbdd70e38593905c79c6f2bd64 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Mon, 27 Apr 2026 11:20:03 +0200 Subject: [PATCH 23/66] refactor(qpl): replace string constants with defined constants in import stages --- .../classes/class.ilObjQuestionPoolGUI.php | 2 +- .../class.ilTestQuestionPoolImporter.php | 45 +-------------- ...class.ilTestQuestionPoolLegacyImporter.php | 8 ++- .../Serializing/SimpleXMLSerializer.php | 2 +- .../src/ExportImport/Import/CleanupStage.php | 4 +- .../Import/DetectLegacyImportStage.php | 11 ++-- .../src/ExportImport/Import/PersistStage.php | 4 +- .../Import/QuestionPoolImporter.php | 9 +++ .../Import/QuestionSelectionStage.php | 20 ++++--- .../ExportImport/Import/QuestionsImporter.php | 56 +++++++++++++++++-- .../Import/UploadValidationStage.php | 15 +++-- 11 files changed, 102 insertions(+), 74 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php index 8a27e25ca8fe..8d9651b03ebf 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php @@ -1111,7 +1111,7 @@ protected function importFile(string $file_to_import, string $path_to_uploaded_f { $this->import_session_repository->clear(); - $context = new ImportContext(['file_to_import' => $file_to_import]); + $context = new ImportContext([UploadValidationStage::FILE_TO_IMPORT => $file_to_import]); $this->import_session_repository->setContext($context); $this->import_session_repository->setCurrentStageIndex(0); diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php index e94a008c0d3e..2dee6dc85e14 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php @@ -83,53 +83,10 @@ public function finalProcessing(ilImportMapping $a_mapping): void return; } - $this->finalizeQuestionPages($a_mapping); + $this->importer->finalize($a_mapping); $this->finalizeTaxonomyUsage($a_mapping); } - /** - * Finalize the imported question pages by replacing the old question ids with the new question ids. - */ - private function finalizeQuestionPages(ilImportMapping $a_mapping): void - { - $page_mappings = $a_mapping->getMappingsOfEntity('components/ILIAS/COPage', 'pg'); - - foreach ($page_mappings as $old => $new) { - if (!preg_match('/^qpl:(\d+)$/', $old, $old_matches)) { - continue; - } - $old_question_id = $old_matches[1]; - - if (!preg_match('/^qpl:(\d+)$/', $new, $new_matches)) { - continue; - } - $new_question_id = $new_matches[1]; - - $page = new ilAssQuestionPage((int) $new_question_id); - $xml = preg_replace( - '/il_\d+_qst_' . preg_quote($old_question_id, '/') . '\b/', - "il__qst_{$new_question_id}", - $page->getXMLContent() - ); - if ($xml === null) { - continue; - } - $page->setXMLContent($xml); - - $parent_obj_id = $a_mapping->getMapping( - 'components/ILIAS/TestQuestionPool', - 'question_assignment', - $new_question_id - ); - if ($parent_obj_id !== null) { - $page->setParentId((int) $parent_obj_id); - } - - $page->updateFromXML(); - unset($page); - } - } - private function finalizeTaxonomyUsage(ilImportMapping $a_mapping): void { $qpl_mappings = $a_mapping->getMappingsOfEntity('components/ILIAS/TestQuestionPool', 'qpl'); diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php index 921d9958f542..5924475c1875 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php @@ -19,6 +19,8 @@ declare(strict_types=1); use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; +use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; +use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\TestQuestionPool\RequestDataCollector; @@ -55,8 +57,8 @@ public function importXmlRepresentation( $a_mapping->addMapping('components/ILIAS/TestQuestionPool', 'qpl', $a_id, (string) $this->pool_obj->getId()); $context = $this->session->getContext(); - $import_base_dir = $context->get('import_base_dir'); - $xml_file = $context->get('xml_file'); + $import_base_dir = $context->get(UploadValidationStage::IMPORT_BASE_DIR); + $xml_file = $context->get(DetectLegacyImportStage::LEGACY_XML_FILE); $context = $context->with('pool_obj_id', $this->pool_obj->getId()); $this->session->setContext($context); @@ -77,7 +79,7 @@ public function importXmlRepresentation( $qti_parser = new ilQTIParser( $import_base_dir, - $context->get('qti_file'), + $context->get(DetectLegacyImportStage::LEGACY_QTI_FILE), ilQTIParser::IL_MO_PARSE_QTI, $this->pool_obj->getId(), $context->get('selected_questions') diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php index 70c73271663c..86b27a4c5b57 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php @@ -55,7 +55,7 @@ public function open(string $path): static * * @throws \LogicException if a document has already been started */ - private function createDocument(string $comment): void + public function createDocument(string $comment): void { if ($this->has_document) { throw new \LogicException('XML document already started'); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php index 78bd35fd448e..739e3f812fce 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php @@ -47,7 +47,7 @@ public function getDescription(): ?string public function process(ImportContext $context): StageResult { - $file_to_import = $context->get('file_to_import'); + $file_to_import = $context->get(UploadValidationStage::FILE_TO_IMPORT); if ($file_to_import !== null) { $temp_dir = dirname($file_to_import); if (file_exists($temp_dir) && is_dir($temp_dir)) { @@ -55,7 +55,7 @@ public function process(ImportContext $context): StageResult } } - $import_base_dir = $context->get('import_base_dir'); + $import_base_dir = $context->get(UploadValidationStage::IMPORT_BASE_DIR); if (file_exists($import_base_dir) && is_dir($import_base_dir)) { $this->removeDirectory($import_base_dir); } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php index e0d657763446..1cb180a77c3e 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php @@ -29,6 +29,9 @@ */ class DetectLegacyImportStage implements ImportStage { + public const string LEGACY_QTI_FILE = 'legacy_qti_file'; + public const string LEGACY_XML_FILE = 'legacy_xml_file'; + public function getIdentifier(): string { return 'detect_legacy_import'; @@ -46,7 +49,7 @@ public function getDescription(): ?string public function process(ImportContext $context): StageResult { - $import_base_dir = $context->get('import_base_dir'); + $import_base_dir = $context->get(UploadValidationStage::IMPORT_BASE_DIR); $import_name = basename($import_base_dir); $xml_file = $import_base_dir . DIRECTORY_SEPARATOR . $import_name . '.xml'; @@ -57,13 +60,13 @@ public function process(ImportContext $context): StageResult } return StageResult::advance( - $context->with('qti_file', $qti_file) - ->with('xml_file', $xml_file) + $context->with(self::LEGACY_QTI_FILE, $qti_file) + ->with(self::LEGACY_XML_FILE, $xml_file) ); } public static function isLegacyImport(ImportContext $context): bool { - return $context->has('qti_file') && $context->has('xml_file'); + return $context->has(self::LEGACY_QTI_FILE) && $context->has(self::LEGACY_XML_FILE); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php index 36a6069fedee..f3173b323d0f 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php @@ -61,8 +61,8 @@ public function process(ImportContext $context): StageResult $importer = new ilImport($this->request_data_collector->getRefId()); $importer->importObject( null, - $context->get('file_to_import'), - basename($context->get('file_to_import')), + $context->get(UploadValidationStage::FILE_TO_IMPORT), + basename($context->get(UploadValidationStage::FILE_TO_IMPORT)), 'qpl', 'components/ILIAS/TestQuestionPool', true, diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php index c5e967d3940e..00162cfeb235 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php @@ -118,6 +118,15 @@ function (array $assignments) use ($tt, &$context): void { return $context; } + /** + * Finalize the import after all dependencies have been imported. + * It will replace the old question ids with the new question ids in the question pages. + */ + public function finalize(ilImportMapping $mapping): void + { + $this->questions_importer->finalizeQuestionPages($mapping); + } + protected function importQuestionPool( array $normalized, Transformations $transformations, diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php index 32e3a0ad6499..9276e158ad88 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php @@ -36,6 +36,9 @@ */ class QuestionSelectionStage implements ImportStage { + public const string SELECTED_QUESTIONS = 'selected_questions'; + private const string SELECTABLE_QUESTIONS = 'selectable_questions'; + private array $old_export_question_types = [ 'ORDERING QUESTION' => \ilQTIItem::QT_ORDERING, 'KPRIM CHOICE QUESTION' => \ilQTIItem::QT_KPRIM_CHOICE, @@ -88,11 +91,11 @@ public function process(ImportContext $context): StageResult ->getData(); if (isset($data['selected_questions'])) { - return StageResult::advance($context->with('selected_questions', $data['selected_questions'])); + return StageResult::advance($context->with(self::SELECTED_QUESTIONS, $data['selected_questions'])); } } - if (!$context->has('import_file')) { + if (!$context->has(UploadValidationStage::COMPONENT_IMPORT_FILE)) { return StageResult::error($context, $this->lng->txt('qpl_import_file_not_found')); } @@ -113,7 +116,7 @@ public function process(ImportContext $context): StageResult ); return StageResult::interact( - $context->with('selectable_questions', array_keys($options)), + $context->with(self::SELECTABLE_QUESTIONS, array_keys($options)), [$panel] ); } @@ -123,14 +126,17 @@ public function process(ImportContext $context): StageResult */ public static function getSelectedQuestions(ImportContext $context): array { - return array_map('intval', $context->get('selected_questions', [])); + return array_map('intval', $context->get(self::SELECTED_QUESTIONS, [])); } private function readQuestions(ImportContext $context): array { $options = []; - $deserializer = new SimpleXMLDeserializer()->open(file_get_contents($context->get('import_file'))); + $deserializer = new SimpleXMLDeserializer()->open( + file_get_contents($context->get(UploadValidationStage::COMPONENT_IMPORT_FILE)) + ); + $deserializer->addHandler('questions', function (array $questions) use (&$options): void { foreach ($questions as $question) { if (!isset($question['title']) || !isset($question['type'])) { @@ -153,8 +159,8 @@ private function readQuestions(ImportContext $context): array private function readQuestionsFromQTI(ImportContext $context): array { $parser = new \ilQTIParser( - $context->get('import_base_dir'), - $context->get('qti_file'), + $context->get(UploadValidationStage::IMPORT_BASE_DIR), + $context->get(DetectLegacyImportStage::LEGACY_QTI_FILE), \ilQTIParser::IL_MO_VERIFY_QTI, 0 ); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php index c11b5af840b3..8cb86d4176e1 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php @@ -24,6 +24,7 @@ use assFormulaQuestionUnit; use assFormulaQuestionUnitCategory; use assQuestion; +use ilAssQuestionPage; use ilCtrl; use ilDBInterface; use ILIAS\Language\Language; @@ -53,7 +54,7 @@ public function importQuestion( Transformations $transformations, ilImportMapping $mapping, array $selected_questions - ): void { + ): ?assQuestion { $question_class = $normalized['type']; if (!class_exists($question_class)) { throw new \InvalidArgumentException("Question class {$question_class} does not exist"); @@ -63,7 +64,7 @@ public function importQuestion( $question = $transformations->denormalize($normalized, new $question_class()); $old_question_id = $question->getId(); if (!in_array($old_question_id, $selected_questions)) { - return; + return null; } // Initialize feedback object to prevent error when saving the question @@ -83,6 +84,8 @@ public function importQuestion( $feedback = $transformations->denormalize($normalized['feedback'], Feedback::class); $this->importFeedback($feedback, $question); + + return $question; } @@ -91,7 +94,7 @@ public function importQuestionImages( ImportContext $context, CollectQuestionImages $pipe, ): void { - $import_dir = dirname($context->get('import_file')) . DIRECTORY_SEPARATOR . 'expDir_1'; + $import_dir = dirname($context->get(UploadValidationStage::COMPONENT_IMPORT_FILE)) . DIRECTORY_SEPARATOR . 'expDir_1'; $question_files = new QuestionFiles(); foreach ($pipe->getEnvelopes() as $from_path => $envelope) { @@ -114,6 +117,49 @@ public function importQuestionImages( } } + /** + * Finalize the imported question pages by replacing the old question ids with the new question ids. + */ + public function finalizeQuestionPages(ilImportMapping $mapping): void + { + $page_mappings = $mapping->getMappingsOfEntity('components/ILIAS/COPage', 'pg'); + + foreach ($page_mappings as $old => $new) { + if (!preg_match('/^qpl:(\d+)$/', $old, $old_matches)) { + continue; + } + $old_question_id = $old_matches[1]; + + if (!preg_match('/^qpl:(\d+)$/', $new, $new_matches)) { + continue; + } + $new_question_id = $new_matches[1]; + + $page = new ilAssQuestionPage((int) $new_question_id); + $xml = preg_replace( + '/il_\d+_qst_' . preg_quote($old_question_id, '/') . '\b/', + "il__qst_{$new_question_id}", + $page->getXMLContent() + ); + if ($xml === null) { + continue; + } + $page->setXMLContent($xml); + + $parent_obj_id = $mapping->getMapping( + $this->component, + 'question_assignment', + $new_question_id + ); + if ($parent_obj_id !== null) { + $page->setParentId((int) $parent_obj_id); + } + + $page->updateFromXML(); + unset($page); + } + } + private function storeQuestionMappings( ilImportMapping $mapping, int $old_question_id, @@ -147,8 +193,8 @@ private function storeQuestionMappings( $mapping->addMapping( 'components/ILIAS/COPage', 'pg', - "{$this->parent_type}:{$old_question_id}", - "{$this->parent_type}:{$new_question_id}" + "qpl:{$old_question_id}", + "qpl:{$new_question_id}" ); } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php index 0e8641e8f2d1..ea73b375c871 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php @@ -35,6 +35,11 @@ */ class UploadValidationStage implements ImportStage { + public const string FILE_TO_IMPORT = 'file_to_import'; + public const string COMPONENT_IMPORT_FILE = 'component_import_file'; + public const string IMPORT_BASE_DIR = 'import_base_dir'; + public const string INSTALL_ID = 'install_id'; + private const string IMPORT_TEMP_DIR = CLIENT_DATA_DIR . DIRECTORY_SEPARATOR . 'temp'; public function __construct( @@ -61,7 +66,7 @@ public function getDescription(): ?string public function process(ImportContext $context): StageResult { - $file_to_import = $context->get('file_to_import'); + $file_to_import = $context->get(self::FILE_TO_IMPORT); if ( $file_to_import === null || !is_file($file_to_import) @@ -88,14 +93,14 @@ public function process(ImportContext $context): StageResult } return StageResult::advance( - $context->with('import_file', $import_base_dir . DIRECTORY_SEPARATOR . $export_file['path']) - ->with('import_base_dir', $import_base_dir) - ->with('install_id', $manifest->getInstallId()) + $context->with(self::COMPONENT_IMPORT_FILE, $import_base_dir . DIRECTORY_SEPARATOR . $export_file['path']) + ->with(self::IMPORT_BASE_DIR, $import_base_dir) + ->with(self::INSTALL_ID, $manifest->getInstallId()) ); } public static function getInstallId(ImportContext $context): int { - return intval($context->get('install_id')); + return intval($context->get(self::INSTALL_ID)); } } From 9e8bab9cc427e7d67cdc25b886218366f807b828 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Tue, 28 Apr 2026 09:48:59 +0200 Subject: [PATCH 24/66] fix(test): normalizing bugs and test mapping --- .../ILIAS/Test/classes/class.ilTestSkillLevelThreshold.php | 2 +- .../src/ExportImport/Normalizer/ParticipantNormalizer.php | 2 +- .../Test/src/ExportImport/Normalizer/ilObjTestNormalizer.php | 2 +- .../Test/src/Settings/MainSettings/SettingsFinishing.php | 4 ++-- .../Test/src/Settings/MainSettings/SettingsIntroduction.php | 2 +- .../src/Settings/ScoreReporting/SettingsResultSummary.php | 2 +- .../Foundation/Serializing/SimpleXMLSerializer.php | 4 ++++ 7 files changed, 11 insertions(+), 7 deletions(-) diff --git a/components/ILIAS/Test/classes/class.ilTestSkillLevelThreshold.php b/components/ILIAS/Test/classes/class.ilTestSkillLevelThreshold.php index 26f40ff71821..a79f3e32a87f 100755 --- a/components/ILIAS/Test/classes/class.ilTestSkillLevelThreshold.php +++ b/components/ILIAS/Test/classes/class.ilTestSkillLevelThreshold.php @@ -227,7 +227,7 @@ public function toNormalized(Transformations $tt): Transformation { return $tt->custom()->transformation(fn(): array => [ 'id' => $tt->normalize(new Id($this->getSkillLevelId(), 'skill_level')), - 'test_id' => $tt->normalize(new Id($this->getTestId(), 'test')), + 'test_id' => $tt->normalize(new Id($this->getTestId(), 'tst')), 'skill_base_id' => $tt->normalize(new Id($this->getSkillBaseId(), 'skill_base')), 'skill_tref_id' => $tt->normalize(new Id($this->getSkillTrefId(), 'skill_tref')), 'threshold' => $this->getThreshold(), diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php index 27d6dc082585..3927a248f290 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php @@ -51,7 +51,7 @@ public function normalize($value): array|float|bool|int|string|null return [ 'user_id' => $this->tt->normalize(new Id($value->getUserId(), 'user')), 'active_id' => $this->tt->normalize(new Id($value->getActiveId(), 'participant')), - 'test_id' => $this->tt->normalize(new Id($value->getTestId(), 'test')), + 'test_id' => $this->tt->normalize(new Id($value->getTestId(), 'tst')), 'anonymous_id' => $value->getAnonymousId(), 'firstname' => $value->getFirstname(), 'lastname' => $value->getLastname(), diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ilObjTestNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/ilObjTestNormalizer.php index 083dd5c228fa..35b05ae5d353 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/ilObjTestNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/ilObjTestNormalizer.php @@ -43,7 +43,7 @@ public function normalize($value): array|float|bool|int|string|null } $normalized = parent::normalize($value); - $normalized['test_id'] = $this->tt->normalize(new Id($value->getTestId(), 'test')); + $normalized['test_id'] = $this->tt->normalize(new Id($value->getTestId(), 'tst')); return $normalized; } diff --git a/components/ILIAS/Test/src/Settings/MainSettings/SettingsFinishing.php b/components/ILIAS/Test/src/Settings/MainSettings/SettingsFinishing.php index 3334bf32c421..9f7d6ded7c67 100755 --- a/components/ILIAS/Test/src/Settings/MainSettings/SettingsFinishing.php +++ b/components/ILIAS/Test/src/Settings/MainSettings/SettingsFinishing.php @@ -270,8 +270,8 @@ public static function fromExport(array $data): static return new self( (bool) $data['enable_examview'], (bool) $data['showfinalstatement'], - $data['concluding_remarks_page_id'], - RedirectionModes::from($data['redirection_mode']), + $data['concluding_remarks_page_id'] !== null ? (int) $data['concluding_remarks_page_id'] : null, + RedirectionModes::from((int) $data['redirection_mode']), $data['redirection_url'] ); } diff --git a/components/ILIAS/Test/src/Settings/MainSettings/SettingsIntroduction.php b/components/ILIAS/Test/src/Settings/MainSettings/SettingsIntroduction.php index 6ef0bded0d66..223c9ba82cc5 100755 --- a/components/ILIAS/Test/src/Settings/MainSettings/SettingsIntroduction.php +++ b/components/ILIAS/Test/src/Settings/MainSettings/SettingsIntroduction.php @@ -122,7 +122,7 @@ public static function fromExport(array $data): static { return new self( (bool) $data['intro_enabled'], - $data['introduction_page_id'], + $data['introduction_page_id'] !== null ? (int) $data['introduction_page_id'] : null, (bool) $data['conditions_checkbox_enabled'], ); } diff --git a/components/ILIAS/Test/src/Settings/ScoreReporting/SettingsResultSummary.php b/components/ILIAS/Test/src/Settings/ScoreReporting/SettingsResultSummary.php index be067e6d41a9..96357dd151d4 100755 --- a/components/ILIAS/Test/src/Settings/ScoreReporting/SettingsResultSummary.php +++ b/components/ILIAS/Test/src/Settings/ScoreReporting/SettingsResultSummary.php @@ -292,7 +292,7 @@ public function toExport(): array public static function fromExport(array $data): static { return (new self()) - ->withScoreReporting(ScoreReportingTypes::from($data['score_reporting'])) + ->withScoreReporting(ScoreReportingTypes::from((int) $data['score_reporting'])) ->withReportingDate($data['reporting_date'] ? new \DateTimeImmutable($data['reporting_date']) : null) ->withShowGradingStatusEnabled((bool) $data['show_grading_status']) ->withShowGradingMarkEnabled((bool) $data['show_grading_mark']) diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php index 86b27a4c5b57..8df8f8e8b15c 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php @@ -107,6 +107,10 @@ public function group(string $name, callable $callback): void public function append(string $name, array $data): void { $this->writer->startElement($this->formatName($name)); + if (count($data) === 0) { + $this->writer->writeAttribute('type', 'empty-array'); + } + $this->appendRecursive($data); $this->writer->endElement(); } From a2198f300cd5c00919c51a38f696fdb1595b726a Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Tue, 28 Apr 2026 11:53:02 +0200 Subject: [PATCH 25/66] feat(test): introduce user import resolving --- .../src/ExportImport/Import/PersistStage.php | 87 +++++++++++++++++++ .../Import/UserImportResolver.php | 86 ++++++++++++++++++ .../Test/src/ExportImport/TestExporter.php | 24 +++-- .../GlobalSettings/UserIdentfiers.php | 11 +++ 4 files changed, 202 insertions(+), 6 deletions(-) create mode 100644 components/ILIAS/Test/src/ExportImport/Import/PersistStage.php create mode 100644 components/ILIAS/Test/src/ExportImport/Import/UserImportResolver.php diff --git a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php new file mode 100644 index 000000000000..a155eef0e21b --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php @@ -0,0 +1,87 @@ +lng->txt('qpl_import_step_persist'); + } + + public function getDescription(): ?string + { + return ''; + } + + public function process(ImportContext $context): StageResult + { + $component_import_dir = dirname($context->get(UploadValidationStage::COMPONENT_IMPORT_FILE)); + $mappings_file = "{$component_import_dir}/mappings.xml"; + if (!file_exists($mappings_file) || !is_file($mappings_file)) { + return StageResult::error($context, $this->lng->txt('obj_import_file_error')); + } + + $deserializer = new SimpleXMLDeserializer()->open(file_get_contents($mappings_file)); + $deserializer->addHandler('mappings', function (array $mappings) use (&$context) { + $context = $context->with('mappings', $mappings); + }); + $deserializer->process(); + + $importer = new ilImport($this->requested_ref_id); + $importer->importObject( + null, + $context->get(UploadValidationStage::FILE_TO_IMPORT), + basename($context->get(UploadValidationStage::FILE_TO_IMPORT)), + 'tst', + 'components/ILIAS/Test', + true, + ); + + // Context is updated by the TestImporter so we need to reload it + return StageResult::complete($this->session->getContext()); + } +} diff --git a/components/ILIAS/Test/src/ExportImport/Import/UserImportResolver.php b/components/ILIAS/Test/src/ExportImport/Import/UserImportResolver.php new file mode 100644 index 000000000000..90e35fa8c3e0 --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Import/UserImportResolver.php @@ -0,0 +1,86 @@ + $users + * @return array + */ + public function resolve(UserIdentifiers $criteria, array $users): array + { + $this->log->debug("Resolving user import for criteria {$criteria->value}"); + + if (!$this->db->tableColumnExists('usr_data', $criteria->value)) { + $this->log->error("User criteria field {$criteria->value} does not exist in usr_data table, using anonymous user ID for all users"); + + return array_fill_keys(array_keys($users), ANONYMOUS_USER_ID); + } + + $in_clause = $this->db->in( + $criteria->value, + array_values($users), + false, + $criteria->getColumnType() + ); + $query = $this->db->query("SELECT usr_id, {$criteria->value} AS identifier FROM usr_data WHERE {$in_clause}"); + + $db_mapping = []; + foreach ($this->db->fetchAll($query) as $row) { + $db_mapping[$row['identifier']] = $row['usr_id']; + } + + $mapping = []; + foreach ($users as $original_id => $identifier) { + if (isset($db_mapping[$identifier])) { + $this->log->debug("User identifier {$identifier} found, mapping user {$original_id} to {$db_mapping[$identifier]}"); + $mapping[$original_id] = $db_mapping[$identifier]; + } else { + $this->log->warning("User identifier {$identifier} not found for user {$original_id}, using anonymous user ID"); + $mapping[$original_id] = ANONYMOUS_USER_ID; + } + } + + return $mapping; + } + + /** + * @param array $user_mapping + */ + public function store(array $user_mapping, ilImportMapping $import_mapping): void + { + foreach ($user_mapping as $original_id => $user_id) { + $import_mapping->addMapping('tst', 'user', (string) $original_id, (string) $user_id); + } + } +} diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index 3534fdfb99e3..08292615aa93 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -26,6 +26,7 @@ use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\ObjectId; use ILIAS\Data\UUID\Factory as UUIDFactory; +use ILIAS\Filesystem\Stream\Streams; use ILIAS\Language\Language; use ILIAS\ResourceStorage\Services as IRSS; use ILIAS\Taxonomy\DomainService as Taxonomy; @@ -41,6 +42,7 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Serializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\CollectResources; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\SimpleXMLSerializer; use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ilTree; @@ -232,10 +234,10 @@ public function write(ExportState $state): void } - $this->exportMappings( + $this->writeMappings( $state->collector(), $state->transformations(), - $state->serializer() + $state ); } @@ -250,7 +252,7 @@ private function exportObject( $obj_id = $collector->getObjectId()->toInt(); $state->addDependency('components/ILIAS/ILIASObject', 'common', [$obj_id]); - $state->addDependency('components/ILIAS/MetaData', 'qpl', ["{$obj_id}:0:qpl"]); + $state->addDependency('components/ILIAS/MetaData', 'tst', ["{$obj_id}:0:tst"]); $state->addDependency( 'components/ILIAS/Taxonomy', 'tax', @@ -286,12 +288,15 @@ private function exportQuestions( Serializer $serializer, ExportState $state ): void { + $question_properties = $collector->getTestQuestionProperties(); + foreach ($collector->getQuestionObjects() as $question) { $normalized = [ ... $transformations->normalize($question), 'feedback' => $transformations->normalize( $collector->getFeedback($question) - ) + ), + 'sequence' => $question_properties[$question->getId()]->getSequenceInformation()->getPlaceInSequence(), ]; if ($question instanceof assFormulaQuestion) { @@ -349,11 +354,13 @@ private function exportResults( } } - private function exportMappings( + private function writeMappings( TestCollector $collector, Transformations $transformations, - Serializer $serializer + ExportState $state ): void { + $serializer = new SimpleXMLSerializer()->open('memory'); + $serializer->createDocument('Test Export Mappings'); $serializer->startGroup('mappings'); $user_ids = $transformations->context(CollectUserIds::class)->getIds(); @@ -366,5 +373,10 @@ private function exportMappings( ); $serializer->endGroup('mappings'); + + $state->writer()->writeFileByStream( + Streams::ofString($serializer->write()), + "{$state->path()->getPathToComponentDirInContainer()}/mappings.xml" + ); } } diff --git a/components/ILIAS/Test/src/Settings/GlobalSettings/UserIdentfiers.php b/components/ILIAS/Test/src/Settings/GlobalSettings/UserIdentfiers.php index ac8d187627fb..9910191d975b 100755 --- a/components/ILIAS/Test/src/Settings/GlobalSettings/UserIdentfiers.php +++ b/components/ILIAS/Test/src/Settings/GlobalSettings/UserIdentfiers.php @@ -27,4 +27,15 @@ enum UserIdentifiers: string case EMAIL = 'email'; case MATRICULATION = 'matriculation'; case EXTERNAL_ACCOUNT = 'ext_account'; + + public function getColumnType(): string + { + return match ($this) { + self::USER_ID => \ilDBConstants::T_INTEGER, + self::LOGIN => \ilDBConstants::T_TEXT, + self::EMAIL => \ilDBConstants::T_TEXT, + self::MATRICULATION => \ilDBConstants::T_TEXT, + self::EXTERNAL_ACCOUNT => \ilDBConstants::T_TEXT, + }; + } } From 883b7d3af347bd1948a2c2d71154a439cf9418fd Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Wed, 29 Apr 2026 11:12:59 +0200 Subject: [PATCH 26/66] fix(test): add export for missing test result dependencies --- .../ExportImport/Envelopes/ManualFeedback.php | 6 +- .../ExportImport/Envelopes/QuestionResult.php | 88 +++++++++++++ .../src/ExportImport/Envelopes/Solution.php | 4 +- .../ExportImport/Envelopes/WorkingTime.php | 2 +- .../Normalizer/ilTestSequenceNormalizer.php | 123 ++++++++++++++++++ .../Test/src/ExportImport/TestCollector.php | 68 +++++++++- .../Test/src/ExportImport/TestExporter.php | 7 +- 7 files changed, 288 insertions(+), 10 deletions(-) create mode 100644 components/ILIAS/Test/src/ExportImport/Envelopes/QuestionResult.php create mode 100644 components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSequenceNormalizer.php diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/ManualFeedback.php b/components/ILIAS/Test/src/ExportImport/Envelopes/ManualFeedback.php index 5531a916bd81..123c743a9032 100644 --- a/components/ILIAS/Test/src/ExportImport/Envelopes/ManualFeedback.php +++ b/components/ILIAS/Test/src/ExportImport/Envelopes/ManualFeedback.php @@ -59,13 +59,13 @@ public function toArray(Transformations $tt): array public static function fromArray(array $value, Transformations $tt): static { return new self( - $tt->denormalize($value['active_id'], Id::class)->getId(), - $tt->denormalize($value['question_id'], Id::class)->getId(), + $tt->denormalize($value['active_id'], Id::class), + $tt->denormalize($value['question_id'], Id::class), $tt->int($value['attempt']), $tt->string($value['feedback']), $tt->bool($value['finalized_evaluation']), $tt->int($value['finalized_timestamp']), - $tt->denormalize($value['finalized_by'], Id::class)->getId(), + $tt->denormalize($value['finalized_by'], Id::class), ); } diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/QuestionResult.php b/components/ILIAS/Test/src/ExportImport/Envelopes/QuestionResult.php new file mode 100644 index 000000000000..dfe9877771ca --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Envelopes/QuestionResult.php @@ -0,0 +1,88 @@ + $tt->normalize($this->active_id), + 'question_id' => $tt->normalize($this->question_id), + 'attempt' => $this->attempt, + 'points' => $this->points, + 'answered' => $this->answered, + 'manual' => $this->manual, + 'step' => $this->step, + 'timestamp' => $this->timestamp, + ]; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $value, Transformations $tt): static + { + return new self( + $tt->denormalize($value['active_id'], Id::class), + $tt->denormalize($value['question_id'], Id::class), + $tt->int($value['attempt']), + $tt->float($value['points']), + $tt->bool($value['answered']), + $tt->bool($value['manual']), + $tt->nullableInt($value['step']), + $tt->int($value['timestamp']), + ); + } + + public static function fromRow(array $row): static + { + return new self( + new Id($row['active_fi'], 'participant'), + new Id($row['question_fi'], 'question'), + (int) $row['pass'], + (float) $row['points'], + (bool) $row['answered'], + (bool) $row['manual'], + $row['step'] !== null ? (int) $row['step'] : null, + (int) $row['tstamp'], + ); + } +} diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/Solution.php b/components/ILIAS/Test/src/ExportImport/Envelopes/Solution.php index 2bdc8d75ba32..5ef1747f5ceb 100644 --- a/components/ILIAS/Test/src/ExportImport/Envelopes/Solution.php +++ b/components/ILIAS/Test/src/ExportImport/Envelopes/Solution.php @@ -65,8 +65,8 @@ public function toArray(Transformations $tt): array public static function fromArray(array $value, Transformations $tt): static { return new self( - $tt->denormalize($value['active_id'], Id::class)->getId(), - $tt->denormalize($value['question_id'], Id::class)->getId(), + $tt->denormalize($value['active_id'], Id::class), + $tt->denormalize($value['question_id'], Id::class), $tt->int($value['attempt']), $tt->nullableFloat($value['points']), $tt->int($value['timestamp']), diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/WorkingTime.php b/components/ILIAS/Test/src/ExportImport/Envelopes/WorkingTime.php index 17deb3dabbd3..97aeeeb4dc9b 100644 --- a/components/ILIAS/Test/src/ExportImport/Envelopes/WorkingTime.php +++ b/components/ILIAS/Test/src/ExportImport/Envelopes/WorkingTime.php @@ -53,7 +53,7 @@ public function toArray(Transformations $tt): array public static function fromArray(array $value, Transformations $tt): static { return new self( - $tt->denormalize($value['active_id'], Id::class)->getId(), + $tt->denormalize($value['active_id'], Id::class), $tt->int($value['attempt']), $tt->string($value['started']), $tt->string($value['finished']), diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSequenceNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSequenceNormalizer.php new file mode 100644 index 000000000000..00762cb1e893 --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSequenceNormalizer.php @@ -0,0 +1,123 @@ + + */ +#[Normalizes(ilTestSequence::class)] +class ilTestSequenceNormalizer implements Normalizer +{ + private readonly ilDBInterface $db; + private readonly GeneralQuestionPropertiesRepository $repository; + + public function __construct( + private readonly Transformations $tt, + Container $dic, + TestDIC $local_dic, + ) { + $this->db = $dic->database(); + $this->repository = $local_dic['question.general_properties.repository']; + } + + /** + * @inheritDoc + */ + public function normalize($value): array|float|bool|int|string|null + { + if (!$value instanceof ilTestSequence) { + throw new NormalizingException('Invalid value', $value); + } + + return [ + 'active_id' => $this->tt->normalize(new Id($value->getActiveId(), 'participant')), + 'attempt' => $value->getPass(), + 'sequence' => $value->sequencedata['sequence'], + 'postponed' => $this->normalizeQuestions($value->sequencedata['postponed'] ?? []), + 'hidden' => $this->normalizeQuestions($value->sequencedata['hidden'] ?? []), + 'ans_opt_confirmed' => $value->isAnsweringOptionalQuestionsConfirmed(), + 'optional_questions' => $this->normalizeQuestions($value->getOptionalQuestions()), + ]; + } + + private function normalizeQuestions(array $questions): array + { + return array_map( + fn(int $question_id) => $this->tt->normalize(new Id($question_id, 'question')), + $questions + ); + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $value, string $type): ilTestSequence + { + if ($type !== ilTestSequence::class) { + throw new NormalizingException("Invalid type for ilTestSequence: {$type}"); + } + + $active_id = $this->tt->denormalize($value['active_id'], Id::class)->getId(); + $attempt = $this->tt->int($value['attempt']); + + $sequence = new ilTestSequence($this->db, $active_id, $attempt, $this->repository); + + $sequence->setAnsweringOptionalQuestionsConfirmed($this->tt->bool($value['ans_opt_confirmed'])); + $sequence->sequencedata['sequence'] = $this->denormalizeSequence($value['sequence']); + $sequence->sequencedata['postponed'] = $this->denormalizeQuestions($value['postponed']); + $sequence->sequencedata['hidden'] = $this->denormalizeQuestions($value['hidden']); + + $optional = $this->denormalizeQuestions($value['optional_questions']); + foreach ($optional as $question_id) { + $sequence->setQuestionOptional($question_id); + } + + return $sequence; + } + + private function denormalizeSequence(array $normalized): array + { + $sequence = []; + foreach ($normalized as $key => $item) { + $sequence[$this->tt->int($key)] = $this->tt->int($item); + } + return $sequence; + } + + private function denormalizeQuestions(array $normalized): array + { + return array_map( + fn(mixed $item) => $this->tt->denormalize($item, Id::class)->getId(), + $normalized + ); + } +} diff --git a/components/ILIAS/Test/src/ExportImport/TestCollector.php b/components/ILIAS/Test/src/ExportImport/TestCollector.php index af4044e6aa64..94add6193096 100644 --- a/components/ILIAS/Test/src/ExportImport/TestCollector.php +++ b/components/ILIAS/Test/src/ExportImport/TestCollector.php @@ -27,6 +27,7 @@ use ILIAS\Data\ObjectId; use ILIAS\Language\Language; use ILIAS\Test\ExportImport\Envelopes\ManualFeedback; +use ILIAS\Test\ExportImport\Envelopes\QuestionResult; use ILIAS\Test\ExportImport\Envelopes\Solution; use ILIAS\Test\ExportImport\Envelopes\WorkingTime; use ILIAS\Test\Logging\TestLogger; @@ -35,13 +36,16 @@ use ILIAS\Test\Questions\Properties\Repository as QuestionsRepository; use ILIAS\Test\Results\Data\Repository as ResultsRepository; use ILIAS\Test\Settings\GlobalSettings\UserIdentifiers; +use ILIAS\Test\TestManScoringDoneHelper; use ILIAS\TestQuestionPool\ExportImport\Export\CollectsQuestions; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\DataCollector; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ilObjTest; use ilTestQuestionSetConfigFactory; use ilTestRandomQuestionSetSourcePoolDefinitionFactory; use ilTestRandomQuestionSetSourcePoolDefinitionList; +use ilTestSequence; use ilTestSkillLevelThreshold; use ilTestSkillLevelThresholdList; use ilTree; @@ -53,6 +57,8 @@ class TestCollector implements DataCollector { use CollectsQuestions; + private readonly TestManScoringDoneHelper $manual_scoring; + /** @var array $questions */ private ?array $questions = null; private ?ilObjTest $test = null; @@ -71,6 +77,7 @@ public function __construct( private readonly ilComponentRepository $component_repository, private readonly ObjectId $object_id ) { + $this->manual_scoring = new TestManScoringDoneHelper($this->db); } private function database(): ilDBInterface @@ -122,7 +129,7 @@ public function getUserMapping(array $user_ids): array $mapping[$user_id] = $user_id; } } else { - $in_clause = $this->db->in('usr_id', $user_ids, false, 'integer'); + $in_clause = $this->db->in('usr_id', $user_ids, false, ilDBConstants::T_INTEGER); $query = $this->db->query("SELECT usr_id, {$export_identifier->value} FROM usr_data WHERE {$in_clause}"); foreach ($this->db->fetchAll($query) as $row) { @@ -245,18 +252,42 @@ public function getParticipantsIds(): array return $this->participants; } + /** + * @param list $participant_ids + * @return array + */ + public function getAdditionalParticipantData(array $participant_ids): array + { + $in_clause = $this->db->in('active_id', $participant_ids, false, ilDBConstants::T_INTEGER); + $query = $this->db->query("SELECT active_id AS mapping_id, submittimestamp, lastindex, objective_container, start_lock FROM tst_active WHERE {$in_clause}"); + + $data = []; + while ($row = $this->db->fetchAssoc($query)) { + $data[$row['mapping_id']] = $row; + } + + return $data; + } + /* Results */ public function getResults(int $participant_id): array { + $attempt_results = $this->results_repository->getTestAttemptResults($participant_id); return [ - 'results' => $this->results_repository->getTestResult($participant_id), - 'attempts' => $this->results_repository->getTestAttemptResults($participant_id), + 'sequences' => $this->getSequences($participant_id, array_keys($attempt_results)), 'solutions' => $this->getSolutions($participant_id), + 'results' => $this->getQuestionResults($participant_id), + 'attempts' => $attempt_results, + 'test_result' => $this->results_repository->getTestResult($participant_id), 'working_times' => $this->getWorkingTimes($participant_id), 'manual_feedback' => $this->getManualFeedback($participant_id), + 'manual_scoring' => [ + 'active_id' => new Id($participant_id, 'participant'), + 'done' => $this->manual_scoring->isDone($participant_id), + ], ]; } @@ -277,6 +308,23 @@ public function getSolutions(int $participant_id): array ); } + /** + * @return list + */ + public function getQuestionResults(int $participant_id): array + { + $query = $this->db->queryF( + "SELECT * FROM tst_test_result WHERE active_fi = %s", + [ilDBConstants::T_INTEGER], + [$participant_id] + ); + + return array_map( + fn(array $row): QuestionResult => QuestionResult::fromRow($row), + $this->db->fetchAll($query) + ); + } + /** * @return list */ @@ -310,4 +358,18 @@ public function getManualFeedback(int $participant_id): array $this->db->fetchAll($query) ); } + + /** + * @param list $attempts + * @return list + */ + public function getSequences(int $participant_id, array $attempts): array + { + foreach ($attempts as $attempt) { + $test_sequence = new ilTestSequence($this->db, $participant_id, $attempt, $this->general_questions_repository); + $test_sequence->loadFromDb(); + $sequences[] = $test_sequence; + } + return $sequences; + } } diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index 08292615aa93..a6dba3fe9adf 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -334,8 +334,13 @@ private function exportParticipants( Transformations $transformations, Serializer $serializer ): void { + $additional_data = $collector->getAdditionalParticipantData($collector->getParticipantsIds()); + foreach ($collector->getParticipants() as $participant) { - $serializer->append('participant', $transformations->normalize($participant)); + $serializer->append('participant', [ + ...$transformations->normalize($participant), + ...$additional_data[$participant->getActiveId()], + ]); } } From ed442cd636e805ba2ceffb3c5122a4f38c05d689 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Wed, 29 Apr 2026 12:13:30 +0200 Subject: [PATCH 27/66] fix(qpl): generate thumbnails for question images --- .../Import/QuestionPoolImporter.php | 7 +- .../ExportImport/Import/QuestionsImporter.php | 79 ++++++++++++++++--- .../TestQuestionPool/src/QuestionPoolDIC.php | 6 +- 3 files changed, 77 insertions(+), 15 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php index 00162cfeb235..3b9761cc446d 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php @@ -113,7 +113,12 @@ function (array $assignments) use ($tt, &$context): void { $deserializer->process(); // Copy the question images from the temporary import directory to the question pool directory - $this->questions_importer->importQuestionImages($mapping, $context, $images_pipe); + $this->questions_importer->importQuestionImages( + $context->get('pool_obj_id'), + $mapping, + $context, + $images_pipe + ); return $context; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php index 8cb86d4176e1..a3d84b6531a0 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php @@ -27,6 +27,12 @@ use ilAssQuestionPage; use ilCtrl; use ilDBInterface; +use ILIAS\Filesystem\Filesystem; +use ILIAS\Filesystem\Filesystems; +use ILIAS\Filesystem\Stream\FileStream; +use ILIAS\Filesystem\Stream\Streams; +use ILIAS\Filesystem\Util\Convert\ImageOutputOptions; +use ILIAS\Filesystem\Util\Convert\Images; use ILIAS\Language\Language; use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; @@ -34,19 +40,25 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Envelopes\Feedback; use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; -use ILIAS\TestQuestionPool\Questions\Files\QuestionFiles; use ilImportMapping; use ilUnitConfigurationRepository; +use Psr\Log\LoggerInterface; class QuestionsImporter { + private readonly Filesystem $filesystem; + public function __construct( private readonly string $component, private readonly string $parent_type, private readonly ilCtrl $ctrl, private readonly ilDBInterface $database, private readonly Language $language, + private readonly LoggerInterface $log, + private readonly Images $image_converter, + Filesystems $filesystems, ) { + $this->filesystem = $filesystems->web(); } public function importQuestion( @@ -90,31 +102,72 @@ public function importQuestion( public function importQuestionImages( + int $parent_obj_id, ilImportMapping $mapping, ImportContext $context, CollectQuestionImages $pipe, ): void { - $import_dir = dirname($context->get(UploadValidationStage::COMPONENT_IMPORT_FILE)) . DIRECTORY_SEPARATOR . 'expDir_1'; + $import_dir = dirname($context->get(UploadValidationStage::COMPONENT_IMPORT_FILE)) . '/expDir_1'; + + foreach ($pipe->getEnvelopes() as $filename => $envelope) { + $source_path = $import_dir . DIRECTORY_SEPARATOR . $filename; + if (!file_exists($source_path)) { + $this->log->error("Imported image path does not exist: {$source_path}"); + continue; + } - $question_files = new QuestionFiles(); - foreach ($pipe->getEnvelopes() as $from_path => $envelope) { - $question_id = $mapping->getMapping($this->component, 'question', (string) $envelope->getQuestionId()); - if (!$question_id) { + $image_base_path = $this->buildImageBasePath($parent_obj_id, $envelope, $mapping); + $image_path = "{$image_base_path}/{$envelope->getFilename()}"; + if ($this->filesystem->has($image_path)) { + $this->log->warning("Question image already exists: {$image_path}, skipping"); continue; } - $base_dir = $envelope->getType() === QuestionImage::TYPE_ANSWER - ? $question_files->buildImagePath($question_id, $context->get('pool_obj_id')) - : $question_files->buildSolutionPath($question_id, $context->get('pool_obj_id')); + $input_stream = Streams::ofReattachableResource(fopen($source_path, 'rb')); + $this->filesystem->writeStream($image_path, $input_stream); + $this->log->debug("Imported question image: {$source_path} -> {$image_path}"); - if (!file_exists($base_dir)) { - mkdir($base_dir, 0755, true); + $thumbnail = $this->generateThumbnail($input_stream); + if (!$thumbnail) { + continue; } - copy($import_dir . DIRECTORY_SEPARATOR . $from_path, $base_dir . $envelope->getFilename()); + $thumbnail_path = "{$image_base_path}/thumb.{$envelope->getFilename()}"; + $this->filesystem->writeStream($thumbnail_path, $thumbnail); + $this->log->debug("Generated question image thumbnail: {$thumbnail_path}"); - //TODO: generate thumbnail + $thumbnail->close(); + $input_stream->close(); + } + } + + private function buildImageBasePath(int $parent_obj_id, QuestionImage $envelope, ilImportMapping $mapping): ?string + { + $question_id = $mapping->getMapping($this->component, 'question', (string) $envelope->getQuestionId()); + if (!$question_id) { + $this->log->error("Question ID mapping not found for {$envelope->getQuestionId()}"); + return null; } + + $subdir = $envelope->getType() === QuestionImage::TYPE_SOLUTION ? 'solution' : 'images'; + return "assessment/{$parent_obj_id}/{$question_id}/{$subdir}"; + } + + + private function generateThumbnail(FileStream $image_stream): ?FileStream + { + $converter = $this->image_converter->thumbnail( + $image_stream, + 100, + new ImageOutputOptions()->withFormat(ImageOutputOptions::FORMAT_KEEP), + ); + + if (!$converter->isOK()) { + $this->log->error("Could not generate thumbnail: {$converter->getThrowableIfAny()?->getMessage()}"); + return null; + } + + return $converter->getStream(); } /** diff --git a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php index bedf56db471c..9a23022146f8 100755 --- a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php +++ b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php @@ -28,6 +28,7 @@ use ILIAS\TestQuestionPool\ExportImport\Import\QuestionPoolImporter; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionsImporter; use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter; +use ilLoggerFactory; use Pimple\Container as PimpleContainer; use ILIAS\DI\Container as ILIASContainer; use ILIAS\TestQuestionPool\Questions\SuggestedSolution\SuggestedSolutionsDatabaseRepository; @@ -105,7 +106,10 @@ protected static function buildDIC(ILIASContainer $DIC): self 'qpl', $DIC->ctrl(), $DIC->database(), - $DIC->language() + $DIC->language(), + ilLoggerFactory::getLogger('exp')->getLogger(), + $DIC->fileConverters()->images(), + $DIC->filesystem() ); $dic['exportimport.importer'] = static fn($c): QuestionPoolImporter => new QuestionPoolImporter( From 7be2608633f28850076f1cc307bcd1d54351098e Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Wed, 29 Apr 2026 12:14:39 +0200 Subject: [PATCH 28/66] fix(filesystem): preserve uri and mode in ReattachableStream across detach calls Stream::detach() nullifies uri and _mode, which made ReattachableStream unable to reopen the resource afterwards. Override detach() to store both values before the parent clears them, so assertStreamAttached() can reliably reattach. --- .../src/Stream/ReattachableStream.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/components/ILIAS/Filesystem/src/Stream/ReattachableStream.php b/components/ILIAS/Filesystem/src/Stream/ReattachableStream.php index 085e899b9456..78d83c7a520f 100644 --- a/components/ILIAS/Filesystem/src/Stream/ReattachableStream.php +++ b/components/ILIAS/Filesystem/src/Stream/ReattachableStream.php @@ -25,15 +25,23 @@ */ class ReattachableStream extends Stream { - /** - * Checks if the stream is attached to the wrapper. - * If not, the stream is reattached. - */ + private ?string $reattach_uri = null; + private ?string $reattach_mode = null; + + #[\Override] + public function detach() + { + $this->reattach_uri = $this->uri; + $this->reattach_mode = $this->_mode; + + return parent::detach(); + } + #[\Override] protected function assertStreamAttached(): void { if ($this->stream === null) { - $this->stream = fopen($this->uri, $this->_mode); + $this->stream = fopen($this->reattach_uri, $this->reattach_mode); } } } From 95427aa0360359ec7eaa9251d75e73ded2fcb906 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Wed, 29 Apr 2026 14:49:49 +0200 Subject: [PATCH 29/66] feat(test): implement test importer class and integrate into xml import system --- .../ILIAS/Test/classes/class.ilObjTestGUI.php | 123 ++-- .../Test/classes/class.ilTestImporter.php | 349 ++--------- .../classes/class.ilTestLegacyImporter.php | 349 +++++++++++ .../src/ExportImport/Import/PersistStage.php | 7 +- .../src/ExportImport/Import/TestImporter.php | 578 ++++++++++++++++++ .../Test/src/ExportImport/TestExporter.php | 4 +- components/ILIAS/Test/src/TestDIC.php | 45 ++ .../Normalizer/ResourceNormalizer.php | 2 +- .../Normalizing/Pipes/CollectResources.php | 43 +- 9 files changed, 1124 insertions(+), 376 deletions(-) create mode 100755 components/ILIAS/Test/classes/class.ilTestLegacyImporter.php create mode 100644 components/ILIAS/Test/src/ExportImport/Import/TestImporter.php diff --git a/components/ILIAS/Test/classes/class.ilObjTestGUI.php b/components/ILIAS/Test/classes/class.ilObjTestGUI.php index dfe4933928e0..45c7034315b2 100755 --- a/components/ILIAS/Test/classes/class.ilObjTestGUI.php +++ b/components/ILIAS/Test/classes/class.ilObjTestGUI.php @@ -18,6 +18,7 @@ declare(strict_types=1); +use ILIAS\Test\ExportImport\Import\PersistStage; use ILIAS\Test\Logging\AdditionalInformationGenerator; use ILIAS\Skill\Service\SkillUsageService; use ILIAS\Test\Results\Data\Repository as TestResultRepository; @@ -60,6 +61,14 @@ use ILIAS\Test\Results\Presentation\Factory as ResultsPresentationFactory; use ILIAS\Test\ExportImport\Factory as ExportImportFactory; use ILIAS\Test\ExportImport\DBRepository as ExportRepository; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportStageRunner; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResultType; +use ILIAS\TestQuestionPool\ExportImport\Import\CleanupStage; +use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; +use ILIAS\TestQuestionPool\ExportImport\Import\QuestionSelectionStage; +use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ILIAS\TestQuestionPool\RequestDataCollector as QPLRequestDataCollector; use ILIAS\TestQuestionPool\Import\TestQuestionsImportTrait; @@ -195,6 +204,7 @@ class ilObjTestGUI extends ilObjectGUI implements ilCtrlBaseClassInterface, ilDe protected TaxonomyService $taxonomy; protected GUIFactory $gui_factory; protected SkillUsageService $skill_usage_service; + protected ImportSessionRepository $import_session_repository; protected bool $create_question_mode; @@ -248,6 +258,7 @@ public function __construct() $this->mark_schema_factory = $local_dic['marks.factory']; $this->additional_information_generator = $local_dic['logging.information_generator']; $this->personal_settings_exporter = $local_dic['settings.personal_templates.exporter']; + $this->import_session_repository = $local_dic['exportimport.session']; $ref_id = 0; if ($this->testrequest->hasRefId() && is_numeric($this->testrequest->getRefId())) { @@ -1377,81 +1388,76 @@ public function runObject() $this->ctrl->redirectByClass([ilRepositoryGUI::class, self::class, ilInfoScreenGUI::class]); } + protected function importFile(string $file_to_import, string $path_to_uploaded_file_in_temp_dir): void { - list($subdir, $importdir, $xmlfile, $qtifile) = $this->buildImportDirectoriesFromImportFile($file_to_import); + $this->import_session_repository->clear(); - $options = (new ILIAS\Filesystem\Util\Archive\UnzipOptions()) - ->withZipOutputPath($this->getImportTempDirectory()); + $context = new ImportContext([UploadValidationStage::FILE_TO_IMPORT => $file_to_import]); + $this->import_session_repository->setContext($context); + $this->import_session_repository->setCurrentStageIndex(0); - $unzip = $this->archives->unzip(Streams::ofResource(fopen($file_to_import, 'r')), $options); - $unzip->extract(); - - if (!is_file($qtifile)) { - ilFileUtils::delDir($importdir); - $this->deleteUploadedImportFile($path_to_uploaded_file_in_temp_dir); - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('tst_import_non_ilias_zip'), true); - } - $qtiParser = new ilQTIParser($importdir, $qtifile, ilQTIParser::IL_MO_VERIFY_QTI, 0, [], [], true); - try { - $qtiParser->startParsing(); - } catch (ilSaxParserException) { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('import_file_not_valid'), true); - $this->ctrl->redirect($this, 'create'); - } - $founditems = $qtiParser->getFoundItems(); - - $complete = 0; - $incomplete = 0; - foreach ($founditems as $item) { - if ($item["type"] !== '') { - $complete++; - } else { - $incomplete++; - } - } + $this->ctrl->redirectByClass(self::class, 'processImport'); + } - if (count($founditems) && $complete == 0) { - ilFileUtils::delDir($importdir); - $this->deleteUploadedImportFile($path_to_uploaded_file_in_temp_dir); - $this->tpl->setOnScreenMessage('info', $this->lng->txt('qpl_import_non_ilias_files')); + public function processImportObject(): void + { + $permission = $this->creation_mode ? 'create' : 'read'; + if (!$this->checkPermissionBool($permission, '', $this->object->getType())) { + $this->redirectAfterMissingWrite(); return; } - ilSession::set('path_to_import_file', $file_to_import); - ilSession::set('path_to_uploaded_file_in_temp_dir', $path_to_uploaded_file_in_temp_dir); + $runner = $this->buildImportStageRunner(); + $result = $runner->run(); - if ($qtiParser->getQuestionSetType() !== ilObjTest::QUESTION_SET_TYPE_FIXED - || file_exists($this->buildResultsFilePath($importdir, $subdir)) - || $founditems === []) { - $this->importVerifiedFileObject(true); - return; - } + switch ($result->type) { + case StageResultType::INTERACT: + $this->tpl->setContent( + $this->ui_renderer->render($result->components) + ); + break; - $form = $this->buildImportQuestionsSelectionForm( - 'importVerifiedFile', - $importdir, - $qtifile, - $file_to_import, - $path_to_uploaded_file_in_temp_dir - ); + case StageResultType::ADVANCE: + $this->ctrl->redirectByClass(self::class, 'processImport'); + break; - if ($form === null) { - return; + case StageResultType::ERROR: + $this->tpl->setOnScreenMessage('failure', $result->error_message, true); + break; + + case StageResultType::COMPLETE: + $this->tpl->setOnScreenMessage('success', $this->lng->txt('object_imported'), true); + $this->ctrl->setParameterByClass(ilObjTestGUI::class, 'ref_id', $result->context->get('test_ref_id')); + $this->ctrl->redirectByClass(self::class, self::SHOW_QUESTIONS_CMD); + break; } + } + + private function buildImportStageRunner(): ImportStageRunner + { + $form_action = $this->ctrl->getFormActionByClass(self::class, 'processImport'); - $panel = $this->ui_factory->panel()->standard( - $this->lng->txt('import_tst'), + return new ImportStageRunner( [ - $this->ui_factory->legacy()->content($this->lng->txt('qpl_import_verify_found_questions')), - $form - ] + new UploadValidationStage($this->archives, $this->lng, 'components/ILIAS/Test'), + new DetectLegacyImportStage(), + new QuestionSelectionStage( + $this->lng, + $this->component_factory, + $this->ui_factory, + $this->request, + $form_action, + $this->lng->txt('import_tst') + ), + new PersistStage($this->lng, $this->requested_ref_id, $this->import_session_repository), + ], + $this->import_session_repository, + new CleanupStage() ); - $this->tpl->setContent($this->ui_renderer->render($panel)); - $this->tpl->printToStdout(); - exit; } + /** * save object * @access public @@ -2406,6 +2412,7 @@ public function addLocatorItems(): void ); break; case "importFile": + case "processImport": case "cloneAll": case "importVerifiedFile": case "cancelImport": diff --git a/components/ILIAS/Test/classes/class.ilTestImporter.php b/components/ILIAS/Test/classes/class.ilTestImporter.php index 6e78e48129a6..7ee0c283a3f0 100755 --- a/components/ILIAS/Test/classes/class.ilTestImporter.php +++ b/components/ILIAS/Test/classes/class.ilTestImporter.php @@ -18,38 +18,38 @@ declare(strict_types=1); -use ILIAS\ResourceStorage\Services as ResourceStorage; -use ILIAS\TestQuestionPool\Import\TestQuestionsImportTrait; +use ILIAS\Data\ReferenceId; +use ILIAS\Test\ExportImport\Import\TestImporter; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\SimpleXMLDeserializer; +use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; use ILIAS\Test\TestDIC; -use ILIAS\Test\Logging\TestLogger; -/** - * Importer class for files - * - * @author Stefan Meyer - * @version $Id$ - * @ingroup components\ILIASLearningModule - */ class ilTestImporter extends ilXmlImporter { - use TestQuestionsImportTrait; - /** - * @var array - */ - public static $finallyProcessedTestsRegistry = []; - - private readonly TestLogger $logger; - private readonly ilDBInterface $db; - private readonly ResourceStorage $irss; + protected readonly ImportSessionRepository $session; + protected readonly TestImporter $importer; + protected readonly ilTestLegacyImporter $legacy_importer; public function __construct() { - global $DIC; - $this->logger = TestDIC::dic()['logging.logger']; - $this->db = $DIC['ilDB']; - $this->irss = $DIC['resource_storage']; - parent::__construct(); + $this->legacy_importer = new ilTestLegacyImporter(); + + $local_dic = TestDIC::dic(); + $this->session = $local_dic['exportimport.session']; + $this->importer = $local_dic['exportimport.importer']; + } + + public function init(): void + { + /** @var ilCOPageImportConfig $co_config */ + $co_config = $this->imp->getConfig('components/ILIAS/COPage'); + $co_config->setUpdateIfExists(true); + + $this->legacy_importer->setImport($this->getImport()); + $this->legacy_importer->setImportDirectory($this->getImportDirectory()); + $this->legacy_importer->init(); } public function importXmlRepresentation( @@ -58,298 +58,35 @@ public function importXmlRepresentation( string $a_xml, ilImportMapping $a_mapping ): void { - $results_file_path = null; - if ($new_id = (int) $a_mapping->getMapping('components/ILIAS/Container', 'objs', $a_id)) { - // container content - $new_obj = ilObjectFactory::getInstanceByObjId((int) $new_id, false); - $new_obj->saveToDb(); - - [$importdir, $xmlfile, $qtifile] = $this->buildImportDirectoriesFromContainerImport( - $this->getImportDirectory() - ); - $selected_questions = []; - } else { - // single object - $new_id = (int) $a_mapping->getMapping('components/ILIAS/Test', 'tst', 'new_id'); - $new_obj = ilObjectFactory::getInstanceByObjId($new_id, false); - - $selected_questions = ilSession::get('tst_import_selected_questions') ?? []; - [$subdir, $importdir, $xmlfile, $qtifile] = $this->buildImportDirectoriesFromImportFile( - ilSession::get('path_to_import_file') - ); - $results_file_path = $this->buildResultsFilePath($importdir, $subdir); - ilSession::clear('tst_import_selected_questions'); - } - - $new_obj->loadFromDb(); - - if (!file_exists($xmlfile)) { - $this->logger->error(__METHOD__ . ': Cannot find xml definition: ' . $xmlfile); - return; - } - if (!file_exists($qtifile)) { - $this->logger->error(__METHOD__ . ': Cannot find xml definition: ' . $qtifile); + // Check if forward to legacy importer is needed + $context = $this->session->getContext(); + if (DetectLegacyImportStage::isLegacyImport($context)) { + $this->legacy_importer->setInstallId($this->getInstallId()); + $this->legacy_importer->setInstallUrl($this->getInstallUrl()); + $this->legacy_importer->setSchemaVersion($this->getSchemaVersion()); + $this->legacy_importer->setSkipEntities($this->getSkipEntities()); + $this->legacy_importer->importXmlRepresentation($a_entity, $a_id, $a_xml, $a_mapping); return; } - // start parsing of QTI files - $qti_parser = new ilQTIParser( - $importdir, - $qtifile, - ilQTIParser::IL_MO_PARSE_QTI, - $new_obj->getId(), - $selected_questions, - $a_mapping->getAllMappings() - ); - $qti_parser->setTestObject($new_obj); - $qti_parser->startParsing(); - $new_obj = $qti_parser->getTestObject(); - - // import page data - $question_page_parser = new ilQuestionPageParser( - $new_obj, - $xmlfile, - $importdir - ); - $question_page_parser->setQuestionMapping($qti_parser->getImportMapping()); - $question_page_parser->startParsing(); - - $a_mapping = $this->addTaxonomyAndQuestionsMapping($qti_parser->getQuestionIdMapping(), $new_obj->getId(), $a_mapping); - - if ($new_obj->isRandomTest()) { - $this->importRandomQuestionSetConfig($new_obj, $xmlfile, $a_mapping); - } - - if ($results_file_path !== null && file_exists($results_file_path)) { - $results = new ilTestResultsImportParser($results_file_path, $new_obj, $this->db, $this->logger, $this->irss); - $results->setQuestionIdMapping($a_mapping->getMappingsOfEntity('components/ILIAS/Test', 'quest')); - $results->setSrcPoolDefIdMapping($a_mapping->getMappingsOfEntity('components/ILIAS/Test', 'rnd_src_pool_def')); - $results->startParsing(); - } - - $new_obj->saveToDb(); // this creates test_fi - $new_obj->update(); // this saves ilObject data - - $this->importSkillLevelThresholds( + $result = $this->importer->import( + new SimpleXMLDeserializer()->open($a_xml), $a_mapping, - $this->importQuestionSkillAssignments($a_mapping, $new_obj, $xmlfile), - $new_obj, - $xmlfile - ); - - $a_mapping->addMapping("components/ILIAS/Test", "tst", (string) $a_id, (string) $new_obj->getId()); - $a_mapping->addMapping( - "components/ILIAS/MetaData", - "md", - $a_id . ":0:tst", - $new_obj->getId() . ":0:tst" - ); - $a_mapping->addMapping( - "components/ILIAS/Tracking", - "obj", - $a_id, - (string) $new_obj->getId() + new ReferenceId($a_mapping->getTargetId()), + $context, ); - } - - public function addTaxonomyAndQuestionsMapping( - array $question_id_mapping, - int $new_obj_id, - ilImportMapping $mapping - ): ilImportMapping { - foreach ($question_id_mapping as $old_question_id => $new_question_id) { - $mapping->addMapping( - 'components/ILIAS/Taxonomy', - 'tax_item', - "tst:quest:{$old_question_id}", - (string) $new_question_id - ); - - $mapping->addMapping( - 'components/ILIAS/Taxonomy', - 'tax_item_obj_id', - "tst:quest:{$old_question_id}", - (string) $new_obj_id - ); - - $mapping->addMapping( - 'components/ILIAS/Test', - 'quest', - (string) $old_question_id, - (string) $new_question_id - ); - } - - return $mapping; + $this->session->setContext($result); } public function finalProcessing(ilImportMapping $a_mapping): void { - $maps = $a_mapping->getMappingsOfEntity("components/ILIAS/Test", "tst"); - - foreach ($maps as $old => $new) { - if ($old == "new_id" || (int) $old <= 0) { - continue; - } - - if (isset(self::$finallyProcessedTestsRegistry[$new])) { - continue; - } - - $test_obj = ilObjectFactory::getInstanceByObjId((int) $new, false); - if ($test_obj->isRandomTest()) { - $this->finalRandomTestTaxonomyProcessing($a_mapping, (string) $old, $new, $test_obj); - } - - self::$finallyProcessedTestsRegistry[$new] = true; - } - } - - protected function finalRandomTestTaxonomyProcessing( - ilImportMapping $mapping, - string $old_tst_obj_id, - string $new_tst_obj_id, - ilObjTest $test_obj - ): void { - $new_tax_ids = $mapping->getMapping( - 'components/ILIAS/Taxonomy', - 'tax_usage_of_obj', - $old_tst_obj_id - ); - - if ($new_tax_ids !== null) { - foreach (explode(':', $new_tax_ids) as $tax_id) { - ilObjTaxonomy::saveUsage((int) $tax_id, (int) $new_tst_obj_id); - } - } - - $src_pool_def_list = new ilTestRandomQuestionSetSourcePoolDefinitionList( - $this->db, - $test_obj, - new ilTestRandomQuestionSetSourcePoolDefinitionFactory( - $this->db, - $test_obj - ) - ); - - $src_pool_def_list->loadDefinitions(); - - foreach ($src_pool_def_list as $definition) { - $mapped_taxonomy_filter = $definition->getMappedTaxonomyFilter(); - if ($mapped_taxonomy_filter === []) { - continue; - } - - $definition->setMappedTaxonomyFilter( - $this->getNewMappedTaxonomyFilter( - $mapping, - $mapped_taxonomy_filter - ) - ); - $definition->saveToDb(); - } - } - - protected function getNewMappedTaxonomyFilter( - ilImportMapping $mapping, - array $mapped_filter - ): array { - $new_mapped_filter = []; - foreach ($mapped_filter as $tax_id => $tax_nodes) { - $new_tax_id = $mapping->getMapping( - 'components/ILIAS/Taxonomy', - 'tax', - (string) $tax_id - ); - - if ($new_tax_id === null) { - continue; - } - - $new_mapped_filter[$new_tax_id] = []; - - foreach ($tax_nodes as $tax_node_id) { - $new_tax_node_id = $mapping->getMapping( - 'components/ILIAS/Taxonomy', - 'tax_tree', - (string) $tax_node_id - ); - - if ($new_tax_node_id === null) { - continue; - } - - $new_mapped_filter[$new_tax_id][] = $new_tax_node_id; - } - } - - return $new_mapped_filter; - } - - public function importRandomQuestionSetConfig( - ilObjTest $test_obj, - ?string $xml_file, - \ilImportMapping $a_mapping - ): void { - $test_obj->questions = []; - $parser = new ilObjTestXMLParser($xml_file); - $parser->setTestOBJ($test_obj); - $parser->setImportMapping($a_mapping); - $parser->startParsing(); - } - - protected function importQuestionSkillAssignments( - ilImportMapping $mapping, - ilObjTest $test_obj, - ?string $xml_file - ): ilAssQuestionSkillAssignmentList { - $parser = new ilAssQuestionSkillAssignmentXmlParser($xml_file); - $parser->startParsing(); - - $importer = new ilAssQuestionSkillAssignmentImporter(); - $importer->setTargetParentObjId($test_obj->getId()); - $importer->setImportInstallationId((int) $this->getInstallId()); - $importer->setImportMappingRegistry($mapping); - $importer->setImportMappingComponent('components/ILIAS/Test'); - $importer->setImportAssignmentList($parser->getAssignmentList()); - - $importer->import(); - - if ($importer->getFailedImportAssignmentList()->assignmentsExist()) { - $qsaImportFails = new ilAssQuestionSkillAssignmentImportFails($test_obj->getId()); - $qsaImportFails->registerFailedImports($importer->getFailedImportAssignmentList()); - - $test_obj->getObjectProperties()->storePropertyIsOnline( - $test_obj->getObjectProperties()->getPropertyIsOnline()->withOffline() - ); + // Check if forward to legacy importer is needed + $context = $this->session->getContext(); + if (DetectLegacyImportStage::isLegacyImport($context)) { + $this->legacy_importer->finalProcessing($a_mapping); + return; } - return $importer->getSuccessImportAssignmentList(); - } - - protected function importSkillLevelThresholds( - ilImportMapping $mapping, - ilAssQuestionSkillAssignmentList $assignment_list, - ilObjTest $test_obj, - ?string $xml_file - ): void { - $parser = new ilTestSkillLevelThresholdXmlParser($xml_file); - $parser->initSkillLevelThresholdImportList(); - $parser->startParsing(); - - $importer = new ilTestSkillLevelThresholdImporter($this->db); - $importer->setTargetTestId($test_obj->getTestId()); - $importer->setImportInstallationId((int) $this->getInstallId()); - $importer->setImportMappingRegistry($mapping); - $importer->setImportedQuestionSkillAssignmentList($assignment_list); - $importer->setImportThresholdList($parser->getSkillLevelThresholdImportList()); - $importer->import(); - - if ($importer->getFailedThresholdImportSkillList()->skillsExist()) { - $sltImportFails = new ilTestSkillLevelThresholdImportFails($test_obj->getId()); - $sltImportFails->registerFailedImports($importer->getFailedThresholdImportSkillList()); - - $test_obj->setOfflineStatus(true); - } + $this->importer->finalize($a_mapping); } } diff --git a/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php b/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php new file mode 100755 index 000000000000..494752bc1602 --- /dev/null +++ b/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php @@ -0,0 +1,349 @@ + + * @version $Id$ + * @ingroup components\ILIASLearningModule + */ +class ilTestLegacyImporter extends ilXmlImporter +{ + use TestQuestionsImportTrait; + /** + * @var array + */ + public static $finallyProcessedTestsRegistry = []; + + private readonly TestLogger $logger; + private readonly ilDBInterface $db; + private readonly ResourceStorage $irss; + + public function __construct() + { + global $DIC; + $this->logger = TestDIC::dic()['logging.logger']; + $this->db = $DIC['ilDB']; + $this->irss = $DIC['resource_storage']; + + parent::__construct(); + } + + public function importXmlRepresentation( + string $a_entity, + string $a_id, + string $a_xml, + ilImportMapping $a_mapping + ): void { + $results_file_path = null; + if ($new_id = (int) $a_mapping->getMapping('components/ILIAS/Container', 'objs', $a_id)) { + // container content + $new_obj = ilObjectFactory::getInstanceByObjId((int) $new_id, false); + $new_obj->saveToDb(); + + [$importdir, $xmlfile, $qtifile] = $this->buildImportDirectoriesFromContainerImport( + $this->getImportDirectory() + ); + $selected_questions = []; + } else { + // single object + $new_id = (int) $a_mapping->getMapping('components/ILIAS/Test', 'tst', 'new_id'); + $new_obj = ilObjectFactory::getInstanceByObjId($new_id, false); + + $selected_questions = ilSession::get('tst_import_selected_questions') ?? []; + [$subdir, $importdir, $xmlfile, $qtifile] = $this->buildImportDirectoriesFromImportFile( + ilSession::get('path_to_import_file') + ); + $results_file_path = $this->buildResultsFilePath($importdir, $subdir); + ilSession::clear('tst_import_selected_questions'); + } + + $new_obj->loadFromDb(); + + if (!file_exists($xmlfile)) { + $this->logger->error(__METHOD__ . ': Cannot find xml definition: ' . $xmlfile); + return; + } + if (!file_exists($qtifile)) { + $this->logger->error(__METHOD__ . ': Cannot find xml definition: ' . $qtifile); + return; + } + + // start parsing of QTI files + $qti_parser = new ilQTIParser( + $importdir, + $qtifile, + ilQTIParser::IL_MO_PARSE_QTI, + $new_obj->getId(), + $selected_questions, + $a_mapping->getAllMappings() + ); + $qti_parser->setTestObject($new_obj); + $qti_parser->startParsing(); + $new_obj = $qti_parser->getTestObject(); + + // import page data + $question_page_parser = new ilQuestionPageParser( + $new_obj, + $xmlfile, + $importdir + ); + $question_page_parser->setQuestionMapping($qti_parser->getImportMapping()); + $question_page_parser->startParsing(); + + $a_mapping = $this->addTaxonomyAndQuestionsMapping($qti_parser->getQuestionIdMapping(), $new_obj->getId(), $a_mapping); + + if ($new_obj->isRandomTest()) { + $this->importRandomQuestionSetConfig($new_obj, $xmlfile, $a_mapping); + } + + if ($results_file_path !== null && file_exists($results_file_path)) { + $results = new ilTestResultsImportParser($results_file_path, $new_obj, $this->db, $this->logger, $this->irss); + $results->setQuestionIdMapping($a_mapping->getMappingsOfEntity('components/ILIAS/Test', 'quest')); + $results->setSrcPoolDefIdMapping($a_mapping->getMappingsOfEntity('components/ILIAS/Test', 'rnd_src_pool_def')); + $results->startParsing(); + } + + $new_obj->saveToDb(); // this creates test_fi + $new_obj->update(); // this saves ilObject data + + $this->importSkillLevelThresholds( + $a_mapping, + $this->importQuestionSkillAssignments($a_mapping, $new_obj, $xmlfile), + $new_obj, + $xmlfile + ); + + $a_mapping->addMapping("components/ILIAS/Test", "tst", (string) $a_id, (string) $new_obj->getId()); + $a_mapping->addMapping( + "components/ILIAS/MetaData", + "md", + $a_id . ":0:tst", + $new_obj->getId() . ":0:tst" + ); + } + + public function addTaxonomyAndQuestionsMapping( + array $question_id_mapping, + int $new_obj_id, + ilImportMapping $mapping + ): ilImportMapping { + foreach ($question_id_mapping as $old_question_id => $new_question_id) { + $mapping->addMapping( + 'components/ILIAS/Taxonomy', + 'tax_item', + "tst:quest:{$old_question_id}", + (string) $new_question_id + ); + + $mapping->addMapping( + 'components/ILIAS/Taxonomy', + 'tax_item_obj_id', + "tst:quest:{$old_question_id}", + (string) $new_obj_id + ); + + $mapping->addMapping( + 'components/ILIAS/Test', + 'quest', + (string) $old_question_id, + (string) $new_question_id + ); + } + + return $mapping; + } + + public function finalProcessing(ilImportMapping $a_mapping): void + { + $maps = $a_mapping->getMappingsOfEntity("components/ILIAS/Test", "tst"); + + foreach ($maps as $old => $new) { + if ($old == "new_id" || (int) $old <= 0) { + continue; + } + + if (isset(self::$finallyProcessedTestsRegistry[$new])) { + continue; + } + + $test_obj = ilObjectFactory::getInstanceByObjId((int) $new, false); + if ($test_obj->isRandomTest()) { + $this->finalRandomTestTaxonomyProcessing($a_mapping, (string) $old, $new, $test_obj); + } + + self::$finallyProcessedTestsRegistry[$new] = true; + } + } + + protected function finalRandomTestTaxonomyProcessing( + ilImportMapping $mapping, + string $old_tst_obj_id, + string $new_tst_obj_id, + ilObjTest $test_obj + ): void { + $new_tax_ids = $mapping->getMapping( + 'components/ILIAS/Taxonomy', + 'tax_usage_of_obj', + $old_tst_obj_id + ); + + if ($new_tax_ids !== null) { + foreach (explode(':', $new_tax_ids) as $tax_id) { + ilObjTaxonomy::saveUsage((int) $tax_id, (int) $new_tst_obj_id); + } + } + + $src_pool_def_list = new ilTestRandomQuestionSetSourcePoolDefinitionList( + $this->db, + $test_obj, + new ilTestRandomQuestionSetSourcePoolDefinitionFactory( + $this->db, + $test_obj + ) + ); + + $src_pool_def_list->loadDefinitions(); + + foreach ($src_pool_def_list as $definition) { + $mapped_taxonomy_filter = $definition->getMappedTaxonomyFilter(); + if ($mapped_taxonomy_filter === []) { + continue; + } + + $definition->setMappedTaxonomyFilter( + $this->getNewMappedTaxonomyFilter( + $mapping, + $mapped_taxonomy_filter + ) + ); + $definition->saveToDb(); + } + } + + protected function getNewMappedTaxonomyFilter( + ilImportMapping $mapping, + array $mapped_filter + ): array { + $new_mapped_filter = []; + foreach ($mapped_filter as $tax_id => $tax_nodes) { + $new_tax_id = $mapping->getMapping( + 'components/ILIAS/Taxonomy', + 'tax', + (string) $tax_id + ); + + if ($new_tax_id === null) { + continue; + } + + $new_mapped_filter[$new_tax_id] = []; + + foreach ($tax_nodes as $tax_node_id) { + $new_tax_node_id = $mapping->getMapping( + 'components/ILIAS/Taxonomy', + 'tax_tree', + (string) $tax_node_id + ); + + if ($new_tax_node_id === null) { + continue; + } + + $new_mapped_filter[$new_tax_id][] = $new_tax_node_id; + } + } + + return $new_mapped_filter; + } + + public function importRandomQuestionSetConfig( + ilObjTest $test_obj, + ?string $xml_file, + \ilImportMapping $a_mapping + ): void { + $test_obj->questions = []; + $parser = new ilObjTestXMLParser($xml_file); + $parser->setTestOBJ($test_obj); + $parser->setImportMapping($a_mapping); + $parser->startParsing(); + } + + protected function importQuestionSkillAssignments( + ilImportMapping $mapping, + ilObjTest $test_obj, + ?string $xml_file + ): ilAssQuestionSkillAssignmentList { + $parser = new ilAssQuestionSkillAssignmentXmlParser($xml_file); + $parser->startParsing(); + + $importer = new ilAssQuestionSkillAssignmentImporter(); + $importer->setTargetParentObjId($test_obj->getId()); + $importer->setImportInstallationId((int) $this->getInstallId()); + $importer->setImportMappingRegistry($mapping); + $importer->setImportMappingComponent('components/ILIAS/Test'); + $importer->setImportAssignmentList($parser->getAssignmentList()); + + $importer->import(); + + if ($importer->getFailedImportAssignmentList()->assignmentsExist()) { + $qsaImportFails = new ilAssQuestionSkillAssignmentImportFails($test_obj->getId()); + $qsaImportFails->registerFailedImports($importer->getFailedImportAssignmentList()); + + $test_obj->getObjectProperties()->storePropertyIsOnline( + $test_obj->getObjectProperties()->getPropertyIsOnline()->withOffline() + ); + } + + return $importer->getSuccessImportAssignmentList(); + } + + protected function importSkillLevelThresholds( + ilImportMapping $mapping, + ilAssQuestionSkillAssignmentList $assignment_list, + ilObjTest $test_obj, + ?string $xml_file + ): void { + $parser = new ilTestSkillLevelThresholdXmlParser($xml_file); + $parser->initSkillLevelThresholdImportList(); + $parser->startParsing(); + + $importer = new ilTestSkillLevelThresholdImporter($this->db); + $importer->setTargetTestId($test_obj->getTestId()); + $importer->setImportInstallationId((int) $this->getInstallId()); + $importer->setImportMappingRegistry($mapping); + $importer->setImportedQuestionSkillAssignmentList($assignment_list); + $importer->setImportThresholdList($parser->getSkillLevelThresholdImportList()); + $importer->import(); + + if ($importer->getFailedThresholdImportSkillList()->skillsExist()) { + $sltImportFails = new ilTestSkillLevelThresholdImportFails($test_obj->getId()); + $sltImportFails->registerFailedImports($importer->getFailedThresholdImportSkillList()); + + $test_obj->setOfflineStatus(true); + } + } +} diff --git a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php index a155eef0e21b..23f3b2f7f3c1 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php +++ b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php @@ -30,8 +30,9 @@ use ilImport; /** - * Final stage of the test import process. Imports the test object and all its dependencies using `ilImport`. It will - * delegate the import to the `ilTestImporter` class. + * Final stage of the test import process. Imports the head dependencies (user and resource mappings) and then + * imports the test object and all its dependencies using `ilImport`. It will delegate the import to the + * `ilTestImporter` class. */ class PersistStage implements ImportStage { @@ -71,6 +72,8 @@ public function process(ImportContext $context): StageResult }); $deserializer->process(); + $this->session->setContext($context); + $importer = new ilImport($this->requested_ref_id); $importer->importObject( null, diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php new file mode 100644 index 000000000000..2e16ba56cd50 --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php @@ -0,0 +1,578 @@ +irss); + $id_mapping_pipe = new IdMappingPipe($mapping, 'components/ILIAS/Test'); + $question_images_pipe = new CollectQuestionImages(new UUIDFactory(), $this->data_factory->objId(0)); + + $tt = $this->builder->withAdditionalPipes(append: [$id_mapping_pipe, $question_images_pipe, $resource_pipe])->create(); + + /** @var ilObjTest|null $test_object */ + $test_object = null; + + $deserializer->addHandler( + 'general', + function (array $objects) use ($tt, $mapping, $parent_id, &$test_object): void { + $test_object = $this->importTest( + array_pop($objects), + $tt, + $mapping, + $parent_id + ); + } + ); + + $deserializer->addHandler( + 'settings', + function (array $settings) use ($tt, $mapping, &$test_object): void { + $this->importSettings( + $settings, + $tt, + $mapping, + $test_object + ); + } + ); + + $deserializer->addHandler( + 'questions', + function (array $normalized) use ($tt, $mapping, $context, &$test_object): void { + $this->importQuestions( + $normalized, + $tt, + $mapping, + $context, + $test_object + ); + } + ); + + $deserializer->addHandler( + 'skill_assignments', + function (array $assignments) use ($tt, &$context): void { + $result = $this->skill_importer->import( + $assignments, + UploadValidationStage::getInstallId($context), + $tt, + ); + $context = $context->with('skill_assignments', $result); + } + ); + + $deserializer->addHandler( + 'participants', + function (array $participants) use ($tt, $mapping): void { + $this->importParticipants( + $participants, + $tt, + $mapping, + ); + } + ); + + $deserializer->addHandler( + 'results', + function (array $results) use ($tt): void { + $this->importGroupedResults( + $results, + $tt, + ); + } + ); + + $this->log->info('Importing users and resources mappings...'); + $this->importMappings($mapping, $resource_pipe, $context); + $this->log->info('...Finished importing users and resources mappings'); + + $this->log->info('Importing test export file...'); + $deserializer->process(); + $this->log->info('...Finished importing test export file'); + + $this->log->info('Importing question images...'); + $this->questions_importer->importQuestionImages( + $test_object->getId(), + $mapping, + $context, + $question_images_pipe + ); + $this->log->info('...Finished importing question images'); + + $this->log->info("Finished importing test {$test_object->getTestId()} (Test ID), {$test_object->getId()} (Object ID)"); + return $context->with('test_obj_id', $test_object->getId())->with('test_ref_id', $test_object->getRefId()); + } + + /** + * Finalize the import after all dependencies have been imported. + * It will replace the old question ids with the new question ids in the test pages. + */ + public function finalize(ilImportMapping $mapping): void + { + $this->log->info('Finalizing test import...'); + $this->questions_importer->finalizeQuestionPages($mapping); + $this->log->info('...Finished finalizing test'); + } + + + private function importMappings( + ilImportMapping $mapping, + CollectResources $resource_pipe, + ImportContext $context + ): void { + $mappings = $context->get('mappings'); + if (count($mappings) < 2) { + throw new RuntimeException('Invalid mappings: Expected at least 2 mappings, got ' . count($mappings)); + } + [$user_mapping, $resource_mapping] = $mappings; + + $this->log->info('Importing user mappings...'); + $user_resolver = new UserImportResolver($this->database, $this->log); + $imported_users = $user_resolver->resolve( + UserIdentifiers::from($user_mapping['identifier']), + $user_mapping['mapping'] + ); + $user_resolver->store($imported_users, $mapping); + $this->log->info('...Finished importing user mappings'); + + $this->log->info('Importing resources and storing mappings...'); + $import_dir = dirname($context->get(UploadValidationStage::COMPONENT_IMPORT_FILE)) . '/expDir_1'; + foreach ($resource_mapping as $resource) { + $clean_id = str_replace(['-', '_'], '', $resource['id']); + $resource_path = "$import_dir/resources/{$clean_id}.{$resource['suffix']}"; + if (!file_exists($resource_path)) { + $this->log->error("Imported resource path does not exist: {$resource_path}, skipping"); + continue; + } + + $new_id = $this->irss->manage()->stream( + Streams::ofResource(fopen($resource_path, 'rb')), + new assFileUploadStakeholder(), + $resource['title'] + ); + $resource_pipe->storeMapping($resource['id'], $new_id); + $this->log->debug("Imported resource: {$resource_path} -> {$new_id->serialize()}"); + } + $this->log->info('...Finished importing resources and storing mappings'); + } + + private function importTest( + array $normalized, + Transformations $tt, + ilImportMapping $mapping, + ReferenceId $parent_id + ): ilObjTest { + $test_object = $tt->denormalize($normalized, ilObjTest::class); + $old_obj_id = $test_object->getId(); + $old_test_id = $test_object->getTestId(); + + $test_object->setTestId(-1); + $test_object->setTitle("{$test_object->getTitle()} (Imported)"); //TODO: Remove after testing + $new_obj_id = $test_object->create(); + $test_object->saveToDb(true); + $this->log->debug("Created new test object: {$old_test_id} -> {$test_object->getTestId()} (Test ID), {$old_obj_id} -> {$new_obj_id} (Object ID)"); + + $test_object->createReference(); + $test_object->putInTree($parent_id->toInt()); + $test_object->setPermissions($parent_id->toInt()); + $this->log->debug("Stored test object in tree: {$parent_id->toInt()} (Parent Ref) -> {$test_object->getRefId()} (Test Ref)"); + + $mapping->addMapping('components/ILIAS/Test', 'tst', (string) $old_test_id, (string) $test_object->getTestId()); + $mapping->addMapping('components/ILIAS/Test', 'object', (string) $old_obj_id, (string) $new_obj_id); + $mapping->addMapping('components/ILIAS/MetaData', 'md', "{$old_obj_id}:0:tst", "{$new_obj_id}:0:tst"); + + return $test_object; + } + + private function importSettings( + array $list, + Transformations $tt, + ilImportMapping $mapping, + ilObjTest $test_object + ): void { + $settings_id = $test_object->getMainSettings()->getId(); + + $main_settings = $tt->denormalize($list[0], MainSettings::class)->withId($settings_id); + $scoring_settings = $tt->denormalize($list[1], ScoreSettings::class)->withId($settings_id); + $mark_schema = $tt->denormalize($list[2], MarkSchema::class)->withTestId($test_object->getTestId()); + + if ($intro_page_id = $main_settings->getIntroductionSettings()->getIntroductionPageId()) { + $new_page_id = $this->createPage($intro_page_id, $test_object->getId(), $mapping); + $main_settings = $main_settings->withIntroductionSettings( + $main_settings->getIntroductionSettings()->withIntroductionPageId($new_page_id) + ); + $this->log->debug("Imported introduction page: {$intro_page_id} -> {$new_page_id}"); + } + + if ($concluding_page_id = $main_settings->getFinishingSettings()->getConcludingRemarksPageId()) { + $new_page_id = $this->createPage($concluding_page_id, $test_object->getId(), $mapping); + $main_settings = $main_settings->withFinishingSettings( + $main_settings->getFinishingSettings()->withConcludingRemarksPageId($new_page_id) + ); + $this->log->debug("Imported concluding remarks page: {$concluding_page_id} -> {$new_page_id}"); + } + + $test_object->getMainSettingsRepository()->store($main_settings); + $test_object->getScoreSettingsRepository()->store($scoring_settings); + $this->marks_repository->storeMarkSchema($mark_schema); + $this->log->debug("Imported test settings and mark schema: {$settings_id} (Settings ID)"); + } + + private function createPage(int $imported_page_id, int $parent_id, ilImportMapping $mapping): int + { + $page = new ilTestPage(); + $page->setParentId($parent_id); + $page->createPageWithNextId(); + + $mapping->addMapping( + 'components/ILIAS/COPage', + 'pg', + "tst:{$imported_page_id}", + "tst:{$page->getId()}" + ); + + return $page->getId(); + } + + private function importQuestions( + array $list, + Transformations $tt, + ilImportMapping $mapping, + ImportContext $context, + ilObjTest $test_object + ): void { + $selected_questions = QuestionSelectionStage::getSelectedQuestions($context); + + foreach ($list as $normalized) { + $question = $this->questions_importer->importQuestion($normalized, $tt, $mapping, $selected_questions); + + if ($question) { + $sequence = $tt->int($normalized['sequence']); + $test_object->questions[$sequence] = $question->getId(); + $this->log->debug("Stored question {$question->getId()} at sequence {$sequence} in test"); + } + } + + $test_object->saveQuestionsToDb(); + $this->log->debug('Saved test questions to database'); + } + + private function importParticipants(array $list, Transformations $tt, ilImportMapping $mapping): void + { + foreach ($list as $normalized) { + $old_active_id = $tt->denormalize($normalized['active_id'], Id::class)->getId(); + $new_active_id = $this->database->nextId('tst_active'); + $mapping->addMapping('components/ILIAS/Test', 'participant', (string) $old_active_id, (string) $new_active_id); + $this->log->debug("Stored participant/test session mapping: {$old_active_id} -> {$new_active_id}"); + + // TestID, UserID and ActiveID will be replaced by the mapping pipe + $participant = $tt->denormalize($normalized, Participant::class); + + $this->database->insert( + 'tst_active', + [ + 'active_id' => [ilDBConstants::T_INTEGER, $new_active_id], + 'user_fi' => [ilDBConstants::T_INTEGER, $participant->getUserId()], + 'test_fi' => [ilDBConstants::T_INTEGER, $participant->getTestId()], + 'anonymous_id' => [ilDBConstants::T_TEXT, $participant->getAnonymousId()], + 'tries' => [ilDBConstants::T_INTEGER, $participant->getAttempts()], + 'submitted' => [ilDBConstants::T_INTEGER, $participant->getSubmitted() ? 1 : 0], + 'last_finished_pass' => [ilDBConstants::T_INTEGER, $participant->getLastFinishedAttempt()], + 'last_started_pass' => [ilDBConstants::T_INTEGER, $participant->getLastStartedAttempt()], + 'importname' => [ilDBConstants::T_TEXT, "{$participant->getFirstname()} {$participant->getLastname()}"], + 'tstamp' => [ilDBConstants::T_INTEGER, time()], + 'submittimestamp' => [ilDBConstants::T_TIMESTAMP, $tt->nullableString($normalized['submittimestamp'])], + 'lastindex' => [ilDBConstants::T_INTEGER, $tt->nullableInt($normalized['lastindex'])], + 'objective_container' => [ilDBConstants::T_INTEGER, $tt->nullableInt($normalized['objective_container'])], + 'start_lock' => [ilDBConstants::T_TEXT, $tt->nullableString($normalized['start_lock'])], + ] + ); + $this->log->debug("Stored test session in database: {$new_active_id} (Active ID)"); + } + } + + /* + Results + */ + + private function importGroupedResults(array $list, Transformations $tt): void + { + foreach ($list as $set) { + foreach ($set as $name => $data) { + match($name) { + 'sequences' => $this->importTestSequences($data, $tt), + 'solutions' => $this->importSolutions($data, $tt), + 'results' => $this->importQuestionResults($data, $tt), + 'attempts' => $this->importAttemptResults($data, $tt), + 'test_result' => $this->importTestResult($data, $tt), + 'working_times' => $this->importWorkingTimes($data, $tt), + 'manual_feedback' => $this->importManualFeedback($data, $tt), + 'manual_scoring' => $this->importManualScoring($data, $tt), + default => $this->log->warning("Invalid result type: {$name}"), + }; + } + } + } + + private function importTestSequences(array $list, Transformations $tt): void + { + foreach ($list as $normalized) { + // ActiveID and QuestionIDs will be replaced by the mapping pipe + $sequence = $tt->denormalize($normalized, ilTestSequence::class); + $sequence->saveToDb(); + $this->log->debug("Stored test sequence in database: {$sequence->getActiveId()} (Active ID), {$sequence->getPass()} (Pass)"); + } + } + + private function importSolutions(array $list, Transformations $tt): void + { + foreach ($list as $normalized) { + // ActiveID and QuestionID will be replaced by the mapping pipe + $solution = $tt->denormalize($normalized, Solution::class); + + $next_id = $this->database->nextId('tst_solutions'); + $this->database->insert( + 'tst_solutions', + [ + 'solution_id' => [ilDBConstants::T_INTEGER, $next_id], + 'active_fi' => [ilDBConstants::T_INTEGER, $solution->active_id->getId()], + 'question_fi' => [ilDBConstants::T_INTEGER, $solution->question_id->getId()], + 'pass' => [ilDBConstants::T_INTEGER, $solution->attempt], + 'value1' => [ilDBConstants::T_TEXT, $solution->value1 !== null ? (string) $solution->value1 : null], + 'value2' => [ilDBConstants::T_TEXT, $solution->value2], + 'points' => [ilDBConstants::T_FLOAT, $solution->points], + 'step' => [ilDBConstants::T_INTEGER, $solution->step], + 'authorized' => [ilDBConstants::T_INTEGER, $solution->authorized ? 1 : 0], + 'tstamp' => [ilDBConstants::T_INTEGER, time()], + ] + ); + $this->log->debug("Stored solution in database: {$next_id}"); + } + } + + private function importQuestionResults(array $list, Transformations $tt): void + { + foreach ($list as $normalized) { + // ActiveID and QuestionID will be replaced by the mapping pipe + $result = $tt->denormalize($normalized, QuestionResult::class); + + $next_id = $this->database->nextId('tst_test_result'); + $this->database->insert( + 'tst_test_result', + [ + 'test_result_id' => [ilDBConstants::T_INTEGER, $next_id], + 'active_fi' => [ilDBConstants::T_INTEGER, $result->active_id->getId()], + 'question_fi' => [ilDBConstants::T_INTEGER, $result->question_id->getId()], + 'pass' => [ilDBConstants::T_INTEGER, $result->attempt], + 'points' => [ilDBConstants::T_FLOAT, $result->points], + 'manual' => [ilDBConstants::T_INTEGER, $result->manual ? 1 : 0], + 'tstamp' => [ilDBConstants::T_INTEGER, time()], + 'answered' => [ilDBConstants::T_INTEGER, $result->answered ? 1 : 0], + 'step' => [ilDBConstants::T_INTEGER, $result->step], + ] + ); + $this->log->debug("Stored question result in database: {$next_id}"); + } + } + private function importAttemptResults(array $list, Transformations $tt): void + { + foreach ($list as $normalized) { + // ActiveID will be replaced by the mapping pipe + $attempt = $tt->denormalize($normalized, AttemptResult::class); + + $this->database->insert( + 'tst_pass_result', + [ + 'active_fi' => [ilDBConstants::T_INTEGER, $attempt->getActiveId()], + 'pass' => [ilDBConstants::T_INTEGER, $attempt->getAttempt()], + 'maxpoints' => [ilDBConstants::T_FLOAT, $attempt->getMaxPoints()], + 'points' => [ilDBConstants::T_FLOAT, $attempt->getReachedPoints()], + 'questioncount' => [ilDBConstants::T_INTEGER, $attempt->getQuestionCount()], + 'answeredquestions' => [ilDBConstants::T_INTEGER, $attempt->getAnsweredQuestions()], + 'workingtime' => [ilDBConstants::T_INTEGER, $attempt->getWorkingTime()], + 'exam_id' => [ilDBConstants::T_TEXT, $attempt->getExamId()], + 'finalized_by' => [ilDBConstants::T_TEXT, $attempt->getFinalizedBy()], + 'tstamp' => [ilDBConstants::T_INTEGER, time()], + ] + ); + $this->log->debug("Stored attempt result in database: {$attempt->getActiveId()} (Active ID), {$attempt->getAttempt()} (Pass)"); + } + } + + private function importTestResult(?array $normalized, Transformations $tt): void + { + if ($normalized === null) { + $this->log->warning("Missing test result, skipping"); + return; + } + + // ActiveID will be replaced by the mapping pipe + $result = $tt->denormalize($normalized, ParticipantResult::class); + + $this->database->insert( + 'tst_result_cache', + [ + 'active_fi' => [ilDBConstants::T_INTEGER, $result->getActiveId()], + 'pass' => [ilDBConstants::T_INTEGER, $result->getAttempt()], + 'max_points' => [ilDBConstants::T_FLOAT, $result->getMaxPoints()], + 'reached_points' => [ilDBConstants::T_FLOAT, $result->getReachedPoints()], + 'mark_short' => [ilDBConstants::T_TEXT, $result->getMark()->getShortName()], + 'mark_official' => [ilDBConstants::T_TEXT, $result->getMark()->getOfficialName()], + 'passed' => [ilDBConstants::T_INTEGER, $result->isPassed() ? 1 : 0], + 'failed' => [ilDBConstants::T_INTEGER, $result->isFailed() ? 1 : 0], + 'tstamp' => [ilDBConstants::T_INTEGER, time()], + ] + ); + $this->log->debug("Stored test result in database: {$result->getActiveId()} (Active ID), {$result->getAttempt()} (Pass)"); + } + + private function importWorkingTimes(array $list, Transformations $tt): void + { + foreach ($list as $normalized) { + // ActiveID will be replaced by the mapping pipe + $working_time = $tt->denormalize($normalized, WorkingTime::class); + + $next_id = $this->database->nextId('tst_times'); + $this->database->insert( + 'tst_times', + [ + 'times_id' => [ilDBConstants::T_INTEGER, $next_id], + 'active_fi' => [ilDBConstants::T_INTEGER, $working_time->active_id->getId()], + 'pass' => [ilDBConstants::T_INTEGER, $working_time->attempt], + 'started' => [ilDBConstants::T_TIMESTAMP, $working_time->started], + 'finished' => [ilDBConstants::T_TIMESTAMP, $working_time->finished], + 'tstamp' => [ilDBConstants::T_INTEGER, time()], + ] + ); + $this->log->debug("Stored working time in database: {$next_id}"); + } + } + + private function importManualFeedback(array $list, Transformations $tt): void + { + foreach ($list as $normalized) { + // ActiveID, QuestionID and UserID will be replaced by the mapping pipe + $manual_feedback = $tt->denormalize($normalized, ManualFeedback::class); + + $next_id = $this->database->nextId('tst_manual_fb'); + $this->database->insert( + 'tst_manual_fb', + [ + 'manual_feedback_id' => [ilDBConstants::T_INTEGER, $next_id], + 'active_fi' => [ilDBConstants::T_INTEGER, $manual_feedback->active_id->getId()], + 'question_fi' => [ilDBConstants::T_INTEGER, $manual_feedback->question_id->getId()], + 'pass' => [ilDBConstants::T_INTEGER, $manual_feedback->attempt], + 'feedback' => [ilDBConstants::T_TEXT, $manual_feedback->feedback], + 'finalized_evaluation' => [ilDBConstants::T_INTEGER, $manual_feedback->finalized_evaluation ? 1 : 0], + 'finalized_timestamp' => [ilDBConstants::T_INTEGER, $manual_feedback->finalized_timestamp], + 'finalized_by_usr_id' => [ilDBConstants::T_INTEGER, $manual_feedback->finalized_by->getId()], + 'tstamp' => [ilDBConstants::T_INTEGER, time()], + ] + ); + $this->log->debug("Stored manual feedback in database: {$next_id}"); + } + } + + private function importManualScoring( + array $normalized, + Transformations $tt, + ): void { + // ActiveID will be replaced by the mapping pipe + $active_id = $tt->denormalize($normalized['active_id'], Id::class)->getId(); + + new TestManScoringDoneHelper()->setDone($active_id, $tt->bool($normalized['done'])); + $this->log->debug("Stored manual scoring in database: {$normalized['active_id']} (Active ID)"); + } +} diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index a6dba3fe9adf..e44d3e4e3aec 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -226,7 +226,9 @@ public function write(ExportState $state): void } foreach ($resource_pipe->getResources() as $id => $resource) { - $file = "{$id}.{$resource->getCurrentRevision()->getInformation()->getSuffix()}"; + $clean_id = str_replace(['-', '_'], '', $id); + $file = "{$clean_id}.{$resource->getCurrentRevision()->getInformation()->getSuffix()}"; + $state->writer()->writeFilesByResourceId( $id, "{$export_dir}/resources/{$file}" diff --git a/components/ILIAS/Test/src/TestDIC.php b/components/ILIAS/Test/src/TestDIC.php index 671bddc9f78e..f17c778a6658 100755 --- a/components/ILIAS/Test/src/TestDIC.php +++ b/components/ILIAS/Test/src/TestDIC.php @@ -52,10 +52,15 @@ use ILIAS\Test\Results\Presentation\Factory as ResultsPresentationFactory; use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\StateHolder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; +use ILIAS\TestQuestionPool\ExportImport\Import\QuestionsImporter; +use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter; +use ILIAS\Test\ExportImport\Import\TestImporter; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ILIAS\TestQuestionPool\RequestDataCollector as QPLRequestDataCollector; use ILIAS\Data\Factory as DataFactory; use ILIAS\DI\Container as ILIASContainer; +use ilLoggerFactory; use Pimple\Container as PimpleContainer; class TestDIC extends PimpleContainer @@ -235,6 +240,9 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic['exportimport.state_holder'] = static fn($c): StateHolder => new StateHolder(); + $dic['exportimport.session'] = static fn($c): ImportSessionRepository => + new ImportSessionRepository('test'); + $dic['exportimport.builder'] = static fn($c): Builder => new Builder( $DIC, @@ -246,13 +254,50 @@ protected static function buildDIC(ILIASContainer $DIC): self $c['exportimport.builder'], new DataFactory(), $DIC->database(), + $DIC->repositoryTree(), + $DIC->language(), + $c['logging.logger'], + $DIC['component.repository'], $DIC->resourceStorage(), $c['participant.repository'], $c['results.data.repository'], $c['questions.properties.repository'], + $c['question.general_properties.repository'], $DIC->taxonomy()->domain() ); + $dic['exportimport.skill_assignments_importer'] = static fn($c): SkillAssignmentsImporter => + new SkillAssignmentsImporter( + $DIC->skills()->internal()->repo()->getTreeRepo(), + $DIC->skills()->usage(), + (int) $DIC->settings()->get('inst_id', '0') + ); + + $dic['exportimport.questions_importer'] = static fn($c): QuestionsImporter => + new QuestionsImporter( + 'components/ILIAS/Test', + 'tst', + $DIC->ctrl(), + $DIC->database(), + $DIC->language(), + ilLoggerFactory::getLogger('exp')->getLogger(), + $DIC->fileConverters()->images(), + $DIC->filesystem() + ); + $dic['exportimport.importer'] = static fn($c): TestImporter => + new TestImporter( + $c['exportimport.builder'], + $DIC->ctrl(), + $DIC->database(), + ilLoggerFactory::getLogger('exp')->getLogger(), + $DIC->language(), + $DIC->resourceStorage(), + new DataFactory(), + $c['exportimport.questions_importer'], + $c['exportimport.skill_assignments_importer'], + $c['marks.repository'] + ); + $dic['questions.properties.repository'] = static fn($c): TestQuestionsRepository => new TestQuestionsDatabaseRepository( $DIC['ilDB'], diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php index 64e7e3dc2c89..2e8452a92dbb 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php @@ -37,7 +37,7 @@ #[Normalizes(ResourceIdentification::class, StorableResource::class)] class ResourceNormalizer implements Normalizer { - private const string KEY_TYPE = '_type'; + private const string KEY_TYPE = 'type'; private const string TYPE_RID = 'rid'; private const string TYPE_RESOURCE = 'resource'; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php index ea9c3b4b4c8b..8c3c4a76ba74 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php @@ -25,9 +25,11 @@ use ILIAS\ResourceStorage\Services as IRSS; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Pipe; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\NormalizeCarry; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\DenormalizeCarry; /** - * Pipe that collects all resources by their identification during normalization. + * Pipe that collects all resources by their identification during normalization. During denormalization, it will replace + * the resource ids with the mapped new resource ids. */ class CollectResources implements Pipe { @@ -36,11 +38,35 @@ class CollectResources implements Pipe */ private array $resources = []; + /** + * @var array $import_mapping + */ + private array $import_mapping = []; + public function __construct( private readonly IRSS $irss ) { } + /** + * Get all resources collected during normalization. + * + * @return array + */ + public function getResources(): array + { + return $this->resources; + } + + /** + * Store a mapping of an old resource id to a new resource id. + * This is used to replace the old resource ids with the new resource ids during denormalization. + */ + public function storeMapping(string $old_id, ResourceIdentification $new_id): void + { + $this->import_mapping[$old_id] = $new_id; + } + /** * @inheritDoc */ @@ -50,6 +76,12 @@ public function handle(mixed $passable, \Closure $next): mixed $this->handleNormalization($passable->value); } + if ($passable instanceof DenormalizeCarry && $passable->expected === ResourceIdentification::class) { + $passable->setResult( + $this->replaceRid($passable->result()) + ); + } + return $next($passable); } @@ -60,13 +92,8 @@ private function handleNormalization(ResourceIdentification $rid): void $this->resources[$rid->serialize()] = $resource; } - /** - * Get all resources collected during normalization. - * - * @return array - */ - public function getResources(): array + private function replaceRid(ResourceIdentification $rid): ResourceIdentification { - return $this->resources; + return $this->import_mapping[$rid->serialize()] ?? $rid; } } From 2363a79932979e32f57291b82d1e84ae82379386 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Wed, 29 Apr 2026 16:12:42 +0200 Subject: [PATCH 30/66] feat(test): add missing participant relations export/import --- .../Envelopes/AdditionalWorkingTime.php | 72 +++++++++++++++++++ .../src/ExportImport/Import/TestImporter.php | 58 +++++++++++++-- .../Normalizer/ParticipantNormalizer.php | 8 ++- .../Test/src/ExportImport/TestCollector.php | 23 +++++- .../Test/src/ExportImport/TestExporter.php | 30 ++++++-- 5 files changed, 178 insertions(+), 13 deletions(-) create mode 100644 components/ILIAS/Test/src/ExportImport/Envelopes/AdditionalWorkingTime.php diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/AdditionalWorkingTime.php b/components/ILIAS/Test/src/ExportImport/Envelopes/AdditionalWorkingTime.php new file mode 100644 index 000000000000..bb55f0f092fe --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Envelopes/AdditionalWorkingTime.php @@ -0,0 +1,72 @@ + $tt->normalize($this->user_id), + 'test_id' => $tt->normalize($this->test_id), + 'time' => $this->time, + 'timestamp' => $this->timestamp, + ]; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $value, Transformations $tt): static + { + return new self( + $tt->denormalize($value['user_id'], Id::class), + $tt->denormalize($value['test_id'], Id::class), + $tt->int($value['time']), + $tt->int($value['timestamp']), + ); + } + + public static function fromRow(array $row): static + { + return new self( + new Id($row['user_fi'], 'user'), + new Id($row['test_fi'], 'tst'), + (int) $row['additionaltime'], + (int) $row['tstamp'], + ); + } +} diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php index 2e16ba56cd50..784927cd67ef 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php @@ -29,6 +29,7 @@ use ILIAS\Filesystem\Stream\Streams; use ILIAS\Language\Language; use ILIAS\ResourceStorage\Services as IRSS; +use ILIAS\Test\ExportImport\Envelopes\AdditionalWorkingTime; use ILIAS\Test\ExportImport\Envelopes\ManualFeedback; use ILIAS\Test\ExportImport\Envelopes\QuestionResult; use ILIAS\Test\ExportImport\Envelopes\Solution; @@ -174,6 +175,16 @@ function (array $results) use ($tt): void { } ); + $deserializer->addHandler( + 'additional_working_times', + function (array $times) use ($tt): void { + $this->importAdditionalWorkingTimes( + $times, + $tt, + ); + } + ); + $this->log->info('Importing users and resources mappings...'); $this->importMappings($mapping, $resource_pipe, $context); $this->log->info('...Finished importing users and resources mappings'); @@ -352,6 +363,11 @@ private function importQuestions( private function importParticipants(array $list, Transformations $tt, ilImportMapping $mapping): void { foreach ($list as $normalized) { + if ($normalized['active_id'] === null) { + $this->importInvitedParticipant($normalized, $tt); + continue; + } + $old_active_id = $tt->denormalize($normalized['active_id'], Id::class)->getId(); $new_active_id = $this->database->nextId('tst_active'); $mapping->addMapping('components/ILIAS/Test', 'participant', (string) $old_active_id, (string) $new_active_id); @@ -383,6 +399,21 @@ private function importParticipants(array $list, Transformations $tt, ilImportMa } } + private function importInvitedParticipant(array $normalized, Transformations $tt): void + { + // TestID and UserID will be replaced by the mapping pipe + $participant = $tt->denormalize($normalized, Participant::class); + + $this->database->insert('tst_invited_user', [ + 'test_fi' => [ilDBConstants::T_INTEGER, $participant->getTestId()], + 'user_fi' => [ilDBConstants::T_INTEGER, $participant->getUserId()], + 'ip_range_from' => [ilDBConstants::T_TEXT, $participant->getClientIpFrom()], + 'ip_range_to' => [ilDBConstants::T_TEXT, $participant->getClientIpTo()], + 'tstamp' => [ilDBConstants::T_INTEGER, $participant->getInvitationDate()], + ]); + $this->log->debug("Stored invited participant in database: {$participant->getUserId()} (User ID), {$participant->getTestId()} (Test ID)"); + } + /* Results */ @@ -565,14 +596,31 @@ private function importManualFeedback(array $list, Transformations $tt): void } } - private function importManualScoring( - array $normalized, - Transformations $tt, - ): void { + private function importManualScoring(array $normalized, Transformations $tt): void + { // ActiveID will be replaced by the mapping pipe $active_id = $tt->denormalize($normalized['active_id'], Id::class)->getId(); new TestManScoringDoneHelper()->setDone($active_id, $tt->bool($normalized['done'])); - $this->log->debug("Stored manual scoring in database: {$normalized['active_id']} (Active ID)"); + $this->log->debug("Stored manual scoring in database: {$active_id} (Active ID)"); + } + + private function importAdditionalWorkingTimes(array $list, Transformations $tt): void + { + foreach ($list as $normalized) { + // UserID and TestID will be replaced by the mapping pipe + $time = $tt->denormalize($normalized, AdditionalWorkingTime::class); + + $this->database->insert( + 'tst_addtime', + [ + 'additionaltime' => [ilDBConstants::T_INTEGER, $time->time], + 'user_fi' => [ilDBConstants::T_INTEGER, $time->user_id->getId()], + 'test_fi' => [ilDBConstants::T_INTEGER, $time->test_id->getId()], + 'tstamp' => [ilDBConstants::T_TIMESTAMP, $time->timestamp], + ] + ); + $this->log->debug("Stored additional working time in database: {$time->user_id->getId()} (User ID)"); + } } } diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php index 3927a248f290..c3e11a9f982b 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php @@ -50,7 +50,9 @@ public function normalize($value): array|float|bool|int|string|null return [ 'user_id' => $this->tt->normalize(new Id($value->getUserId(), 'user')), - 'active_id' => $this->tt->normalize(new Id($value->getActiveId(), 'participant')), + 'active_id' => $value->getActiveId() !== null + ? $this->tt->normalize(new Id($value->getActiveId(), 'participant')) + : null, 'test_id' => $this->tt->normalize(new Id($value->getTestId(), 'tst')), 'anonymous_id' => $value->getAnonymousId(), 'firstname' => $value->getFirstname(), @@ -84,7 +86,9 @@ public function denormalize(array|float|bool|int|string|null $value, string $typ return new Participant( $this->tt->denormalize($value['user_id'], Id::class)->getId(), - $this->tt->denormalize($value['active_id'], Id::class)->getId(), + $value['active_id'] !== null + ? $this->tt->denormalize($value['active_id'], Id::class)->getId() + : null, $this->tt->denormalize($value['test_id'], Id::class)->getId(), $this->tt->nullableString($value['anonymous_id']), $this->tt->string($value['firstname']), diff --git a/components/ILIAS/Test/src/ExportImport/TestCollector.php b/components/ILIAS/Test/src/ExportImport/TestCollector.php index 94add6193096..3d59f2c50d30 100644 --- a/components/ILIAS/Test/src/ExportImport/TestCollector.php +++ b/components/ILIAS/Test/src/ExportImport/TestCollector.php @@ -26,6 +26,7 @@ use ilDBInterface; use ILIAS\Data\ObjectId; use ILIAS\Language\Language; +use ILIAS\Test\ExportImport\Envelopes\AdditionalWorkingTime; use ILIAS\Test\ExportImport\Envelopes\ManualFeedback; use ILIAS\Test\ExportImport\Envelopes\QuestionResult; use ILIAS\Test\ExportImport\Envelopes\Solution; @@ -246,7 +247,9 @@ public function getParticipantsIds(): array if ($this->participants === null) { $this->participants = []; foreach ($this->getParticipants() as $participant) { - $this->participants[] = $participant->getActiveId(); + if ($participant->getActiveId() !== null) { + $this->participants[] = $participant->getActiveId(); + } } } return $this->participants; @@ -372,4 +375,22 @@ public function getSequences(int $participant_id, array $attempts): array } return $sequences; } + + + /** + * @return list + */ + public function getAdditionalWorkingTimes(): array + { + $query = $this->db->queryF( + "SELECT * FROM tst_addtime WHERE test_fi = %s", + [ilDBConstants::T_INTEGER], + [$this->getTestId()] + ); + + return array_map( + fn(array $row): AdditionalWorkingTime => AdditionalWorkingTime::fromRow($row), + $this->db->fetchAll($query) + ); + } } diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index e44d3e4e3aec..53fdc761ce93 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -160,6 +160,14 @@ public function process(ExportState $state): void $state ) ); + $state->serializer()->group( + 'additional_working_times', + fn() => $this->exportAdditionalWorkingTimes( + $state->collector(), + $state->transformations(), + $state->serializer(), + ) + ); $state->serializer()->group( 'skill_assignments', fn() => $this->exportSkillAssignments( @@ -339,10 +347,12 @@ private function exportParticipants( $additional_data = $collector->getAdditionalParticipantData($collector->getParticipantsIds()); foreach ($collector->getParticipants() as $participant) { - $serializer->append('participant', [ - ...$transformations->normalize($participant), - ...$additional_data[$participant->getActiveId()], - ]); + $normalized = $transformations->normalize($participant); + if ($participant->getActiveId() !== null) { + $normalized = array_merge($normalized, $additional_data[$participant->getActiveId()]); + } + + $serializer->append('participant', $normalized); } } @@ -353,7 +363,7 @@ private function exportResults( ): void { foreach ($collector->getParticipantsIds() as $participant_id) { $serializer->append( - 'results', + 'set', $transformations->normalize( $collector->getResults($participant_id) ) @@ -361,6 +371,16 @@ private function exportResults( } } + private function exportAdditionalWorkingTimes( + TestCollector $collector, + Transformations $transformations, + Serializer $serializer, + ): void { + foreach ($collector->getAdditionalWorkingTimes() as $additional_working_time) { + $serializer->append('time', $transformations->normalize($additional_working_time)); + } + } + private function writeMappings( TestCollector $collector, Transformations $transformations, From 61385f5e3289ef5f363c807a7be1f65110bdda74 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Wed, 29 Apr 2026 16:50:55 +0200 Subject: [PATCH 31/66] refactor(qpl): introduce export/import logging --- .../src/ExportImport/Import/TestImporter.php | 4 +- .../Test/src/ExportImport/TestExporter.php | 5 ++- components/ILIAS/Test/src/TestDIC.php | 10 +++-- .../Foundation/Bridge/XmlExporterBridge.php | 20 ++++++++- .../Normalizing/Pipes/CollectResources.php | 13 +++++- .../Normalizing/Pipes/IdMappingPipe.php | 6 ++- .../Import/QuestionPoolImporter.php | 4 +- .../src/ExportImport/LoggingProvider.php | 41 +++++++++++++++++++ .../TestQuestionPool/src/QuestionPoolDIC.php | 6 ++- 9 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/LoggingProvider.php diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php index 784927cd67ef..b837d9b7e6e1 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php @@ -96,8 +96,8 @@ public function import( ReferenceId $parent_id, ImportContext $context ): ImportContext { - $resource_pipe = new CollectResources($this->irss); - $id_mapping_pipe = new IdMappingPipe($mapping, 'components/ILIAS/Test'); + $resource_pipe = new CollectResources($this->irss, $this->log); + $id_mapping_pipe = new IdMappingPipe($mapping, 'components/ILIAS/Test', $this->log); $question_images_pipe = new CollectQuestionImages(new UUIDFactory(), $this->data_factory->objId(0)); $tt = $this->builder->withAdditionalPipes(append: [$id_mapping_pipe, $question_images_pipe, $resource_pipe])->create(); diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index 53fdc761ce93..7e4c519ce955 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -100,7 +100,10 @@ public function prepare(ExportState $state): void new UUIDFactory(), $object_id ), - new CollectResources($this->irss), + new CollectResources( + $this->irss, + $this->logger + ), ]) ->create(); diff --git a/components/ILIAS/Test/src/TestDIC.php b/components/ILIAS/Test/src/TestDIC.php index f17c778a6658..cf7b12ada9eb 100755 --- a/components/ILIAS/Test/src/TestDIC.php +++ b/components/ILIAS/Test/src/TestDIC.php @@ -56,6 +56,7 @@ use ILIAS\TestQuestionPool\ExportImport\Import\QuestionsImporter; use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter; use ILIAS\Test\ExportImport\Import\TestImporter; +use ILIAS\TestQuestionPool\ExportImport\LoggingProvider; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ILIAS\TestQuestionPool\RequestDataCollector as QPLRequestDataCollector; use ILIAS\Data\Factory as DataFactory; @@ -237,11 +238,14 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC['ilDB'] ); + $dic['exportimport.logging'] = static fn($c): LoggingProvider => + new LoggingProvider(); + $dic['exportimport.state_holder'] = static fn($c): StateHolder => new StateHolder(); $dic['exportimport.session'] = static fn($c): ImportSessionRepository => - new ImportSessionRepository('test'); + new ImportSessionRepository('tst'); $dic['exportimport.builder'] = static fn($c): Builder => new Builder( @@ -280,7 +284,7 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC->ctrl(), $DIC->database(), $DIC->language(), - ilLoggerFactory::getLogger('exp')->getLogger(), + $c['exportimport.logging'](), $DIC->fileConverters()->images(), $DIC->filesystem() ); @@ -289,7 +293,7 @@ protected static function buildDIC(ILIASContainer $DIC): self $c['exportimport.builder'], $DIC->ctrl(), $DIC->database(), - ilLoggerFactory::getLogger('exp')->getLogger(), + $c['exportimport.logging'](), $DIC->language(), $DIC->resourceStorage(), new DataFactory(), diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php index f6767b577d61..546b7c0f0828 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php @@ -52,13 +52,17 @@ private function processExport(): ExportState $state = $this->state_holder->get(); if ($state->getStep()->value < ExportStep::PREPARE->value) { + $this->logger->debug("Preparing export for component {$state->target()->getComponent()}..."); $state->setLogger($this->logger); $this->exporter->prepare($state); + $this->logger->debug("...Finished preparing export"); } if ($state->getStep()->value < ExportStep::PROCESS->value) { + $this->logger->debug("Processing export for component {$state->target()->getComponent()}..."); $state->setSerializer(new SimpleXMLSerializer()->open('memory')); $this->exporter->process($state); + $this->logger->debug("...Finished processing export"); } $this->state_holder->set($state); @@ -77,7 +81,9 @@ private function finalizeExport(): ExportState $state->setPathInfo($this->createPathInfo()); $state->setWriter($this->exp->getExportWriter()); + $this->logger->debug("Writing export for component {$state->target()->getComponent()}..."); $this->exporter->write($state); + $this->logger->debug("...Finished writing export"); } $this->state_holder->set($state); @@ -98,11 +104,23 @@ private function initExportState( ->withClassname(static::class) ->withComponent($component); - return $this->state_holder->create( + $state = $this->state_holder->create( $target, $this->export_handler->consumer()->exportConfig()->collection(), $option ); + + $this->logger->debug(sprintf( + "Export state created for component %s with release %s, type %s, class %s, object ids %s, option %s", + $target->getComponent(), + $target->getTargetRelease(), + $target->getType(), + $target->getClassname(), + implode(', ', $object_ids), + $option + )); + + return $state; } private function createPathInfo(): ExportPath diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php index 8c3c4a76ba74..87529d81b575 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php @@ -26,6 +26,7 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Pipe; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\NormalizeCarry; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\DenormalizeCarry; +use Psr\Log\LoggerInterface; /** * Pipe that collects all resources by their identification during normalization. During denormalization, it will replace @@ -44,7 +45,8 @@ class CollectResources implements Pipe private array $import_mapping = []; public function __construct( - private readonly IRSS $irss + private readonly IRSS $irss, + private readonly LoggerInterface $log ) { } @@ -94,6 +96,13 @@ private function handleNormalization(ResourceIdentification $rid): void private function replaceRid(ResourceIdentification $rid): ResourceIdentification { - return $this->import_mapping[$rid->serialize()] ?? $rid; + $id = $rid->serialize(); + if (isset($this->import_mapping[$id])) { + $this->log->debug("Replaced resource id {$id} with {$this->import_mapping[$id]->serialize()}"); + return $this->import_mapping[$id]; + } else { + $this->log->warning("Unresolved resource id {$id}"); + return $rid; + } } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/IdMappingPipe.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/IdMappingPipe.php index 138c4d6533bc..5645ed580eef 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/IdMappingPipe.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/IdMappingPipe.php @@ -24,6 +24,7 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; use ilImportMapping; +use Psr\Log\LoggerInterface; /** * Resolves imported object references by mapping exported ids to their newly created local ids via `ilImportMapping`. @@ -39,7 +40,8 @@ class IdMappingPipe implements Pipe public function __construct( private readonly ilImportMapping $mapping, - private readonly string $component + private readonly string $component, + private readonly LoggerInterface $log ) { } @@ -61,8 +63,10 @@ public function handle(mixed $passable, \Closure $next): mixed } $passable->setResult(new Id($new_id, $envelope->getObject())); + $this->log->debug("Replaced id {$envelope->getObject()}:{$envelope->getId()} with {$new_id}"); } else { $this->unresolved[] = $envelope; + $this->log->warning("Unresolved id {$envelope->getObject()}:{$envelope->getId()}"); } return $next($passable); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php index 3b9761cc446d..0be8d25cd088 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php @@ -36,6 +36,7 @@ use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; use ilImportMapping; use ilObjQuestionPool; +use Psr\Log\LoggerInterface; /** * Orchestrates the import of a question pool. It uses the Builder to create a pipeline of transformations that are used @@ -49,6 +50,7 @@ public function __construct( private readonly ilCtrl $ctrl, private readonly ilDBInterface $database, private readonly Language $language, + private readonly LoggerInterface $log, private readonly DataFactory $data_factory, private readonly QuestionsImporter $questions_importer, private readonly SkillAssignmentsImporter $skill_importer, @@ -65,7 +67,7 @@ public function import( ReferenceId $parent_id, ImportContext $context ): ImportContext { - $id_mapping_pipe = new IdMappingPipe($mapping, 'components/ILIAS/TestQuestionPool'); + $id_mapping_pipe = new IdMappingPipe($mapping, 'components/ILIAS/TestQuestionPool', $this->log); $images_pipe = new CollectQuestionImages(new Factory(), $this->data_factory->objId(0)); $tt = $this->builder->withAdditionalPipes(append: [$id_mapping_pipe, $images_pipe])->create(); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/LoggingProvider.php b/components/ILIAS/TestQuestionPool/src/ExportImport/LoggingProvider.php new file mode 100644 index 000000000000..98817b30cfc7 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/LoggingProvider.php @@ -0,0 +1,41 @@ +getLogger(); + } + + public function __invoke() { + return $this->getLogger(); + } +} \ No newline at end of file diff --git a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php index 9a23022146f8..7240f58a9597 100755 --- a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php +++ b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php @@ -28,6 +28,7 @@ use ILIAS\TestQuestionPool\ExportImport\Import\QuestionPoolImporter; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionsImporter; use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter; +use ILIAS\TestQuestionPool\ExportImport\LoggingProvider; use ilLoggerFactory; use Pimple\Container as PimpleContainer; use ILIAS\DI\Container as ILIASContainer; @@ -76,6 +77,8 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic['global_test_settings'] = static fn($c): GlobalTestSettings => (new GlobalTestSettingsRepository($DIC['ilSetting'], new \ilSetting('assessment')))->getGlobalSettings(); + $dic['exportimport.logging'] = static fn($c): LoggingProvider => + new LoggingProvider(); $dic['exportimport.builder'] = static fn($c): Builder => new Builder( $DIC, @@ -107,7 +110,7 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC->ctrl(), $DIC->database(), $DIC->language(), - ilLoggerFactory::getLogger('exp')->getLogger(), + $c['exportimport.logging'](), $DIC->fileConverters()->images(), $DIC->filesystem() ); @@ -117,6 +120,7 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC->ctrl(), $DIC->database(), $DIC->language(), + $c['exportimport.logging'](), new DataFactory(), $c['exportimport.questions_importer'], $c['exportimport.skill_assignments_importer'] From 7d8a24f05cecf51c9627fb8ee739ed9a07ac51c9 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Thu, 30 Apr 2026 11:28:09 +0200 Subject: [PATCH 32/66] fix(foundation): improve legacy normalizer resolving --- .../src/ExportImport/Foundation/Builder.php | 67 +++++++++++++++++-- .../Attributes/NormalizesLegacy.php | 12 ++-- .../Setup/NormalizerArtifactObjective.php | 4 +- 3 files changed, 74 insertions(+), 9 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Builder.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Builder.php index ddab7b63f94c..07cf40ee86a4 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Builder.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Builder.php @@ -110,8 +110,6 @@ public function create(): TransformationsContract } if ($this->legacy_version !== null) { - // TODO: Implement semver comparison here? Use ILIAS\Data\Version class? - $registry = $this->buildRegistry($object, $this->legacy_version); $pipeline->pipe(new NormalizingPipe($registry)); $pipeline->pipe(new DenormalizingPipe($registry)); @@ -142,19 +140,80 @@ private function buildRegistry(Transformations $object, string $version = Normal $registry = new Registry(); foreach ($type_map as $type => $normalizer_classes) { - if (!isset($normalizer_classes[$version]) || $registry->hasNormalizer($type)) { + if ($registry->hasNormalizer($type)) { + continue; + } + + $resolved_key = $this->resolveVersionKey($version, $normalizer_classes); + if (!isset($normalizer_classes[$resolved_key])) { continue; } $registry->registerNormalizer( $type, - fn() => $this->createInstance($normalizer_classes[$version], $object) + fn() => $this->createInstance($normalizer_classes[$resolved_key], $object) ); } return $registry; } + /** + * Resolve the best matching version key from the available normalizer versions. + * + * Priority: exact match > nearest concrete version before requested > wildcard (major.*) > default. + * Uses version_compare for comparing ILIAS versions (major.minor, optional suffixes like alpha). + * + * @param array $available_versions + */ + private function resolveVersionKey(string $requested_version, array $available_versions): string + { + if (isset($available_versions[$requested_version])) { + return $requested_version; + } + + if ($requested_version === NormalizerArtifactObjective::DEFAULT_KEY) { + return $requested_version; + } + + $requested_major = strstr($requested_version, '.', true) ?: $requested_version; + + $best_concrete = null; + foreach (array_keys($available_versions) as $version) { + if ($version === NormalizerArtifactObjective::DEFAULT_KEY + || $this->isWildcardVersion($version)) { + continue; + } + + $version_major = strstr($version, '.', true) ?: $version; + if ($version_major !== $requested_major) { + continue; + } + + if (version_compare($version, $requested_version, '<=') + && ($best_concrete === null || version_compare($version, $best_concrete, '>'))) { + $best_concrete = $version; + } + } + + if ($best_concrete !== null) { + return $best_concrete; + } + + foreach (["{$requested_major}.*", $requested_major] as $wildcard) { + if (isset($available_versions[$wildcard])) { + return $wildcard; + } + } + + return NormalizerArtifactObjective::DEFAULT_KEY; + } + + private function isWildcardVersion(string $version): bool + { + return !str_contains($version, '.') || str_ends_with($version, '.*'); + } + /* Factory & Autowiring */ diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/NormalizesLegacy.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/NormalizesLegacy.php index c1ddb635de5f..f7343aa82a4e 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/NormalizesLegacy.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/NormalizesLegacy.php @@ -29,16 +29,20 @@ #[Attribute(Attribute::TARGET_CLASS)] final readonly class NormalizesLegacy { + /** @var list */ + public array $versions; + + /** @var list */ + public array $types; + /** * @param class-string ...$types */ public function __construct( - public string $version, + string|array $version, string ...$types ) { + $this->versions = is_array($version) ? $version : [$version]; $this->types = $types; } - - /** @var list */ - public array $types; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Setup/NormalizerArtifactObjective.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Setup/NormalizerArtifactObjective.php index 25055088ffb5..22c220a0ca28 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Setup/NormalizerArtifactObjective.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Setup/NormalizerArtifactObjective.php @@ -70,7 +70,9 @@ public function build(): Artifact foreach ($attrs as $attr) { $instance = $attr->newInstance(); foreach ($instance->types as $type) { - $type_map[$type][$instance->version] = $class_name; + foreach ($instance->versions as $version) { + $type_map[$type][$version] = $class_name; + } } } } From 75b17509a39f709bb2f4dd1d079d85be1c11c553 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Mon, 4 May 2026 09:18:58 +0200 Subject: [PATCH 33/66] refactor(foundation): introduce XMLFileDeserializer --- .../Test/classes/class.ilTestImporter.php | 4 +- .../src/ExportImport/Import/PersistStage.php | 4 +- .../class.ilTestQuestionPoolImporter.php | 4 +- ...erializer.php => XMLDeserializerTrait.php} | 53 ++------------ .../Serializing/XMLFileDeserializer.php | 72 +++++++++++++++++++ .../Serializing/XMLMemoryDeserializer.php | 64 +++++++++++++++++ .../Import/QuestionSelectionStage.php | 6 +- 7 files changed, 149 insertions(+), 58 deletions(-) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/{SimpleXMLDeserializer.php => XMLDeserializerTrait.php} (73%) create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLFileDeserializer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLMemoryDeserializer.php diff --git a/components/ILIAS/Test/classes/class.ilTestImporter.php b/components/ILIAS/Test/classes/class.ilTestImporter.php index 7ee0c283a3f0..cbbc2732d66b 100755 --- a/components/ILIAS/Test/classes/class.ilTestImporter.php +++ b/components/ILIAS/Test/classes/class.ilTestImporter.php @@ -21,7 +21,7 @@ use ILIAS\Data\ReferenceId; use ILIAS\Test\ExportImport\Import\TestImporter; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\SimpleXMLDeserializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XMLMemoryDeserializer; use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; use ILIAS\Test\TestDIC; @@ -70,7 +70,7 @@ public function importXmlRepresentation( } $result = $this->importer->import( - new SimpleXMLDeserializer()->open($a_xml), + new XMLMemoryDeserializer()->open($a_xml), $a_mapping, new ReferenceId($a_mapping->getTargetId()), $context, diff --git a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php index 23f3b2f7f3c1..fd054b7a89b7 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php +++ b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php @@ -25,7 +25,7 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResult; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\SimpleXMLDeserializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XMLFileDeserializer; use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; use ilImport; @@ -66,7 +66,7 @@ public function process(ImportContext $context): StageResult return StageResult::error($context, $this->lng->txt('obj_import_file_error')); } - $deserializer = new SimpleXMLDeserializer()->open(file_get_contents($mappings_file)); + $deserializer = new XMLFileDeserializer()->open($mappings_file); $deserializer->addHandler('mappings', function (array $mappings) use (&$context) { $context = $context->with('mappings', $mappings); }); diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php index 2dee6dc85e14..1c6a2a528a4d 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php @@ -20,7 +20,7 @@ use ILIAS\Data\ReferenceId; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\SimpleXMLDeserializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XMLMemoryDeserializer; use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionPoolImporter; use ILIAS\TestQuestionPool\QuestionPoolDIC; @@ -66,7 +66,7 @@ public function importXmlRepresentation( } $result = $this->importer->import( - new SimpleXMLDeserializer()->open($a_xml), + new XMLMemoryDeserializer()->open($a_xml), $a_mapping, new ReferenceId($a_mapping->getTargetId()), $context, diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLDeserializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLDeserializerTrait.php similarity index 73% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLDeserializer.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLDeserializerTrait.php index 7e93b5eac22b..e4ab81b7bff1 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLDeserializer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLDeserializerTrait.php @@ -20,48 +20,21 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Deserializer; - /** - * Simple XML deserializer that reads an XML string in chunks and invokes registered handlers per group. + * Shared XMLWriter-based parsing logic used by both in-memory and file-based XML serializers. */ -class SimpleXMLDeserializer implements Deserializer +trait XMLDeserializerTrait { - private string $xml = ''; - - /** @var array $handler */ + /** @var array */ private array $handler = []; - /** - * @inheritDoc - */ - public function open(string $path): static - { - $clone = clone $this; - $clone->xml = $path; - return $clone; - } - - /** - * @inheritDoc - */ public function addHandler(string $group, callable $handler): void { $this->handler[$group] = $handler; } - /** - * @inheritDoc - */ - public function process(): void + private function processReader(\XMLReader $reader): void { - $reader = new \XMLReader(); - $xml = $this->prepareXmlInput($this->xml); - - if (!$reader->XML($xml, null, LIBXML_NONET)) { - throw new \RuntimeException('Unable to read XML input.'); - } - while ($reader->read()) { $node_name = $this->kebabToSnake($reader->name); @@ -85,7 +58,6 @@ private function readGroup(\XMLReader $reader): array return []; } - // Track the current group boundary so we can stop exactly at its closing tag $group_depth = $reader->depth; $group_name = $reader->name; $group_data = []; @@ -103,7 +75,6 @@ private function readGroup(\XMLReader $reader): array $reader->nodeType !== \XMLReader::ELEMENT || $reader->depth !== $group_depth + 1 ) { - // Only direct children belong to this group entry list continue; } @@ -117,7 +88,6 @@ private function readElementValue(\XMLReader $reader): mixed { $is_marked_empty_array = $this->isMarkedEmptyArray($reader); - // Keep empty elements as empty strings to preserve the legacy XML shape if ($reader->isEmptyElement) { if ($is_marked_empty_array) { return []; @@ -143,17 +113,14 @@ private function readElementValue(\XMLReader $reader): mixed $reader->nodeType === \XMLReader::ELEMENT && $reader->depth === $element_depth + 1 ) { - // Direct child elements are deserialized recursively $child_key = $this->resolveElementKey($reader); $child_value = $this->readElementValue($reader); if ($child_key === null) { - // Unkeyed nodes are appended as list entries $children[] = $child_value; continue; } - // Repeated keys are normalized to list values via appendValue() $this->appendValue($children, $child_key, $child_value); continue; } @@ -170,13 +137,11 @@ private function readElementValue(\XMLReader $reader): mixed true ) ) { - // Keep raw text content and decode scalar tokens after traversal $text_content .= $reader->value; } } if ($children !== []) { - // Structured child data has precedence over accumulated text content return $children; } @@ -194,24 +159,20 @@ private function isMarkedEmptyArray(\XMLReader $reader): bool private function resolveElementKey(\XMLReader $reader): ?string { - // Regular element names map directly to associative keys if ($reader->name !== 'item') { return $this->kebabToSnake($reader->name); } - // nodes are list-like unless they define an explicit key attribute $raw_key = $reader->getAttribute('key'); if ($raw_key === null || $raw_key === '') { return null; } - // Normalize kebab-case keys to snake_case for downstream consumers return $this->kebabToSnake($raw_key); } /** * @param array $target - * @param mixed $value */ private function appendValue(array &$target, string $key, mixed $value): void { @@ -237,10 +198,4 @@ private function kebabToSnake(string $name): string { return str_replace('-', '_', $name); } - - private function prepareXmlInput(string $xml): string - { - $xml = preg_replace('/^\s*<\?xml[^>]*\?>\s*/i', '', trim($xml)) ?? trim($xml); - return "{$xml}"; - } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLFileDeserializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLFileDeserializer.php new file mode 100644 index 000000000000..90e7ad6e52e2 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLFileDeserializer.php @@ -0,0 +1,72 @@ +file_path = $path; + return $clone; + } + + /** + * @inheritDoc + */ + public function process(): void + { + if ($this->file_path === '') { + throw new \RuntimeException( + 'No file has been opened. Call open() before process().' + ); + } + + $reader = new \XMLReader(); + + if (!$reader->open($this->file_path, null, LIBXML_NONET)) { + throw new \RuntimeException( + "Unable to open XML file '{$this->file_path}'." + ); + } + + $this->processReader($reader); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLMemoryDeserializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLMemoryDeserializer.php new file mode 100644 index 000000000000..395d0d2a5b87 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLMemoryDeserializer.php @@ -0,0 +1,64 @@ +xml = $path; + return $clone; + } + + /** + * @inheritDoc + */ + public function process(): void + { + $reader = new \XMLReader(); + $xml = $this->prepareXmlInput($this->xml); + + if (!$reader->XML($xml, null, LIBXML_NONET)) { + throw new \RuntimeException('Unable to read XML input.'); + } + + $this->processReader($reader); + } + + private function prepareXmlInput(string $xml): string + { + $xml = preg_replace('/^\s*<\?xml[^>]*\?>\s*/i', '', trim($xml)) ?? trim($xml); + return "{$xml}"; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php index 9276e158ad88..bb890f483b9f 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php @@ -25,7 +25,7 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ImportStage; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResult; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\SimpleXMLDeserializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XMLFileDeserializer; use ILIAS\UI\Component\Input\Container\Form\Form; use ILIAS\UI\Factory as UIFactory; use Psr\Http\Message\ServerRequestInterface; @@ -133,8 +133,8 @@ private function readQuestions(ImportContext $context): array { $options = []; - $deserializer = new SimpleXMLDeserializer()->open( - file_get_contents($context->get(UploadValidationStage::COMPONENT_IMPORT_FILE)) + $deserializer = new XMLFileDeserializer()->open( + $context->get(UploadValidationStage::COMPONENT_IMPORT_FILE) ); $deserializer->addHandler('questions', function (array $questions) use (&$options): void { From 862d40aa5f43ec002dc137403ce54f24e9eb17df Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Mon, 4 May 2026 12:15:10 +0200 Subject: [PATCH 34/66] feat(test): implement skill level thresholds import --- .../class.ilTestSkillLevelThreshold.php | 37 +----- .../Import/SkillLevelThresholdsImporter.php | 119 ++++++++++++++++++ .../src/ExportImport/Import/TestImporter.php | 23 +++- .../ilTestSkillLevelThresholdNormalizer.php | 83 ++++++++++++ .../Test/src/ExportImport/TestExporter.php | 4 +- components/ILIAS/Test/src/TestDIC.php | 13 +- .../Import/QuestionPoolImporter.php | 3 +- .../Import/SkillAssignmentsImporter.php | 18 ++- .../TestQuestionPool/src/QuestionPoolDIC.php | 1 + 9 files changed, 252 insertions(+), 49 deletions(-) create mode 100644 components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php create mode 100644 components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSkillLevelThresholdNormalizer.php diff --git a/components/ILIAS/Test/classes/class.ilTestSkillLevelThreshold.php b/components/ILIAS/Test/classes/class.ilTestSkillLevelThreshold.php index a79f3e32a87f..70f22450ad38 100755 --- a/components/ILIAS/Test/classes/class.ilTestSkillLevelThreshold.php +++ b/components/ILIAS/Test/classes/class.ilTestSkillLevelThreshold.php @@ -17,10 +17,6 @@ *********************************************************************/ declare(strict_types=1); -use ILIAS\Refinery\Transformation; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; /** * @author Björn Heyser @@ -28,7 +24,7 @@ * * @package components\ILIAS/Test */ -class ilTestSkillLevelThreshold implements Normalizable +class ilTestSkillLevelThreshold { /** * @var ilDBInterface @@ -218,35 +214,4 @@ public function getThreshold(): ?int { return is_numeric($this->threshold) ? (int) $this->threshold : null; } - - - /** - * @inheritDoc - */ - public function toNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(fn(): array => [ - 'id' => $tt->normalize(new Id($this->getSkillLevelId(), 'skill_level')), - 'test_id' => $tt->normalize(new Id($this->getTestId(), 'tst')), - 'skill_base_id' => $tt->normalize(new Id($this->getSkillBaseId(), 'skill_base')), - 'skill_tref_id' => $tt->normalize(new Id($this->getSkillTrefId(), 'skill_tref')), - 'threshold' => $this->getThreshold(), - ]); - } - - /** - * @inheritDoc - */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = clone $this; - $clone->setSkillLevelId($tt->denormalize($normalized['id'], Id::class)->getId()); - $clone->setTestId($tt->denormalize($normalized['test_id'], Id::class)->getId()); - $clone->setSkillBaseId($tt->denormalize($normalized['skill_base_id'], Id::class)->getId()); - $clone->setSkillTrefId($tt->denormalize($normalized['skill_tref_id'], Id::class)->getId()); - $clone->setThreshold($normalized['threshold']); - return $clone; - }); - } } diff --git a/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php b/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php new file mode 100644 index 000000000000..0c9cf976ba0c --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php @@ -0,0 +1,119 @@ +> $normalized_thresholds + * @return array{failed: list, success: list} + */ + public function import( + array $normalized_thresholds, + int $import_install_id, + Transformations $transformations, + ilImportMapping $mapping, + ): array { + $result = ['failed' => [], 'success' => []]; + $threshold_list = new ilTestSkillLevelThresholdList($this->db); + + foreach ($normalized_thresholds as $item) { + // TestID and Skill BaseID/TRefID will be replaced by the mapping pipe + $threshold = $transformations->denormalize($item, ilTestSkillLevelThreshold::class); + + $local_level_id = $this->getLevelIdMapping($import_install_id, $threshold->getSkillLevelId()); + if ($local_level_id === null) { + $result['failed'][] = $this->buildResultData($threshold); + continue; + } + + $mapping->addMapping( + $this->component, + 'skill_level', + (string) $threshold->getSkillLevelId(), + (string) $local_level_id, + ); + $threshold->setSkillLevelId($local_level_id); + + $threshold_list->addThreshold($threshold); + $result['success'][] = $this->buildResultData($threshold); + } + + $threshold_list->saveToDb(); + return $result; + } + + protected function getLevelIdMapping(int $import_install_id, int $import_level_id): ?int + { + if ($import_install_id === $this->local_install_id) { + return $import_level_id; + } + + $result = $this->skill_repo->getCommonSkillIdForImportId($import_install_id, $import_level_id); + $most_new_level_data = current($result); + if (!is_array($most_new_level_data)) { + return null; + } + + return $most_new_level_data['level_id']; + } + + /** + * @return ImportResultData + */ + private function buildResultData(ilTestSkillLevelThreshold $threshold): array + { + return [ + 'skill_base_id' => $threshold->getSkillBaseId() ?? 0, + 'skill_tref_id' => $threshold->getSkillTrefId() ?? 0, + 'skill_level_id' => $threshold->getSkillLevelId() ?? 0, + 'threshold' => $threshold->getThreshold() ?? 0, + ]; + } +} diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php index b837d9b7e6e1..077e3d8ea2af 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php @@ -21,13 +21,12 @@ namespace ILIAS\Test\ExportImport\Import; use assFileUploadStakeholder; -use ilCtrl; +use ilDBConstants; use ilDBInterface; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\ReferenceId; use ILIAS\Data\UUID\Factory as UUIDFactory; use ILIAS\Filesystem\Stream\Streams; -use ILIAS\Language\Language; use ILIAS\ResourceStorage\Services as IRSS; use ILIAS\Test\ExportImport\Envelopes\AdditionalWorkingTime; use ILIAS\Test\ExportImport\Envelopes\ManualFeedback; @@ -58,7 +57,6 @@ use ilImportMapping; use ilObjTest; use ilTestPage; -use ilDBConstants; use ilTestSequence; use Psr\Log\LoggerInterface; use RuntimeException; @@ -73,14 +71,13 @@ class TestImporter { public function __construct( private readonly Builder $builder, - private readonly ilCtrl $ctrl, private readonly ilDBInterface $database, private readonly LoggerInterface $log, - private readonly Language $language, private readonly IRSS $irss, private readonly DataFactory $data_factory, private readonly QuestionsImporter $questions_importer, private readonly SkillAssignmentsImporter $skill_importer, + private readonly SkillLevelThresholdsImporter $skill_thresholds_importer, private readonly MarksRepository $marks_repository, ) { } @@ -144,16 +141,30 @@ function (array $normalized) use ($tt, $mapping, $context, &$test_object): void $deserializer->addHandler( 'skill_assignments', - function (array $assignments) use ($tt, &$context): void { + function (array $assignments) use ($tt, $mapping, &$context): void { $result = $this->skill_importer->import( $assignments, UploadValidationStage::getInstallId($context), $tt, + $mapping, ); $context = $context->with('skill_assignments', $result); } ); + $deserializer->addHandler( + 'skill_thresholds', + function (array $thresholds) use ($tt, $mapping, &$context): void { + $result = $this->skill_thresholds_importer->import( + $thresholds, + UploadValidationStage::getInstallId($context), + $tt, + $mapping, + ); + $context = $context->with('skill_thresholds', $result); + } + ); + $deserializer->addHandler( 'participants', function (array $participants) use ($tt, $mapping): void { diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSkillLevelThresholdNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSkillLevelThresholdNormalizer.php new file mode 100644 index 000000000000..e788922778ee --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSkillLevelThresholdNormalizer.php @@ -0,0 +1,83 @@ + + */ +#[Normalizes(ilTestSkillLevelThreshold::class)] +class ilTestSkillLevelThresholdNormalizer implements Normalizer +{ + private readonly ilDBInterface $db; + + public function __construct( + private readonly Transformations $tt, + Container $dic + ) { + $this->db = $dic->database(); + } + + /** + * @inheritDoc + */ + public function normalize($value): array|float|bool|int|string|null + { + if (!$value instanceof ilTestSkillLevelThreshold) { + throw new NormalizingException('Invalid value', $value); + } + + return [ + 'id' => $this->tt->normalize(new Id($value->getSkillLevelId(), 'skill_level')), + 'test_id' => $this->tt->normalize(new Id($value->getTestId(), 'tst')), + 'skill_base_id' => $this->tt->normalize(new Id($value->getSkillBaseId(), 'skill_base')), + 'skill_tref_id' => $this->tt->normalize(new Id($value->getSkillTrefId(), 'skill_tref')), + 'threshold' => $value->getThreshold(), + ]; + } + + /** + * @inheritDoc + */ + public function denormalize(array|float|bool|int|string|null $value, string $type): ilTestSkillLevelThreshold + { + if ($type !== ilTestSkillLevelThreshold::class) { + throw new NormalizingException("Invalid type for ilTestSkillLevelThreshold: {$type}"); + } + + $threshold = new ilTestSkillLevelThreshold($this->db); + $threshold->setSkillLevelId($this->tt->denormalize($value['id'], Id::class)->getId()); + $threshold->setTestId($this->tt->denormalize($value['test_id'], Id::class)->getId()); + $threshold->setSkillBaseId($this->tt->denormalize($value['skill_base_id'], Id::class)->getId()); + $threshold->setSkillTrefId($this->tt->denormalize($value['skill_tref_id'], Id::class)->getId()); + $threshold->setThreshold($this->tt->int($value['threshold'])); + + return $threshold; + } +} diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index 7e4c519ce955..59d2c5e228b0 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -101,7 +101,7 @@ public function prepare(ExportState $state): void $object_id ), new CollectResources( - $this->irss, + $this->irss, $this->logger ), ]) @@ -309,7 +309,7 @@ private function exportQuestions( 'feedback' => $transformations->normalize( $collector->getFeedback($question) ), - 'sequence' => $question_properties[$question->getId()]->getSequenceInformation()->getPlaceInSequence(), + 'sequence' => $question_properties[$question->getId()]->getSequenceInformation()?->getPlaceInSequence(), ]; if ($question instanceof assFormulaQuestion) { diff --git a/components/ILIAS/Test/src/TestDIC.php b/components/ILIAS/Test/src/TestDIC.php index cf7b12ada9eb..c10aeb344111 100755 --- a/components/ILIAS/Test/src/TestDIC.php +++ b/components/ILIAS/Test/src/TestDIC.php @@ -56,6 +56,7 @@ use ILIAS\TestQuestionPool\ExportImport\Import\QuestionsImporter; use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter; use ILIAS\Test\ExportImport\Import\TestImporter; +use ILIAS\Test\ExportImport\Import\SkillLevelThresholdsImporter; use ILIAS\TestQuestionPool\ExportImport\LoggingProvider; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ILIAS\TestQuestionPool\RequestDataCollector as QPLRequestDataCollector; @@ -274,6 +275,15 @@ protected static function buildDIC(ILIASContainer $DIC): self new SkillAssignmentsImporter( $DIC->skills()->internal()->repo()->getTreeRepo(), $DIC->skills()->usage(), + 'components/ILIAS/Test', + (int) $DIC->settings()->get('inst_id', '0') + ); + + $dic['exportimport.skill_level_thresholds_importer'] = static fn($c): SkillLevelThresholdsImporter => + new SkillLevelThresholdsImporter( + $DIC->database(), + $DIC->skills()->internal()->repo()->getTreeRepo(), + 'components/ILIAS/Test', (int) $DIC->settings()->get('inst_id', '0') ); @@ -291,14 +301,13 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic['exportimport.importer'] = static fn($c): TestImporter => new TestImporter( $c['exportimport.builder'], - $DIC->ctrl(), $DIC->database(), $c['exportimport.logging'](), - $DIC->language(), $DIC->resourceStorage(), new DataFactory(), $c['exportimport.questions_importer'], $c['exportimport.skill_assignments_importer'], + $c['exportimport.skill_level_thresholds_importer'], $c['marks.repository'] ); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php index 0be8d25cd088..8c9ef8e678ce 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php @@ -102,11 +102,12 @@ function (array $questions) use ($tt, $mapping, $selected_questions): void { $deserializer->addHandler( 'skill_assignments', - function (array $assignments) use ($tt, &$context): void { + function (array $assignments) use ($tt, $mapping, &$context): void { $result = $this->skill_importer->import( $assignments, UploadValidationStage::getInstallId($context), $tt, + $mapping, ); $context = $context->with('skill_assignments', $result); } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php index f0de70e409f8..04eec3458349 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php @@ -23,6 +23,7 @@ use ilAssQuestionSkillAssignment; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\Skill\Service\SkillUsageService; +use ilImportMapping; use ilSkillTreeRepository; /** @@ -36,6 +37,7 @@ class SkillAssignmentsImporter public function __construct( private readonly ilSkillTreeRepository $skill_repo, private readonly SkillUsageService $skill_usage_service, + private readonly string $component, private readonly int $local_install_id ) { } @@ -51,10 +53,12 @@ public function import( array $normalized_assignments, int $import_install_id, Transformations $transformations, + ilImportMapping $mapping, ): array { $result = ['failed' => [], 'success' => []]; foreach ($normalized_assignments as $item) { + // ParentObjID and QuestionID will be replaced by the mapping pipe $assignment = $transformations->denormalize($item, ilAssQuestionSkillAssignment::class); $skill_data = $this->getSkillIdMapping( @@ -67,8 +71,18 @@ public function import( continue; } - // Map imported skill ids to local skill ids. Question id and object id are already replaced in the id - // mapping pipe. + $mapping->addMapping( + $this->component, + 'skill_base', + (string) $assignment->getSkillBaseId(), + (string) $skill_data['skill_id'] + ); + $mapping->addMapping( + $this->component, + 'skill_tref', + (string) $assignment->getSkillTrefId(), + (string) $skill_data['tref_id'] + ); $assignment->setSkillBaseId($skill_data['skill_id']); $assignment->setSkillTrefId($skill_data['tref_id']); diff --git a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php index 7240f58a9597..cb89f529776d 100755 --- a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php +++ b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php @@ -101,6 +101,7 @@ protected static function buildDIC(ILIASContainer $DIC): self new SkillAssignmentsImporter( $DIC->skills()->internal()->repo()->getTreeRepo(), $DIC->skills()->usage(), + 'components/ILIAS/TestQuestionPool', (int) $DIC->settings()->get('inst_id', '0') ); $dic['exportimport.questions_importer'] = static fn($c): QuestionsImporter => From 235db308d413b17b6cb51130e852ba9f2f4949c6 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Mon, 4 May 2026 15:54:16 +0200 Subject: [PATCH 35/66] feat(test): introduce random question set config import --- .../Envelopes/QuestionSetConfig.php | 79 ++++++++ .../src/ExportImport/Import/TestImporter.php | 96 +++++++++- .../QuestionSetConfigNormalizer.php | 173 ++++++++++++++++-- .../Test/src/ExportImport/TestCollector.php | 42 ++++- .../Test/src/ExportImport/TestExporter.php | 20 +- 5 files changed, 386 insertions(+), 24 deletions(-) create mode 100644 components/ILIAS/Test/src/ExportImport/Envelopes/QuestionSetConfig.php diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/QuestionSetConfig.php b/components/ILIAS/Test/src/ExportImport/Envelopes/QuestionSetConfig.php new file mode 100644 index 000000000000..6e1a75dececb --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Envelopes/QuestionSetConfig.php @@ -0,0 +1,79 @@ + */ + private array $definitions = [], + /** @var array> */ + private array $staging_pools = [], + ) { + } + + public function getConfig(): ilTestQuestionSetConfig + { + return $this->config; + } + + public function isRandom(): bool + { + return $this->config instanceof ilTestRandomQuestionSetConfig; + } + + /** + * @return list + */ + public function getDefinitions(): array + { + return $this->definitions; + } + + /** + * @param list $definitions + */ + public function setDefinitions(array $definitions): void + { + $this->definitions = $definitions; + } + + /** + * @return array> + */ + public function getStagingPools(): array + { + return $this->staging_pools; + } + + /** + * @param list $questions + */ + public function addStagingPoolQuestions(int $pool_id, array $questions): void + { + $this->staging_pools[$pool_id] = $questions; + } +} diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php index 077e3d8ea2af..649084021c3e 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php @@ -31,6 +31,7 @@ use ILIAS\Test\ExportImport\Envelopes\AdditionalWorkingTime; use ILIAS\Test\ExportImport\Envelopes\ManualFeedback; use ILIAS\Test\ExportImport\Envelopes\QuestionResult; +use ILIAS\Test\ExportImport\Envelopes\QuestionSetConfig; use ILIAS\Test\ExportImport\Envelopes\Solution; use ILIAS\Test\ExportImport\Envelopes\WorkingTime; use ILIAS\Test\Participants\Participant; @@ -57,6 +58,8 @@ use ilImportMapping; use ilObjTest; use ilTestPage; +use ilTestRandomQuestionSetSourcePoolDefinition; +use ilTestRandomQuestionSetStagingPoolQuestion; use ilTestSequence; use Psr\Log\LoggerInterface; use RuntimeException; @@ -139,6 +142,18 @@ function (array $normalized) use ($tt, $mapping, $context, &$test_object): void } ); + $deserializer->addHandler( + 'question_set_config', + function (array $normalized) use ($tt, $mapping, $context, &$test_object): void { + $this->importQuestionSetConfig( + reset($normalized), + $tt, + $mapping, + $test_object + ); + } + ); + $deserializer->addHandler( 'skill_assignments', function (array $assignments) use ($tt, $mapping, &$context): void { @@ -360,7 +375,7 @@ private function importQuestions( foreach ($list as $normalized) { $question = $this->questions_importer->importQuestion($normalized, $tt, $mapping, $selected_questions); - if ($question) { + if ($question && $normalized['sequence'] !== null) { $sequence = $tt->int($normalized['sequence']); $test_object->questions[$sequence] = $question->getId(); $this->log->debug("Stored question {$question->getId()} at sequence {$sequence} in test"); @@ -371,6 +386,85 @@ private function importQuestions( $this->log->debug('Saved test questions to database'); } + private function importQuestionSetConfig( + array $normalized, + Transformations $tt, + ilImportMapping $mapping, + ilObjTest $test_object + ): void { + $config = $tt->denormalize($normalized, QuestionSetConfig::class); + if (!$config->isRandom()) { + return; + } + + $config->getConfig()->saveToDb(); + $this->log->debug("Imported random question set config for test {$test_object->getTestId()}"); + + foreach ($config->getStagingPools() as $pool_id => $questions) { + $this->importRandomQuestionStagingPool($pool_id, $questions, $mapping, $test_object); + } + + foreach ($config->getDefinitions() as $definition) { + $this->importSourcePoolDefinition($definition, $mapping); + } + } + + private function importRandomQuestionStagingPool( + int $old_pool_id, + array $questions, + ilImportMapping $mapping, + ilObjTest $test_object + ): void { + $new_pool_id = $this->database->nextId('object_data'); + $mapping->addMapping( + 'components/ILIAS/Test', + 'pool', + (string) $old_pool_id, + (string) $new_pool_id + ); + $this->log->debug("Imported random question staging pool: {$old_pool_id} -> {$new_pool_id}"); + + // QuestionID was mapped during question set config denormalization + foreach ($questions as $question_id) { + $question = new ilTestRandomQuestionSetStagingPoolQuestion($this->database); + $question->setTestId($test_object->getTestId()); + $question->setPoolId($new_pool_id); + $question->setQuestionId($question_id); + $question->saveQuestionStaging(); + $this->log->debug("Imported random question staging question: {$question_id}"); + } + } + + private function importSourcePoolDefinition( + ilTestRandomQuestionSetSourcePoolDefinition $definition, + ilImportMapping $mapping, + ): void { + // New PoolID was not available during denormalization, so we have to map it here + $old_pool_id = $definition->getPoolId(); + $new_pool_id = (int) $mapping->getMapping('components/ILIAS/Test', 'pool', (string) $old_pool_id); + $definition->setPoolId($new_pool_id); + + if ($old_pool_id !== $new_pool_id) { + $ref_ids = $this->data_factory->objId($new_pool_id)->toReferenceIds(); + if (count($ref_ids) > 0) { + $definition->setPoolRefId(current($ref_ids)->toInt()); + $this->log->debug("Derived source pool definition from Object ID: {$old_pool_id} -> {$definition->getPoolRefId()}"); + } + } + + $old_definition_id = $definition->getId(); + $definition->setId(0); + $definition->saveToDb(); + $this->log->debug("Imported source pool definition: {$old_definition_id} -> {$definition->getId()}"); + + $mapping->addMapping( + 'components/ILIAS/Test', + 'rnd_src_pool_def', + (string) $old_definition_id, + (string) $definition->getId() + ); + } + private function importParticipants(array $list, Transformations $tt, ilImportMapping $mapping): void { foreach ($list as $normalized) { diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php index e2ca52dad50d..f91a2aff589d 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php @@ -20,53 +20,200 @@ namespace ILIAS\Test\ExportImport\Normalizer; +use ILIAS\DI\Container; +use ILIAS\Test\ExportImport\Envelopes\QuestionSetConfig; +use ILIAS\Test\TestDIC; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; use ilObjTest; +use ilTestException; use ilTestFixedQuestionSetConfig; use ilTestRandomQuestionSetConfig; use ilTestQuestionSetConfig; +use ilTestRandomQuestionSetSourcePoolDefinition; +use ReflectionClass; /** - * @implements Normalizer + * @implements Normalizer */ -#[Normalizes(ilTestQuestionSetConfig::class)] +#[Normalizes(QuestionSetConfig::class)] class QuestionSetConfigNormalizer implements Normalizer { + public function __construct( + private readonly Transformations $tt, + private readonly Container $dic, + private readonly TestDIC $test_dic, + ) { + } + /** * @inheritDoc */ public function normalize($value): array|float|bool|int|string|null { - if ($value instanceof ilTestFixedQuestionSetConfig) { + if (!$value instanceof QuestionSetConfig) { + throw new NormalizingException('Invalid value', $value); + } + + $normalized = [ + 'config' => $this->normalizeQuestionSetConfig($value->getConfig()), + 'test_obj' => $this->normalizeTestObj($value->getConfig()), + ]; + + if ($value->isRandom()) { + $normalized['definitions'] = $this->tt->normalize($value->getDefinitions()); + $normalized['staging_pools'] = $this->normalizeStagingPools($value->getStagingPools()); + } + + return $normalized; + } + + private function normalizeQuestionSetConfig(ilTestQuestionSetConfig $config): array + { + if ($config instanceof ilTestFixedQuestionSetConfig) { return [ 'type' => ilObjTest::QUESTION_SET_TYPE_FIXED, ]; } - if ($value instanceof ilTestRandomQuestionSetConfig) { + if ($config instanceof ilTestRandomQuestionSetConfig) { return [ 'type' => ilObjTest::QUESTION_SET_TYPE_RANDOM, - 'homogeneous' => $value->arePoolsWithHomogeneousScoredQuestionsRequired(), - 'amount_mode' => $value->getQuestionAmountConfigurationMode(), - 'amount' => $value->getQuestionAmountPerTest(), - 'sync_timestamp' => $value->getLastQuestionSyncTimestamp(), + 'homogeneous' => $config->arePoolsWithHomogeneousScoredQuestionsRequired(), + 'amount_mode' => $config->getQuestionAmountConfigurationMode(), + 'amount' => $config->getQuestionAmountPerTest(), + 'sync_timestamp' => $config->getLastQuestionSyncTimestamp(), + ]; + } + + throw new NormalizingException('Invalid value', $config); + } + + private function normalizeStagingPools(array $staging_pools): array + { + $normalized = []; + foreach ($staging_pools as $pool_id => $questions) { + $normalized[] = [ + 'pool_id' => $this->tt->normalize(new Id($pool_id, 'pool')), + 'questions' => array_map( + fn($question) => $this->tt->normalize( + new Id($question, 'question') + ), + $questions + ), ]; } + return $normalized; + } + + private function normalizeTestObj(ilTestQuestionSetConfig $config): array + { + $reflection = new ReflectionClass($config); + $property = $reflection->getProperty('test_obj'); + $property->setAccessible(true); + $test_obj = $property->getValue($config); + + if (!$test_obj instanceof ilObjTest) { + throw new NormalizingException('Invalid test object', $test_obj); + } - throw new NormalizingException('Invalid value', $value); + return [ + 'obj_id' => $this->tt->normalize(new Id($test_obj->getId(), 'object')), + 'test_id' => $this->tt->normalize(new Id($test_obj->getTestId(), 'tst')), + ]; } /** * @inheritDoc */ - public function denormalize(array|float|bool|int|string|null $value, string $type): ilTestQuestionSetConfig + public function denormalize(array|float|bool|int|string|null $value, string $type): mixed + { + if ($type !== QuestionSetConfig::class) { + throw new NormalizingException("Invalid type for QuestionSetConfig: {$type}"); + } + + $test_obj = $this->denormalizeTestObj($value['test_obj']); + $config = $this->denormalizeQuestionSetConfig($value['config'], $test_obj); + + if (!$config instanceof ilTestRandomQuestionSetConfig) { + return new QuestionSetConfig($config); + } + + $definitions = array_map( + fn($definition) => $this->denormalizeSourcePoolDefinition($definition, $test_obj), + $value['definitions'] + ); + $staging_pools = $this->denormalizeStagingPools($value['staging_pools']); + + return new QuestionSetConfig($config, $definitions, $staging_pools); + } + + private function denormalizeQuestionSetConfig(array $normalized, ilObjTest $test_obj): ilTestQuestionSetConfig + { + $class = $normalized['type'] === ilObjTest::QUESTION_SET_TYPE_RANDOM + ? ilTestRandomQuestionSetConfig::class + : ilTestFixedQuestionSetConfig::class; + + $config = new $class( + $this->dic->repositoryTree(), + $this->dic->database(), + $this->dic->language(), + $this->test_dic['logging.logger'], + $this->dic['component.repository'], + $test_obj, + $this->test_dic['question.general_properties.repository'] + ); + + if ($config instanceof ilTestFixedQuestionSetConfig) { + return $config; + } + + $amount_mode = $this->tt->string($normalized['amount_mode']); + if (!$config->isValidQuestionAmountConfigurationMode($amount_mode)) { + throw new ilTestException("Invalid random test question set config amount mode given: {$amount_mode}"); + } + + $config->setQuestionAmountConfigurationMode($amount_mode); + $config->setQuestionAmountPerTest($this->tt->nullableInt($normalized['amount'])); + $config->setPoolsWithHomogeneousScoredQuestionsRequired($this->tt->nullableBool($normalized['homogeneous'])); + $config->setLastQuestionSyncTimestamp($this->tt->nullableInt($normalized['sync_timestamp'])); + + return $config; + } + + private function denormalizeSourcePoolDefinition(array $normalized, ilObjTest $test_obj): ilTestRandomQuestionSetSourcePoolDefinition + { + $definition = new ilTestRandomQuestionSetSourcePoolDefinition( + $this->dic->database(), + $test_obj + ); + + return $this->tt->denormalize($normalized, $definition); + } + + private function denormalizeStagingPools(array $normalized): array { - if (!in_array(ilTestQuestionSetConfig::class, class_parents($type))) { - throw new NormalizingException("Invalid type for ilTestQuestionSetConfig: {$type}"); + $staging_pools = []; + foreach ($normalized as $staging_pool) { + $pool_id = $this->tt->denormalize($staging_pool['pool_id'], Id::class)->getId(); + + $staging_pools[$pool_id] = array_map( + fn($question) => $this->tt->denormalize($question, Id::class)->getId(), + $staging_pool['questions'] + ); } + return $staging_pools; + } + + private function denormalizeTestObj(array $normalized): ilObjTest + { + $test_obj = new ilObjTest(0, false); + $test_obj->setTestId($this->tt->denormalize($normalized['test_id'], Id::class)->getId()); + $test_obj->setId($this->tt->denormalize($normalized['obj_id'], Id::class)->getId()); - //TODO: Implement denormalization + return $test_obj; } } diff --git a/components/ILIAS/Test/src/ExportImport/TestCollector.php b/components/ILIAS/Test/src/ExportImport/TestCollector.php index 3d59f2c50d30..e41250590b03 100644 --- a/components/ILIAS/Test/src/ExportImport/TestCollector.php +++ b/components/ILIAS/Test/src/ExportImport/TestCollector.php @@ -29,6 +29,7 @@ use ILIAS\Test\ExportImport\Envelopes\AdditionalWorkingTime; use ILIAS\Test\ExportImport\Envelopes\ManualFeedback; use ILIAS\Test\ExportImport\Envelopes\QuestionResult; +use ILIAS\Test\ExportImport\Envelopes\QuestionSetConfig; use ILIAS\Test\ExportImport\Envelopes\Solution; use ILIAS\Test\ExportImport\Envelopes\WorkingTime; use ILIAS\Test\Logging\TestLogger; @@ -44,8 +45,10 @@ use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ilObjTest; use ilTestQuestionSetConfigFactory; +use ilTestRandomQuestionSetConfig; use ilTestRandomQuestionSetSourcePoolDefinitionFactory; use ilTestRandomQuestionSetSourcePoolDefinitionList; +use ilTestRandomQuestionSetStagingPoolQuestionList; use ilTestSequence; use ilTestSkillLevelThreshold; use ilTestSkillLevelThresholdList; @@ -191,11 +194,10 @@ public function getSkillLevelThresholds(): array } /** - * Get the question set config and the source pool definitions. - * - * @return array{config: \ilTestQuestionSetConfig, definitions: list<\ilTestRandomQuestionSetSourcePoolDefinition>} + * Get the question set config. If it is a random question set config, also return the source pool sages and + * definitions. */ - public function getQuestionSetConfig(): array + public function getQuestionSetConfig(): QuestionSetConfig { $factory = new ilTestQuestionSetConfigFactory( $this->tree, @@ -207,7 +209,10 @@ public function getQuestionSetConfig(): array $this->general_questions_repository ); - $config = $factory->getQuestionSetConfig(); + $config = new QuestionSetConfig($factory->getQuestionSetConfig()); + if (!$config->isRandom()) { + return $config; + } $definition_factory = new ilTestRandomQuestionSetSourcePoolDefinitionFactory( $this->db, @@ -219,12 +224,31 @@ public function getQuestionSetConfig(): array $this->getObject(), $definition_factory ); + $definition_list->loadDefinitions(); + $config->setDefinitions(iterator_to_array($definition_list)); - return [ - 'config' => $config, - 'definitions' => iterator_to_array($definition_list), - ]; + foreach ($definition_list->getInvolvedSourcePoolIds() as $pool_id) { + $config->addStagingPoolQuestions($pool_id, $this->getStagingPoolQuestions($pool_id)); + } + + return $config; + } + + /** + * @return list + */ + private function getStagingPoolQuestions(int $pool_id): array + { + $question_list = new ilTestRandomQuestionSetStagingPoolQuestionList( + $this->db, + $this->component_repository + ); + $question_list->setTestId($this->getTestId()); + $question_list->setPoolId($pool_id); + $question_list->loadQuestions(); + + return $question_list->getQuestions(); } /* diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index 59d2c5e228b0..553e0c9b37c3 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -163,6 +163,14 @@ public function process(ExportState $state): void $state ) ); + $state->serializer()->group( + 'question_set_config', + fn() => $this->exportQuestionSetConfig( + $state->collector(), + $state->transformations(), + $state->serializer(), + ) + ); $state->serializer()->group( 'additional_working_times', fn() => $this->exportAdditionalWorkingTimes( @@ -285,7 +293,6 @@ private function exportSettings( $serializer->append('main', $transformations->normalize($main_settings)); $serializer->append('scoring', $transformations->normalize($test->getScoreSettings())); $serializer->append('marks', $transformations->normalize($test->getMarkSchema())); - $serializer->append('question_set_config', $transformations->normalize($collector->getQuestionSetConfig())); if ($intro_page_id = $main_settings->getIntroductionSettings()->getIntroductionPageId()) { $state->addDependency('components/ILIAS/COPage', 'pg', ["tst:{$intro_page_id}"]); @@ -322,6 +329,17 @@ private function exportQuestions( } } + private function exportQuestionSetConfig( + TestCollector $collector, + Transformations $transformations, + Serializer $serializer, + ): void { + $serializer->append( + 'question_set_config', + $transformations->normalize($collector->getQuestionSetConfig()) + ); + } + private function exportSkillAssignments( TestCollector $collector, Transformations $transformations, From ac0714962477f3a37e50b673e242e9e2cede048f Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Thu, 7 May 2026 09:45:11 +0200 Subject: [PATCH 36/66] fix(qpl): remove todo annotations in normalizing steps --- .../classes/class.ilAssKprimChoiceAnswer.php | 2 -- .../Normalizing/Normalizer/IlObjectNormalizer.php | 11 ++--------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php b/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php index 942a707abc73..827000926ccd 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php @@ -173,8 +173,6 @@ public function fromNormalized(Transformations $tt): Transformation $clone->answertext = $tt->nullableString($normalized['answertext']); $clone->imageFile = $tt->denormalize($normalized['image'], QuestionImage::class)?->getFilename(); $clone->correctness = $tt->int($normalized['correctness']); - - //TODO: imageFSDir, imageWebDir, thumbPrefix ? return $clone; }); } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php index 50aadff251f6..727963aa1378 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php @@ -57,11 +57,8 @@ public function normalize($value): array|float|bool|int|string|null throw new NormalizingException('Invalid value', $value); } - // TODO: Icon - // TODO: Tile Image - // TODO: Translations - // TODO: Container Settings - + // Icon, tile, translations and container settings are exported by the ILIASObject component. + // See: ilObjectDataSet return [ 'obj_id' => $this->tt->normalize(new Id($value->getId(), 'object')), 'title' => $value->getTitle(), @@ -84,8 +81,6 @@ private function normalizeProperties(Properties $properties): array 'title_and_icon_visibility' => $this->normalizeProperty($properties->getPropertyTitleAndIconVisibility()), 'header_action_visibility' => $this->normalizeProperty($properties->getPropertyHeaderActionVisibility()), 'info_tab_visibility' => $this->normalizeProperty($properties->getPropertyInfoTabVisibility()), - // 'tile_image' => $properties->getPropertyTileImage(), - // 'icon' => $this->normalizeProperty($properties->getPropertyIcon()), 'translations' => $this->normalizeTranslations($properties->getPropertyTranslations()), ]; } @@ -148,8 +143,6 @@ public function denormalize(array|float|bool|int|string|null $value, string $typ $object->setOwner($this->tt->int($value['owner'])); $object->setImportId($this->tt->string($value['import_id'])); - // TODO: Properties - return $object; } } From 7b73a28e90aa89aa2747fc0305e0670a6c2cfb8a Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Thu, 7 May 2026 11:00:57 +0200 Subject: [PATCH 37/66] refactor(test): improve random test import and split importer class --- .../Test/classes/class.ilTestImporter.php | 23 ++ ...tRandomQuestionSetSourcePoolDefinition.php | 21 ++ .../Import/RandomTestConfigImporter.php | 186 +++++++++++ .../src/ExportImport/Import/TestImporter.php | 301 +----------------- .../Import/TestResultsImporter.php | 258 +++++++++++++++ components/ILIAS/Test/src/TestDIC.php | 15 + 6 files changed, 512 insertions(+), 292 deletions(-) create mode 100644 components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php create mode 100644 components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php diff --git a/components/ILIAS/Test/classes/class.ilTestImporter.php b/components/ILIAS/Test/classes/class.ilTestImporter.php index cbbc2732d66b..7922e637ec52 100755 --- a/components/ILIAS/Test/classes/class.ilTestImporter.php +++ b/components/ILIAS/Test/classes/class.ilTestImporter.php @@ -88,5 +88,28 @@ public function finalProcessing(ilImportMapping $a_mapping): void } $this->importer->finalize($a_mapping); + $this->finalizeTaxonomyUsage($a_mapping); + } + + private function finalizeTaxonomyUsage(ilImportMapping $a_mapping): void + { + $tst_mappings = $a_mapping->getMappingsOfEntity('components/ILIAS/Test', 'tst'); + + foreach ($tst_mappings as $old => $new) { + if ($old !== 'new_id' && (int) $old > 0) { + $new_tax_ids = $a_mapping->getMapping( + 'components/ILIAS/Taxonomy', + 'tax_usage_of_obj', + (string) $old + ); + + if ($new_tax_ids !== null) { + $tax_ids = explode(':', $new_tax_ids); + foreach ($tax_ids as $tid) { + ilObjTaxonomy::saveUsage((int) $tid, (int) $new); + } + } + } + } } } diff --git a/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php b/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php index b4164c416cf1..f53ed0238826 100755 --- a/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php +++ b/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php @@ -492,6 +492,7 @@ public function toNormalized(Transformations $tt): Transformation 'type_filter' => $this->getTypeFilterAsTypeTags(), 'lifecycle_filter' => $this->getLifecycleFilter(), 'taxonomy_filter' => [], + 'mapped_taxonomy_filter' => [], ]; foreach ($this->getOriginalTaxonomyFilter() as $tax_id => $node_ids) { @@ -504,6 +505,16 @@ public function toNormalized(Transformations $tt): Transformation ]; } + foreach ($this->getMappedTaxonomyFilter() as $tax_id => $node_ids) { + $normalized['mapped_taxonomy_filter'][] = [ + 'tax_id' => $tt->normalize(new Id($tax_id, 'mapped_tax')), + 'node_ids' => array_map( + fn($node_id) => $tt->normalize(new Id($node_id, 'mapped_tax_node')), + $node_ids + ), + ]; + } + return $normalized; }); } @@ -535,6 +546,16 @@ public function fromNormalized(Transformations $tt): Transformation } $clone->setOriginalTaxonomyFilter($taxonomy_filter); + $mapped_taxonomy_filter = []; + foreach (($normalized['mapped_taxonomy_filter'] ?? []) as $item) { + $tax_id = $tt->denormalize($item['tax_id'], Id::class)->getId(); + $mapped_taxonomy_filter[$tax_id] = array_map( + fn($node_id) => $tt->denormalize($node_id, Id::class)->getId(), + $item['node_ids'] + ); + } + $clone->setMappedTaxonomyFilter($mapped_taxonomy_filter); + return $clone; }); } diff --git a/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php b/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php new file mode 100644 index 000000000000..f9726a6454ca --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php @@ -0,0 +1,186 @@ +isRandom()) { + throw new \InvalidArgumentException('Expected random question set config'); + } + + $config->getConfig()->saveToDb(); + $this->log->debug("Imported random question set config for test {$test_object->getTestId()}"); + + foreach ($config->getStagingPools() as $pool_id => $questions) { + $this->importRandomQuestionStagingPool($pool_id, $questions, $mapping, $test_object); + } + + foreach ($config->getDefinitions() as $definition) { + $this->importSourcePoolDefinition($definition, $mapping); + } + } + + private function importRandomQuestionStagingPool( + int $old_pool_id, + array $questions, + ilImportMapping $mapping, + ilObjTest $test_object + ): void { + $new_pool_id = $this->database->nextId('object_data'); + $mapping->addMapping( + 'components/ILIAS/Test', + 'pool', + (string) $old_pool_id, + (string) $new_pool_id + ); + $this->log->debug("Imported random question staging pool: {$old_pool_id} -> {$new_pool_id}"); + + // QuestionID was mapped during question set config denormalization + foreach ($questions as $question_id) { + $question = new ilTestRandomQuestionSetStagingPoolQuestion($this->database); + $question->setTestId($test_object->getTestId()); + $question->setPoolId($new_pool_id); + $question->setQuestionId($question_id); + $question->saveQuestionStaging(); + $this->log->debug("Imported random question staging question: {$question_id}"); + } + } + + private function importSourcePoolDefinition( + ilTestRandomQuestionSetSourcePoolDefinition $definition, + ilImportMapping $mapping, + ): void { + // New PoolID was not available during denormalization, so we have to map it here + $old_pool_id = $definition->getPoolId(); + $new_pool_id = (int) $mapping->getMapping('components/ILIAS/Test', 'pool', (string) $old_pool_id); + $definition->setPoolId($new_pool_id); + + if ($old_pool_id !== $new_pool_id) { + $ref_ids = $this->data_factory->objId($new_pool_id)->toReferenceIds(); + if (count($ref_ids) > 0) { + $definition->setPoolRefId(current($ref_ids)->toInt()); + $this->log->debug("Derived source pool definition from Object ID: {$old_pool_id} -> {$definition->getPoolRefId()}"); + } + } + + $old_definition_id = $definition->getId(); + $definition->setId(0); + $definition->saveToDb(); + $this->log->debug("Imported source pool definition: {$old_definition_id} -> {$definition->getId()}"); + + $mapping->addMapping( + 'components/ILIAS/Test', + 'rnd_src_pool_def', + (string) $old_definition_id, + (string) $definition->getId() + ); + } + + /** + * Remap taxonomy IDs in the mapped_taxonomy_filter of all imported source pool definitions. + * Taxonomy mappings are only available after the Taxonomy component has finished its import, so this must run + * during finalProcessing(). + */ + public function finalizeTaxonomyFilters(ilImportMapping $mapping): void + { + $tst_mappings = $mapping->getMappingsOfEntity('components/ILIAS/Test', 'tst'); + + foreach ($tst_mappings as $old_test_id => $new_test_id) { + if ($old_test_id === 'new_id' || (int) $old_test_id <= 0) { + continue; + } + + $test_obj = new ilObjTest(0, false); + $test_obj->setTestId((int) $new_test_id); + + $definition_list = new ilTestRandomQuestionSetSourcePoolDefinitionList( + $this->database, + $test_obj, + new ilTestRandomQuestionSetSourcePoolDefinitionFactory($this->database, $test_obj) + ); + $definition_list->loadDefinitions(); + + foreach ($definition_list as $definition) { + $mapped_filter = $definition->getMappedTaxonomyFilter(); + if ($mapped_filter === []) { + continue; + } + + $new_filter = $this->remapTaxonomyFilter($mapping, $mapped_filter); + $definition->setMappedTaxonomyFilter($new_filter); + $definition->saveToDb(); + $this->log->debug("Remapped taxonomy filter for definition {$definition->getId()}"); + } + } + } + + private function remapTaxonomyFilter(ilImportMapping $mapping, array $filter): array + { + $remapped = []; + foreach ($filter as $tax_id => $node_ids) { + $new_tax_id = (int) $mapping->getMapping('components/ILIAS/Taxonomy', 'tax', (string) $tax_id); + if ($new_tax_id <= 0) { + continue; + } + + $remapped[$new_tax_id] = []; + foreach ($node_ids as $node_id) { + $new_node_id = (int) $mapping->getMapping('components/ILIAS/Taxonomy', 'tax_tree', (string) $node_id); + if ($new_node_id > 0) { + $remapped[$new_tax_id][] = $new_node_id; + } + } + } + + return $remapped; + } +} diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php index 649084021c3e..c737b428b1f7 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php @@ -28,21 +28,13 @@ use ILIAS\Data\UUID\Factory as UUIDFactory; use ILIAS\Filesystem\Stream\Streams; use ILIAS\ResourceStorage\Services as IRSS; -use ILIAS\Test\ExportImport\Envelopes\AdditionalWorkingTime; -use ILIAS\Test\ExportImport\Envelopes\ManualFeedback; -use ILIAS\Test\ExportImport\Envelopes\QuestionResult; use ILIAS\Test\ExportImport\Envelopes\QuestionSetConfig; -use ILIAS\Test\ExportImport\Envelopes\Solution; -use ILIAS\Test\ExportImport\Envelopes\WorkingTime; use ILIAS\Test\Participants\Participant; -use ILIAS\Test\Results\Data\AttemptResult; -use ILIAS\Test\Results\Data\ParticipantResult; use ILIAS\Test\Scoring\Marks\MarkSchema; use ILIAS\Test\Scoring\Marks\MarksRepository; use ILIAS\Test\Settings\GlobalSettings\UserIdentifiers; use ILIAS\Test\Settings\MainSettings\MainSettings; use ILIAS\Test\Settings\ScoreReporting\ScoreSettings; -use ILIAS\Test\TestManScoringDoneHelper; use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Deserializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; @@ -58,9 +50,6 @@ use ilImportMapping; use ilObjTest; use ilTestPage; -use ilTestRandomQuestionSetSourcePoolDefinition; -use ilTestRandomQuestionSetStagingPoolQuestion; -use ilTestSequence; use Psr\Log\LoggerInterface; use RuntimeException; @@ -79,6 +68,8 @@ public function __construct( private readonly IRSS $irss, private readonly DataFactory $data_factory, private readonly QuestionsImporter $questions_importer, + private readonly RandomTestConfigImporter $random_test_config_importer, + private readonly TestResultsImporter $test_results_importer, private readonly SkillAssignmentsImporter $skill_importer, private readonly SkillLevelThresholdsImporter $skill_thresholds_importer, private readonly MarksRepository $marks_repository, @@ -194,7 +185,7 @@ function (array $participants) use ($tt, $mapping): void { $deserializer->addHandler( 'results', function (array $results) use ($tt): void { - $this->importGroupedResults( + $this->test_results_importer->import( $results, $tt, ); @@ -204,7 +195,7 @@ function (array $results) use ($tt): void { $deserializer->addHandler( 'additional_working_times', function (array $times) use ($tt): void { - $this->importAdditionalWorkingTimes( + $this->test_results_importer->importAdditionalWorkingTimes( $times, $tt, ); @@ -234,12 +225,14 @@ function (array $times) use ($tt): void { /** * Finalize the import after all dependencies have been imported. - * It will replace the old question ids with the new question ids in the test pages. + * It will replace the old question ids with the new question ids in the test pages and remap taxonomy IDs in random + * question set source pool definitions. */ public function finalize(ilImportMapping $mapping): void { $this->log->info('Finalizing test import...'); $this->questions_importer->finalizeQuestionPages($mapping); + $this->random_test_config_importer->finalizeTaxonomyFilters($mapping); $this->log->info('...Finished finalizing test'); } @@ -393,78 +386,12 @@ private function importQuestionSetConfig( ilObjTest $test_object ): void { $config = $tt->denormalize($normalized, QuestionSetConfig::class); - if (!$config->isRandom()) { - return; - } - - $config->getConfig()->saveToDb(); - $this->log->debug("Imported random question set config for test {$test_object->getTestId()}"); - - foreach ($config->getStagingPools() as $pool_id => $questions) { - $this->importRandomQuestionStagingPool($pool_id, $questions, $mapping, $test_object); - } - - foreach ($config->getDefinitions() as $definition) { - $this->importSourcePoolDefinition($definition, $mapping); - } - } - private function importRandomQuestionStagingPool( - int $old_pool_id, - array $questions, - ilImportMapping $mapping, - ilObjTest $test_object - ): void { - $new_pool_id = $this->database->nextId('object_data'); - $mapping->addMapping( - 'components/ILIAS/Test', - 'pool', - (string) $old_pool_id, - (string) $new_pool_id - ); - $this->log->debug("Imported random question staging pool: {$old_pool_id} -> {$new_pool_id}"); - - // QuestionID was mapped during question set config denormalization - foreach ($questions as $question_id) { - $question = new ilTestRandomQuestionSetStagingPoolQuestion($this->database); - $question->setTestId($test_object->getTestId()); - $question->setPoolId($new_pool_id); - $question->setQuestionId($question_id); - $question->saveQuestionStaging(); - $this->log->debug("Imported random question staging question: {$question_id}"); + if ($config->isRandom()) { + $this->random_test_config_importer->import($config, $mapping, $test_object); } } - private function importSourcePoolDefinition( - ilTestRandomQuestionSetSourcePoolDefinition $definition, - ilImportMapping $mapping, - ): void { - // New PoolID was not available during denormalization, so we have to map it here - $old_pool_id = $definition->getPoolId(); - $new_pool_id = (int) $mapping->getMapping('components/ILIAS/Test', 'pool', (string) $old_pool_id); - $definition->setPoolId($new_pool_id); - - if ($old_pool_id !== $new_pool_id) { - $ref_ids = $this->data_factory->objId($new_pool_id)->toReferenceIds(); - if (count($ref_ids) > 0) { - $definition->setPoolRefId(current($ref_ids)->toInt()); - $this->log->debug("Derived source pool definition from Object ID: {$old_pool_id} -> {$definition->getPoolRefId()}"); - } - } - - $old_definition_id = $definition->getId(); - $definition->setId(0); - $definition->saveToDb(); - $this->log->debug("Imported source pool definition: {$old_definition_id} -> {$definition->getId()}"); - - $mapping->addMapping( - 'components/ILIAS/Test', - 'rnd_src_pool_def', - (string) $old_definition_id, - (string) $definition->getId() - ); - } - private function importParticipants(array $list, Transformations $tt, ilImportMapping $mapping): void { foreach ($list as $normalized) { @@ -518,214 +445,4 @@ private function importInvitedParticipant(array $normalized, Transformations $tt ]); $this->log->debug("Stored invited participant in database: {$participant->getUserId()} (User ID), {$participant->getTestId()} (Test ID)"); } - - /* - Results - */ - - private function importGroupedResults(array $list, Transformations $tt): void - { - foreach ($list as $set) { - foreach ($set as $name => $data) { - match($name) { - 'sequences' => $this->importTestSequences($data, $tt), - 'solutions' => $this->importSolutions($data, $tt), - 'results' => $this->importQuestionResults($data, $tt), - 'attempts' => $this->importAttemptResults($data, $tt), - 'test_result' => $this->importTestResult($data, $tt), - 'working_times' => $this->importWorkingTimes($data, $tt), - 'manual_feedback' => $this->importManualFeedback($data, $tt), - 'manual_scoring' => $this->importManualScoring($data, $tt), - default => $this->log->warning("Invalid result type: {$name}"), - }; - } - } - } - - private function importTestSequences(array $list, Transformations $tt): void - { - foreach ($list as $normalized) { - // ActiveID and QuestionIDs will be replaced by the mapping pipe - $sequence = $tt->denormalize($normalized, ilTestSequence::class); - $sequence->saveToDb(); - $this->log->debug("Stored test sequence in database: {$sequence->getActiveId()} (Active ID), {$sequence->getPass()} (Pass)"); - } - } - - private function importSolutions(array $list, Transformations $tt): void - { - foreach ($list as $normalized) { - // ActiveID and QuestionID will be replaced by the mapping pipe - $solution = $tt->denormalize($normalized, Solution::class); - - $next_id = $this->database->nextId('tst_solutions'); - $this->database->insert( - 'tst_solutions', - [ - 'solution_id' => [ilDBConstants::T_INTEGER, $next_id], - 'active_fi' => [ilDBConstants::T_INTEGER, $solution->active_id->getId()], - 'question_fi' => [ilDBConstants::T_INTEGER, $solution->question_id->getId()], - 'pass' => [ilDBConstants::T_INTEGER, $solution->attempt], - 'value1' => [ilDBConstants::T_TEXT, $solution->value1 !== null ? (string) $solution->value1 : null], - 'value2' => [ilDBConstants::T_TEXT, $solution->value2], - 'points' => [ilDBConstants::T_FLOAT, $solution->points], - 'step' => [ilDBConstants::T_INTEGER, $solution->step], - 'authorized' => [ilDBConstants::T_INTEGER, $solution->authorized ? 1 : 0], - 'tstamp' => [ilDBConstants::T_INTEGER, time()], - ] - ); - $this->log->debug("Stored solution in database: {$next_id}"); - } - } - - private function importQuestionResults(array $list, Transformations $tt): void - { - foreach ($list as $normalized) { - // ActiveID and QuestionID will be replaced by the mapping pipe - $result = $tt->denormalize($normalized, QuestionResult::class); - - $next_id = $this->database->nextId('tst_test_result'); - $this->database->insert( - 'tst_test_result', - [ - 'test_result_id' => [ilDBConstants::T_INTEGER, $next_id], - 'active_fi' => [ilDBConstants::T_INTEGER, $result->active_id->getId()], - 'question_fi' => [ilDBConstants::T_INTEGER, $result->question_id->getId()], - 'pass' => [ilDBConstants::T_INTEGER, $result->attempt], - 'points' => [ilDBConstants::T_FLOAT, $result->points], - 'manual' => [ilDBConstants::T_INTEGER, $result->manual ? 1 : 0], - 'tstamp' => [ilDBConstants::T_INTEGER, time()], - 'answered' => [ilDBConstants::T_INTEGER, $result->answered ? 1 : 0], - 'step' => [ilDBConstants::T_INTEGER, $result->step], - ] - ); - $this->log->debug("Stored question result in database: {$next_id}"); - } - } - private function importAttemptResults(array $list, Transformations $tt): void - { - foreach ($list as $normalized) { - // ActiveID will be replaced by the mapping pipe - $attempt = $tt->denormalize($normalized, AttemptResult::class); - - $this->database->insert( - 'tst_pass_result', - [ - 'active_fi' => [ilDBConstants::T_INTEGER, $attempt->getActiveId()], - 'pass' => [ilDBConstants::T_INTEGER, $attempt->getAttempt()], - 'maxpoints' => [ilDBConstants::T_FLOAT, $attempt->getMaxPoints()], - 'points' => [ilDBConstants::T_FLOAT, $attempt->getReachedPoints()], - 'questioncount' => [ilDBConstants::T_INTEGER, $attempt->getQuestionCount()], - 'answeredquestions' => [ilDBConstants::T_INTEGER, $attempt->getAnsweredQuestions()], - 'workingtime' => [ilDBConstants::T_INTEGER, $attempt->getWorkingTime()], - 'exam_id' => [ilDBConstants::T_TEXT, $attempt->getExamId()], - 'finalized_by' => [ilDBConstants::T_TEXT, $attempt->getFinalizedBy()], - 'tstamp' => [ilDBConstants::T_INTEGER, time()], - ] - ); - $this->log->debug("Stored attempt result in database: {$attempt->getActiveId()} (Active ID), {$attempt->getAttempt()} (Pass)"); - } - } - - private function importTestResult(?array $normalized, Transformations $tt): void - { - if ($normalized === null) { - $this->log->warning("Missing test result, skipping"); - return; - } - - // ActiveID will be replaced by the mapping pipe - $result = $tt->denormalize($normalized, ParticipantResult::class); - - $this->database->insert( - 'tst_result_cache', - [ - 'active_fi' => [ilDBConstants::T_INTEGER, $result->getActiveId()], - 'pass' => [ilDBConstants::T_INTEGER, $result->getAttempt()], - 'max_points' => [ilDBConstants::T_FLOAT, $result->getMaxPoints()], - 'reached_points' => [ilDBConstants::T_FLOAT, $result->getReachedPoints()], - 'mark_short' => [ilDBConstants::T_TEXT, $result->getMark()->getShortName()], - 'mark_official' => [ilDBConstants::T_TEXT, $result->getMark()->getOfficialName()], - 'passed' => [ilDBConstants::T_INTEGER, $result->isPassed() ? 1 : 0], - 'failed' => [ilDBConstants::T_INTEGER, $result->isFailed() ? 1 : 0], - 'tstamp' => [ilDBConstants::T_INTEGER, time()], - ] - ); - $this->log->debug("Stored test result in database: {$result->getActiveId()} (Active ID), {$result->getAttempt()} (Pass)"); - } - - private function importWorkingTimes(array $list, Transformations $tt): void - { - foreach ($list as $normalized) { - // ActiveID will be replaced by the mapping pipe - $working_time = $tt->denormalize($normalized, WorkingTime::class); - - $next_id = $this->database->nextId('tst_times'); - $this->database->insert( - 'tst_times', - [ - 'times_id' => [ilDBConstants::T_INTEGER, $next_id], - 'active_fi' => [ilDBConstants::T_INTEGER, $working_time->active_id->getId()], - 'pass' => [ilDBConstants::T_INTEGER, $working_time->attempt], - 'started' => [ilDBConstants::T_TIMESTAMP, $working_time->started], - 'finished' => [ilDBConstants::T_TIMESTAMP, $working_time->finished], - 'tstamp' => [ilDBConstants::T_INTEGER, time()], - ] - ); - $this->log->debug("Stored working time in database: {$next_id}"); - } - } - - private function importManualFeedback(array $list, Transformations $tt): void - { - foreach ($list as $normalized) { - // ActiveID, QuestionID and UserID will be replaced by the mapping pipe - $manual_feedback = $tt->denormalize($normalized, ManualFeedback::class); - - $next_id = $this->database->nextId('tst_manual_fb'); - $this->database->insert( - 'tst_manual_fb', - [ - 'manual_feedback_id' => [ilDBConstants::T_INTEGER, $next_id], - 'active_fi' => [ilDBConstants::T_INTEGER, $manual_feedback->active_id->getId()], - 'question_fi' => [ilDBConstants::T_INTEGER, $manual_feedback->question_id->getId()], - 'pass' => [ilDBConstants::T_INTEGER, $manual_feedback->attempt], - 'feedback' => [ilDBConstants::T_TEXT, $manual_feedback->feedback], - 'finalized_evaluation' => [ilDBConstants::T_INTEGER, $manual_feedback->finalized_evaluation ? 1 : 0], - 'finalized_timestamp' => [ilDBConstants::T_INTEGER, $manual_feedback->finalized_timestamp], - 'finalized_by_usr_id' => [ilDBConstants::T_INTEGER, $manual_feedback->finalized_by->getId()], - 'tstamp' => [ilDBConstants::T_INTEGER, time()], - ] - ); - $this->log->debug("Stored manual feedback in database: {$next_id}"); - } - } - - private function importManualScoring(array $normalized, Transformations $tt): void - { - // ActiveID will be replaced by the mapping pipe - $active_id = $tt->denormalize($normalized['active_id'], Id::class)->getId(); - - new TestManScoringDoneHelper()->setDone($active_id, $tt->bool($normalized['done'])); - $this->log->debug("Stored manual scoring in database: {$active_id} (Active ID)"); - } - - private function importAdditionalWorkingTimes(array $list, Transformations $tt): void - { - foreach ($list as $normalized) { - // UserID and TestID will be replaced by the mapping pipe - $time = $tt->denormalize($normalized, AdditionalWorkingTime::class); - - $this->database->insert( - 'tst_addtime', - [ - 'additionaltime' => [ilDBConstants::T_INTEGER, $time->time], - 'user_fi' => [ilDBConstants::T_INTEGER, $time->user_id->getId()], - 'test_fi' => [ilDBConstants::T_INTEGER, $time->test_id->getId()], - 'tstamp' => [ilDBConstants::T_TIMESTAMP, $time->timestamp], - ] - ); - $this->log->debug("Stored additional working time in database: {$time->user_id->getId()} (User ID)"); - } - } } diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php new file mode 100644 index 000000000000..ffceef4aa72f --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php @@ -0,0 +1,258 @@ + $data) { + match($name) { + 'sequences' => $this->importTestSequences($data, $tt), + 'solutions' => $this->importSolutions($data, $tt), + 'results' => $this->importQuestionResults($data, $tt), + 'attempts' => $this->importAttemptResults($data, $tt), + 'test_result' => $this->importTestResult($data, $tt), + 'working_times' => $this->importWorkingTimes($data, $tt), + 'manual_feedback' => $this->importManualFeedback($data, $tt), + 'manual_scoring' => $this->importManualScoring($data, $tt), + default => $this->log->warning("Invalid result type: {$name}"), + }; + } + } + } + + public function importTestSequences(array $list, Transformations $tt): void + { + foreach ($list as $normalized) { + // ActiveID and QuestionIDs will be replaced by the mapping pipe + $sequence = $tt->denormalize($normalized, ilTestSequence::class); + $sequence->saveToDb(); + $this->log->debug("Stored test sequence in database: {$sequence->getActiveId()} (Active ID), {$sequence->getPass()} (Pass)"); + } + } + + public function importSolutions(array $list, Transformations $tt): void + { + foreach ($list as $normalized) { + // ActiveID and QuestionID will be replaced by the mapping pipe + $solution = $tt->denormalize($normalized, Solution::class); + + $next_id = $this->database->nextId('tst_solutions'); + $this->database->insert( + 'tst_solutions', + [ + 'solution_id' => [ilDBConstants::T_INTEGER, $next_id], + 'active_fi' => [ilDBConstants::T_INTEGER, $solution->active_id->getId()], + 'question_fi' => [ilDBConstants::T_INTEGER, $solution->question_id->getId()], + 'pass' => [ilDBConstants::T_INTEGER, $solution->attempt], + 'value1' => [ilDBConstants::T_TEXT, $solution->value1 !== null ? (string) $solution->value1 : null], + 'value2' => [ilDBConstants::T_TEXT, $solution->value2], + 'points' => [ilDBConstants::T_FLOAT, $solution->points], + 'step' => [ilDBConstants::T_INTEGER, $solution->step], + 'authorized' => [ilDBConstants::T_INTEGER, $solution->authorized ? 1 : 0], + 'tstamp' => [ilDBConstants::T_INTEGER, time()], + ] + ); + $this->log->debug("Stored solution in database: {$next_id}"); + } + } + + public function importQuestionResults(array $list, Transformations $tt): void + { + foreach ($list as $normalized) { + // ActiveID and QuestionID will be replaced by the mapping pipe + $result = $tt->denormalize($normalized, QuestionResult::class); + + $next_id = $this->database->nextId('tst_test_result'); + $this->database->insert( + 'tst_test_result', + [ + 'test_result_id' => [ilDBConstants::T_INTEGER, $next_id], + 'active_fi' => [ilDBConstants::T_INTEGER, $result->active_id->getId()], + 'question_fi' => [ilDBConstants::T_INTEGER, $result->question_id->getId()], + 'pass' => [ilDBConstants::T_INTEGER, $result->attempt], + 'points' => [ilDBConstants::T_FLOAT, $result->points], + 'manual' => [ilDBConstants::T_INTEGER, $result->manual ? 1 : 0], + 'tstamp' => [ilDBConstants::T_INTEGER, time()], + 'answered' => [ilDBConstants::T_INTEGER, $result->answered ? 1 : 0], + 'step' => [ilDBConstants::T_INTEGER, $result->step], + ] + ); + $this->log->debug("Stored question result in database: {$next_id}"); + } + } + public function importAttemptResults(array $list, Transformations $tt): void + { + foreach ($list as $normalized) { + // ActiveID will be replaced by the mapping pipe + $attempt = $tt->denormalize($normalized, AttemptResult::class); + + $this->database->insert( + 'tst_pass_result', + [ + 'active_fi' => [ilDBConstants::T_INTEGER, $attempt->getActiveId()], + 'pass' => [ilDBConstants::T_INTEGER, $attempt->getAttempt()], + 'maxpoints' => [ilDBConstants::T_FLOAT, $attempt->getMaxPoints()], + 'points' => [ilDBConstants::T_FLOAT, $attempt->getReachedPoints()], + 'questioncount' => [ilDBConstants::T_INTEGER, $attempt->getQuestionCount()], + 'answeredquestions' => [ilDBConstants::T_INTEGER, $attempt->getAnsweredQuestions()], + 'workingtime' => [ilDBConstants::T_INTEGER, $attempt->getWorkingTime()], + 'exam_id' => [ilDBConstants::T_TEXT, $attempt->getExamId()], + 'finalized_by' => [ilDBConstants::T_TEXT, $attempt->getFinalizedBy()], + 'tstamp' => [ilDBConstants::T_INTEGER, time()], + ] + ); + $this->log->debug("Stored attempt result in database: {$attempt->getActiveId()} (Active ID), {$attempt->getAttempt()} (Pass)"); + } + } + + public function importTestResult(?array $normalized, Transformations $tt): void + { + if ($normalized === null) { + $this->log->warning("Missing test result, skipping"); + return; + } + + // ActiveID will be replaced by the mapping pipe + $result = $tt->denormalize($normalized, ParticipantResult::class); + + $this->database->insert( + 'tst_result_cache', + [ + 'active_fi' => [ilDBConstants::T_INTEGER, $result->getActiveId()], + 'pass' => [ilDBConstants::T_INTEGER, $result->getAttempt()], + 'max_points' => [ilDBConstants::T_FLOAT, $result->getMaxPoints()], + 'reached_points' => [ilDBConstants::T_FLOAT, $result->getReachedPoints()], + 'mark_short' => [ilDBConstants::T_TEXT, $result->getMark()->getShortName()], + 'mark_official' => [ilDBConstants::T_TEXT, $result->getMark()->getOfficialName()], + 'passed' => [ilDBConstants::T_INTEGER, $result->isPassed() ? 1 : 0], + 'failed' => [ilDBConstants::T_INTEGER, $result->isFailed() ? 1 : 0], + 'tstamp' => [ilDBConstants::T_INTEGER, time()], + ] + ); + $this->log->debug("Stored test result in database: {$result->getActiveId()} (Active ID), {$result->getAttempt()} (Pass)"); + } + + public function importWorkingTimes(array $list, Transformations $tt): void + { + foreach ($list as $normalized) { + // ActiveID will be replaced by the mapping pipe + $working_time = $tt->denormalize($normalized, WorkingTime::class); + + $next_id = $this->database->nextId('tst_times'); + $this->database->insert( + 'tst_times', + [ + 'times_id' => [ilDBConstants::T_INTEGER, $next_id], + 'active_fi' => [ilDBConstants::T_INTEGER, $working_time->active_id->getId()], + 'pass' => [ilDBConstants::T_INTEGER, $working_time->attempt], + 'started' => [ilDBConstants::T_TIMESTAMP, $working_time->started], + 'finished' => [ilDBConstants::T_TIMESTAMP, $working_time->finished], + 'tstamp' => [ilDBConstants::T_INTEGER, time()], + ] + ); + $this->log->debug("Stored working time in database: {$next_id}"); + } + } + + public function importManualFeedback(array $list, Transformations $tt): void + { + foreach ($list as $normalized) { + // ActiveID, QuestionID and UserID will be replaced by the mapping pipe + $manual_feedback = $tt->denormalize($normalized, ManualFeedback::class); + + $next_id = $this->database->nextId('tst_manual_fb'); + $this->database->insert( + 'tst_manual_fb', + [ + 'manual_feedback_id' => [ilDBConstants::T_INTEGER, $next_id], + 'active_fi' => [ilDBConstants::T_INTEGER, $manual_feedback->active_id->getId()], + 'question_fi' => [ilDBConstants::T_INTEGER, $manual_feedback->question_id->getId()], + 'pass' => [ilDBConstants::T_INTEGER, $manual_feedback->attempt], + 'feedback' => [ilDBConstants::T_TEXT, $manual_feedback->feedback], + 'finalized_evaluation' => [ilDBConstants::T_INTEGER, $manual_feedback->finalized_evaluation ? 1 : 0], + 'finalized_timestamp' => [ilDBConstants::T_INTEGER, $manual_feedback->finalized_timestamp], + 'finalized_by_usr_id' => [ilDBConstants::T_INTEGER, $manual_feedback->finalized_by->getId()], + 'tstamp' => [ilDBConstants::T_INTEGER, time()], + ] + ); + $this->log->debug("Stored manual feedback in database: {$next_id}"); + } + } + + public function importManualScoring(array $normalized, Transformations $tt): void + { + // ActiveID will be replaced by the mapping pipe + $active_id = $tt->denormalize($normalized['active_id'], Id::class)->getId(); + + new TestManScoringDoneHelper()->setDone($active_id, $tt->bool($normalized['done'])); + $this->log->debug("Stored manual scoring in database: {$active_id} (Active ID)"); + } + + public function importAdditionalWorkingTimes(array $list, Transformations $tt): void + { + foreach ($list as $normalized) { + // UserID and TestID will be replaced by the mapping pipe + $time = $tt->denormalize($normalized, AdditionalWorkingTime::class); + + $this->database->insert( + 'tst_addtime', + [ + 'additionaltime' => [ilDBConstants::T_INTEGER, $time->time], + 'user_fi' => [ilDBConstants::T_INTEGER, $time->user_id->getId()], + 'test_fi' => [ilDBConstants::T_INTEGER, $time->test_id->getId()], + 'tstamp' => [ilDBConstants::T_TIMESTAMP, $time->timestamp], + ] + ); + $this->log->debug("Stored additional working time in database: {$time->user_id->getId()} (User ID)"); + } + } +} diff --git a/components/ILIAS/Test/src/TestDIC.php b/components/ILIAS/Test/src/TestDIC.php index c10aeb344111..81ed3b4b400b 100755 --- a/components/ILIAS/Test/src/TestDIC.php +++ b/components/ILIAS/Test/src/TestDIC.php @@ -21,6 +21,8 @@ namespace ILIAS\Test; use ILIAS\LegalDocuments\ConsumerToolbox\Setting; +use ILIAS\Test\ExportImport\Import\RandomTestConfigImporter; +use ILIAS\Test\ExportImport\Import\TestResultsImporter; use ILIAS\Test\ExportImport\TestExporter; use ILIAS\Test\Participants\ParticipantRepository; use ILIAS\Test\Results\Data\Repository as TestResultRepository; @@ -298,6 +300,17 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC->fileConverters()->images(), $DIC->filesystem() ); + $dic['exportimport.test_results_importer'] = static fn($c): TestResultsImporter => + new TestResultsImporter( + $DIC->database(), + $c['exportimport.logging']() + ); + $dic['exportimport.random_test_config_importer'] = static fn($c): RandomTestConfigImporter => + new RandomTestConfigImporter( + $DIC->database(), + $c['exportimport.logging'](), + new DataFactory() + ); $dic['exportimport.importer'] = static fn($c): TestImporter => new TestImporter( $c['exportimport.builder'], @@ -306,6 +319,8 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC->resourceStorage(), new DataFactory(), $c['exportimport.questions_importer'], + $c['exportimport.random_test_config_importer'], + $c['exportimport.test_results_importer'], $c['exportimport.skill_assignments_importer'], $c['exportimport.skill_level_thresholds_importer'], $c['marks.repository'] From 861f6036e2b2eec9edc25d536f9425f3d419126d Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Thu, 7 May 2026 11:23:30 +0200 Subject: [PATCH 38/66] fix(test): support random test question reference export/import --- ...tRandomQuestionSetSourcePoolDefinition.php | 2 +- .../Envelopes/RandomTestQuestion.php | 80 +++++++++++++++++++ .../Import/TestResultsImporter.php | 25 ++++++ .../Test/src/ExportImport/TestCollector.php | 26 +++++- 4 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 components/ILIAS/Test/src/ExportImport/Envelopes/RandomTestQuestion.php diff --git a/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php b/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php index f53ed0238826..af218dfcdfbc 100755 --- a/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php +++ b/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php @@ -482,7 +482,7 @@ public function toNormalized(Transformations $tt): Transformation { return $tt->custom()->transformation(function () use ($tt): array { $normalized = [ - 'id' => $tt->normalize(new Id($this->getId(), static::class)), + 'id' => $tt->normalize(new Id($this->getId(), 'rnd_src_pool_def')), 'pool_id' => $tt->normalize(new Id($this->getPoolId(), 'qpl')), 'pool_title' => $this->getPoolTitle(), 'pool_path' => $this->getPoolPath(), diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/RandomTestQuestion.php b/components/ILIAS/Test/src/ExportImport/Envelopes/RandomTestQuestion.php new file mode 100644 index 000000000000..e759fa26f9ed --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/Envelopes/RandomTestQuestion.php @@ -0,0 +1,80 @@ + $tt->normalize($this->active_id), + 'question_id' => $tt->normalize($this->question_id), + 'sequence' => $this->sequence, + 'pass' => $this->pass, + 'timestamp' => $this->timestamp, + 'src_pool_def_id' => $tt->normalize($this->src_pool_def_id), + ]; + } + + /** + * @inheritDoc + */ + public static function fromArray(array $value, Transformations $tt): static + { + return new self( + $tt->denormalize($value['active_id'], Id::class), + $tt->denormalize($value['question_id'], Id::class), + $tt->int($value['sequence']), + $tt->int($value['pass']), + $tt->int($value['timestamp']), + $tt->denormalize($value['src_pool_def_id'], Id::class), + ); + } + + public static function fromRow(array $row): static + { + return new self( + new Id($row['active_fi'], 'participant'), + new Id($row['question_fi'], 'question'), + (int) $row['sequence'], + (int) $row['pass'], + (int) $row['tstamp'], + new Id($row['src_pool_def_fi'], 'rnd_src_pool_def'), + ); + } +} diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php index ffceef4aa72f..e01733bd9ca9 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php @@ -25,6 +25,7 @@ use ILIAS\Test\ExportImport\Envelopes\AdditionalWorkingTime; use ILIAS\Test\ExportImport\Envelopes\ManualFeedback; use ILIAS\Test\ExportImport\Envelopes\QuestionResult; +use ILIAS\Test\ExportImport\Envelopes\RandomTestQuestion; use ILIAS\Test\ExportImport\Envelopes\Solution; use ILIAS\Test\ExportImport\Envelopes\WorkingTime; use ILIAS\Test\Results\Data\AttemptResult; @@ -63,6 +64,7 @@ public function import(array $list, Transformations $tt): void 'working_times' => $this->importWorkingTimes($data, $tt), 'manual_feedback' => $this->importManualFeedback($data, $tt), 'manual_scoring' => $this->importManualScoring($data, $tt), + 'questions' => $this->importRandomTestQuestions($data, $tt), default => $this->log->warning("Invalid result type: {$name}"), }; } @@ -255,4 +257,27 @@ public function importAdditionalWorkingTimes(array $list, Transformations $tt): $this->log->debug("Stored additional working time in database: {$time->user_id->getId()} (User ID)"); } } + + public function importRandomTestQuestions(array $list, Transformations $tt): void + { + foreach ($list as $normalized) { + // ActiveID, QuestionID and SourcePoolDefinitionID will be replaced by the mapping pipe + $question = $tt->denormalize($normalized, RandomTestQuestion::class); + + $next_id = $this->database->nextId('tst_test_rnd_qst'); + $this->database->insert( + 'tst_test_rnd_qst', + [ + 'test_random_question_id' => [ilDBConstants::T_INTEGER, $next_id], + 'active_fi' => [ilDBConstants::T_INTEGER, $question->active_id->getId()], + 'question_fi' => [ilDBConstants::T_INTEGER, $question->question_id->getId()], + 'sequence' => [ilDBConstants::T_INTEGER, $question->sequence], + 'pass' => [ilDBConstants::T_INTEGER, $question->pass], + 'tstamp' => [ilDBConstants::T_INTEGER, time()], + 'src_pool_def_fi' => [ilDBConstants::T_INTEGER, $question->src_pool_def_id->getId()], + ] + ); + $this->log->debug("Stored random test question in database: {$next_id}"); + } + } } diff --git a/components/ILIAS/Test/src/ExportImport/TestCollector.php b/components/ILIAS/Test/src/ExportImport/TestCollector.php index e41250590b03..538d194e3b6f 100644 --- a/components/ILIAS/Test/src/ExportImport/TestCollector.php +++ b/components/ILIAS/Test/src/ExportImport/TestCollector.php @@ -30,6 +30,7 @@ use ILIAS\Test\ExportImport\Envelopes\ManualFeedback; use ILIAS\Test\ExportImport\Envelopes\QuestionResult; use ILIAS\Test\ExportImport\Envelopes\QuestionSetConfig; +use ILIAS\Test\ExportImport\Envelopes\RandomTestQuestion; use ILIAS\Test\ExportImport\Envelopes\Solution; use ILIAS\Test\ExportImport\Envelopes\WorkingTime; use ILIAS\Test\Logging\TestLogger; @@ -45,7 +46,6 @@ use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ilObjTest; use ilTestQuestionSetConfigFactory; -use ilTestRandomQuestionSetConfig; use ilTestRandomQuestionSetSourcePoolDefinitionFactory; use ilTestRandomQuestionSetSourcePoolDefinitionList; use ilTestRandomQuestionSetStagingPoolQuestionList; @@ -303,7 +303,7 @@ public function getAdditionalParticipantData(array $participant_ids): array public function getResults(int $participant_id): array { $attempt_results = $this->results_repository->getTestAttemptResults($participant_id); - return [ + $set = [ 'sequences' => $this->getSequences($participant_id, array_keys($attempt_results)), 'solutions' => $this->getSolutions($participant_id), 'results' => $this->getQuestionResults($participant_id), @@ -316,6 +316,11 @@ public function getResults(int $participant_id): array 'done' => $this->manual_scoring->isDone($participant_id), ], ]; + + if ($this->getObject()->isRandomTest()) { + $set['questions'] = $this->getRandomTestQuestions($participant_id); + } + return $set; } /** @@ -417,4 +422,21 @@ public function getAdditionalWorkingTimes(): array $this->db->fetchAll($query) ); } + + /** + * @return list + */ + public function getRandomTestQuestions(int $participant_id): array + { + $query = $this->db->queryF( + "SELECT * FROM tst_test_rnd_qst WHERE active_fi = %s", + [ilDBConstants::T_INTEGER], + [$participant_id] + ); + + return array_map( + fn(array $row): RandomTestQuestion => RandomTestQuestion::fromRow($row), + $this->db->fetchAll($query) + ); + } } From f4366e86376b5becab4fdbcc2a4aebe6fde0e8b4 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Fri, 8 May 2026 13:59:34 +0200 Subject: [PATCH 39/66] feat(test): support legacy test import and handle redirect after import completed --- .../ILIAS/Test/classes/class.ilObjTestGUI.php | 126 +++++------------- .../classes/class.ilTestLegacyImporter.php | 89 ++++++------- .../src/ExportImport/Import/PersistStage.php | 34 +++-- .../src/ExportImport/Import/CleanupStage.php | 4 +- 4 files changed, 94 insertions(+), 159 deletions(-) diff --git a/components/ILIAS/Test/classes/class.ilObjTestGUI.php b/components/ILIAS/Test/classes/class.ilObjTestGUI.php index 45c7034315b2..50691e39491b 100755 --- a/components/ILIAS/Test/classes/class.ilObjTestGUI.php +++ b/components/ILIAS/Test/classes/class.ilObjTestGUI.php @@ -1427,13 +1427,42 @@ public function processImportObject(): void break; case StageResultType::COMPLETE: - $this->tpl->setOnScreenMessage('success', $this->lng->txt('object_imported'), true); - $this->ctrl->setParameterByClass(ilObjTestGUI::class, 'ref_id', $result->context->get('test_ref_id')); - $this->ctrl->redirectByClass(self::class, self::SHOW_QUESTIONS_CMD); + $this->afterImportCompleted($result->context); break; } } + private function afterImportCompleted(ImportContext $context): void + { + $new_obj = new ilObjTest(0, false); + $new_obj->setId($context->get('test_obj_id')); + $new_obj->setRefId($context->get('test_ref_id')); + + if ($new_obj->getTestLogger()->isLoggingEnabled()) { + $new_obj->getTestLogger()->logTestAdministrationInteraction( + $new_obj->getTestLogger()->getInteractionFactory()->buildTestAdministrationInteraction( + $new_obj->getRefId(), + $this->user->getId(), + TestAdministrationInteractionTypes::NEW_TEST_CREATED, + [] + ) + ); + } + + $question_skill_assignments_import_fails = new ilAssQuestionSkillAssignmentImportFails($new_obj->getId()); + if ($question_skill_assignments_import_fails->failedImportsRegistered()) { + $this->tpl->setOnScreenMessage( + 'info', + $question_skill_assignments_import_fails->getFailedImportsMessage($this->lng), + true + ); + } + + $this->tpl->setOnScreenMessage('success', $this->lng->txt('object_imported'), true); + $this->ctrl->setParameterByClass(ilObjTestGUI::class, 'ref_id', $new_obj->getRefId()); + $this->ctrl->redirectByClass(self::class, self::SHOW_QUESTIONS_CMD); + } + private function buildImportStageRunner(): ImportStageRunner { $form_action = $this->ctrl->getFormActionByClass(self::class, 'processImport'); @@ -1499,97 +1528,6 @@ public function getTestObject(): ?ilObjTest return $this->object; } - /** - * imports question(s) into the questionpool (after verification) - */ - public function importVerifiedFileObject( - bool $skip_retrieve_selected_questions = false - ): void { - if (!$this->checkPermissionBool('create', '', 'tst')) { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt('no_permission'), true); - $this->ctrl->returnToParent($this); - } - $file_to_import = ilSession::get('path_to_import_file'); - $path_to_uploaded_file_in_temp_dir = ilSession::get('path_to_uploaded_file_in_temp_dir'); - list($subdir, $importdir, $xmlfile, $qtifile) = $this->buildImportDirectoriesFromImportFile($file_to_import); - - $new_obj = new ilObjTest(0, true); - $new_obj->setTitle('dummy'); - $new_obj->setDescription('test import'); - $new_obj->create(true); - $new_obj->createReference(); - $new_obj->putInTree($this->testrequest->getRefId()); - $new_obj->setPermissions($this->testrequest->getRefId()); - $new_obj->saveToDb(); - - $selected_questions = []; - if (!$skip_retrieve_selected_questions) { - $selected_questions = $this->retrieveSelectedQuestionsFromImportQuestionsSelectionForm( - 'importVerifiedFile', - $importdir, - $qtifile, - $this->request - ); - } - - ilSession::set('tst_import_selected_questions', $selected_questions); - - $imp = new ilImport($this->testrequest->getRefId()); - $map = $imp->getMapping(); - $map->addMapping('components/ILIAS/Test', 'tst', 'new_id', (string) $new_obj->getId()); - - /** - * 2025-03-22, sk: This is now only needed for legacy exports as - * now also exports with results do contain a manifest.xml. - */ - if (is_file($importdir . DIRECTORY_SEPARATOR . '/manifest.xml')) { - $imp->importObject($new_obj, $file_to_import, basename($file_to_import), 'tst', 'components/ILIAS/Test', true); - } else { - $test_importer = new ilTestImporter(); - $test_importer->setImport($imp); - $test_importer->setInstallId(IL_INST_ID); - $test_importer->setImportDirectory($importdir . '/' . $subdir); - $test_importer->init(); - - $test_importer->importXmlRepresentation( - '', - '', - '', - $map, - ); - } - - if ($new_obj->getTestLogger()->isLoggingEnabled()) { - $new_obj->getTestLogger()->logTestAdministrationInteraction( - $new_obj->getTestLogger()->getInteractionFactory()->buildTestAdministrationInteraction( - $new_obj->getRefId(), - $this->user->getId(), - TestAdministrationInteractionTypes::NEW_TEST_CREATED, - [] - ) - ); - } - - ilFileUtils::delDir($importdir); - $this->deleteUploadedImportFile($path_to_uploaded_file_in_temp_dir); - ilSession::clear('path_to_import_file'); - ilSession::clear('path_to_uploaded_file_in_temp_dir'); - - $this->tpl->setOnScreenMessage('success', $this->lng->txt("object_imported"), true); - - $question_skill_assignments_import_fails = new ilAssQuestionSkillAssignmentImportFails($new_obj->getId()); - if ($question_skill_assignments_import_fails->failedImportsRegistered()) { - $this->tpl->setOnScreenMessage( - 'info', - $question_skill_assignments_import_fails->getFailedImportsMessage($this->lng), - true - ); - } - - $this->ctrl->setParameterByClass(ilObjTestGUI::class, 'ref_id', $new_obj->getRefId()); - $this->ctrl->redirectByClass(ilObjTestGUI::class); - } - /** * download file */ diff --git a/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php b/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php index 494752bc1602..8a44bafdc327 100755 --- a/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php +++ b/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php @@ -19,17 +19,15 @@ declare(strict_types=1); use ILIAS\ResourceStorage\Services as ResourceStorage; +use ILIAS\Test\RequestDataCollector; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; +use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; +use ILIAS\TestQuestionPool\ExportImport\Import\QuestionSelectionStage; +use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; use ILIAS\TestQuestionPool\Import\TestQuestionsImportTrait; use ILIAS\Test\TestDIC; use ILIAS\Test\Logging\TestLogger; -/** - * Importer class for files - * - * @author Stefan Meyer - * @version $Id$ - * @ingroup components\ILIASLearningModule - */ class ilTestLegacyImporter extends ilXmlImporter { use TestQuestionsImportTrait; @@ -41,15 +39,20 @@ class ilTestLegacyImporter extends ilXmlImporter private readonly TestLogger $logger; private readonly ilDBInterface $db; private readonly ResourceStorage $irss; + private readonly ImportSessionRepository $session; + private readonly RequestDataCollector $request_data_collector; public function __construct() { global $DIC; - $this->logger = TestDIC::dic()['logging.logger']; + parent::__construct(); + + $local_dic = TestDIC::dic(); + $this->logger = $local_dic['logging.logger']; + $this->session = $local_dic['exportimport.session']; + $this->request_data_collector = $local_dic['request_data_collector']; $this->db = $DIC['ilDB']; $this->irss = $DIC['resource_storage']; - - parent::__construct(); } public function importXmlRepresentation( @@ -58,47 +61,28 @@ public function importXmlRepresentation( string $a_xml, ilImportMapping $a_mapping ): void { - $results_file_path = null; - if ($new_id = (int) $a_mapping->getMapping('components/ILIAS/Container', 'objs', $a_id)) { - // container content - $new_obj = ilObjectFactory::getInstanceByObjId((int) $new_id, false); - $new_obj->saveToDb(); - - [$importdir, $xmlfile, $qtifile] = $this->buildImportDirectoriesFromContainerImport( - $this->getImportDirectory() - ); - $selected_questions = []; - } else { - // single object - $new_id = (int) $a_mapping->getMapping('components/ILIAS/Test', 'tst', 'new_id'); - $new_obj = ilObjectFactory::getInstanceByObjId($new_id, false); - - $selected_questions = ilSession::get('tst_import_selected_questions') ?? []; - [$subdir, $importdir, $xmlfile, $qtifile] = $this->buildImportDirectoriesFromImportFile( - ilSession::get('path_to_import_file') - ); - $results_file_path = $this->buildResultsFilePath($importdir, $subdir); - ilSession::clear('tst_import_selected_questions'); - } + $new_obj = new ilObjTest(0, true); + $new_obj->setTitle('dummy'); + $new_obj->setDescription('test import'); + $new_obj->create(true); + $new_obj->createReference(); + $new_obj->putInTree($this->request_data_collector->getRefId()); + $new_obj->setPermissions($this->request_data_collector->getRefId()); + $new_obj->saveToDb(); - $new_obj->loadFromDb(); + $a_mapping->addMapping('components/ILIAS/Test', 'tst', 'new_id', (string) $new_obj->getId()); - if (!file_exists($xmlfile)) { - $this->logger->error(__METHOD__ . ': Cannot find xml definition: ' . $xmlfile); - return; - } - if (!file_exists($qtifile)) { - $this->logger->error(__METHOD__ . ': Cannot find xml definition: ' . $qtifile); - return; - } + $context = $this->session->getContext(); + $import_base_dir = $context->get(UploadValidationStage::IMPORT_BASE_DIR); + $xml_file = $context->get(DetectLegacyImportStage::LEGACY_XML_FILE); // start parsing of QTI files $qti_parser = new ilQTIParser( - $importdir, - $qtifile, + $import_base_dir, + $context->get(DetectLegacyImportStage::LEGACY_QTI_FILE), ilQTIParser::IL_MO_PARSE_QTI, $new_obj->getId(), - $selected_questions, + $context->get('selected_questions'), $a_mapping->getAllMappings() ); $qti_parser->setTestObject($new_obj); @@ -108,8 +92,8 @@ public function importXmlRepresentation( // import page data $question_page_parser = new ilQuestionPageParser( $new_obj, - $xmlfile, - $importdir + $xml_file, + $import_base_dir ); $question_page_parser->setQuestionMapping($qti_parser->getImportMapping()); $question_page_parser->startParsing(); @@ -117,10 +101,11 @@ public function importXmlRepresentation( $a_mapping = $this->addTaxonomyAndQuestionsMapping($qti_parser->getQuestionIdMapping(), $new_obj->getId(), $a_mapping); if ($new_obj->isRandomTest()) { - $this->importRandomQuestionSetConfig($new_obj, $xmlfile, $a_mapping); + $this->importRandomQuestionSetConfig($new_obj, $xml_file, $a_mapping); } - if ($results_file_path !== null && file_exists($results_file_path)) { + $results_file_path = str_replace('__tst', '__results', $xml_file); + if (file_exists($results_file_path)) { $results = new ilTestResultsImportParser($results_file_path, $new_obj, $this->db, $this->logger, $this->irss); $results->setQuestionIdMapping($a_mapping->getMappingsOfEntity('components/ILIAS/Test', 'quest')); $results->setSrcPoolDefIdMapping($a_mapping->getMappingsOfEntity('components/ILIAS/Test', 'rnd_src_pool_def')); @@ -132,18 +117,20 @@ public function importXmlRepresentation( $this->importSkillLevelThresholds( $a_mapping, - $this->importQuestionSkillAssignments($a_mapping, $new_obj, $xmlfile), + $this->importQuestionSkillAssignments($a_mapping, $new_obj, $xml_file), $new_obj, - $xmlfile + $xml_file ); - $a_mapping->addMapping("components/ILIAS/Test", "tst", (string) $a_id, (string) $new_obj->getId()); $a_mapping->addMapping( "components/ILIAS/MetaData", "md", $a_id . ":0:tst", $new_obj->getId() . ":0:tst" ); + + $context = $context->with('test_obj_id', $new_obj->getId())->with('test_ref_id', $new_obj->getRefId()); + $this->session->setContext($context); } public function addTaxonomyAndQuestionsMapping( diff --git a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php index fd054b7a89b7..b0dad1fac42a 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php +++ b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php @@ -26,6 +26,7 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResult; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XMLFileDeserializer; +use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; use ilImport; @@ -60,20 +61,12 @@ public function getDescription(): ?string public function process(ImportContext $context): StageResult { - $component_import_dir = dirname($context->get(UploadValidationStage::COMPONENT_IMPORT_FILE)); - $mappings_file = "{$component_import_dir}/mappings.xml"; - if (!file_exists($mappings_file) || !is_file($mappings_file)) { - return StageResult::error($context, $this->lng->txt('obj_import_file_error')); + if(!DetectLegacyImportStage::isLegacyImport($context)) { + if($result = $this->importMappingsFile($context)) { + return $result; + } } - $deserializer = new XMLFileDeserializer()->open($mappings_file); - $deserializer->addHandler('mappings', function (array $mappings) use (&$context) { - $context = $context->with('mappings', $mappings); - }); - $deserializer->process(); - - $this->session->setContext($context); - $importer = new ilImport($this->requested_ref_id); $importer->importObject( null, @@ -87,4 +80,21 @@ public function process(ImportContext $context): StageResult // Context is updated by the TestImporter so we need to reload it return StageResult::complete($this->session->getContext()); } + + private function importMappingsFile(ImportContext $context): ?StageResult { + $component_import_dir = dirname($context->get(UploadValidationStage::COMPONENT_IMPORT_FILE)); + $mappings_file = "{$component_import_dir}/mappings.xml"; + if (!file_exists($mappings_file) || !is_file($mappings_file)) { + return StageResult::error($context, $this->lng->txt('obj_import_file_error')); + } + + $deserializer = new XMLFileDeserializer()->open($mappings_file); + $deserializer->addHandler('mappings', function (array $mappings) use (&$context) { + $context = $context->with('mappings', $mappings); + }); + $deserializer->process(); + + $this->session->setContext($context); + return null; + } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php index 739e3f812fce..9b926ec14f5a 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php @@ -50,13 +50,13 @@ public function process(ImportContext $context): StageResult $file_to_import = $context->get(UploadValidationStage::FILE_TO_IMPORT); if ($file_to_import !== null) { $temp_dir = dirname($file_to_import); - if (file_exists($temp_dir) && is_dir($temp_dir)) { + if ($temp_dir && file_exists($temp_dir) && is_dir($temp_dir)) { $this->removeDirectory($temp_dir); } } $import_base_dir = $context->get(UploadValidationStage::IMPORT_BASE_DIR); - if (file_exists($import_base_dir) && is_dir($import_base_dir)) { + if ($import_base_dir && file_exists($import_base_dir) && is_dir($import_base_dir)) { $this->removeDirectory($import_base_dir); } From 418e283ea2e4662d4a582eb953cd0c74a68d2b73 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Wed, 13 May 2026 09:31:55 +0200 Subject: [PATCH 40/66] refactor(test/qpl): integrate logging into import/export processes for better traceability --- .../ILIAS/Test/classes/class.ilObjTestGUI.php | 22 +++++++++++++++---- .../Test/classes/class.ilTestExporter.php | 2 +- .../src/ExportImport/Import/PersistStage.php | 12 +++++++--- .../Import/SkillLevelThresholdsImporter.php | 7 +++++- .../Test/src/ExportImport/TestExporter.php | 14 +++++++++++- components/ILIAS/Test/src/TestDIC.php | 2 ++ .../classes/class.ilObjQuestionPoolGUI.php | 21 ++++++++++++++---- .../class.ilTestQuestionPoolExporter.php | 3 +-- .../Export/QuestionPoolExporter.php | 12 ++++++++++ .../src/ExportImport/Import/CleanupStage.php | 12 ++++++++++ .../Import/DetectLegacyImportStage.php | 8 +++++++ .../Import/QuestionPoolImporter.php | 13 +++++++---- .../Import/QuestionSelectionStage.php | 10 +++++++++ .../ExportImport/Import/QuestionsImporter.php | 9 ++++++++ .../Import/SkillAssignmentsImporter.php | 5 +++++ .../Import/UploadValidationStage.php | 11 +++++++++- .../TestQuestionPool/src/QuestionPoolDIC.php | 5 +---- 17 files changed, 143 insertions(+), 25 deletions(-) diff --git a/components/ILIAS/Test/classes/class.ilObjTestGUI.php b/components/ILIAS/Test/classes/class.ilObjTestGUI.php index 50691e39491b..477e5e219f5d 100755 --- a/components/ILIAS/Test/classes/class.ilObjTestGUI.php +++ b/components/ILIAS/Test/classes/class.ilObjTestGUI.php @@ -89,6 +89,7 @@ use ILIAS\Style\Content\Service as ContentStyle; use ILIAS\User\Profile\PublicProfileGUI; use ILIAS\Test\GUIFactory; +use Psr\Log\LoggerInterface; /** * Class ilObjTestGUI @@ -205,6 +206,7 @@ class ilObjTestGUI extends ilObjectGUI implements ilCtrlBaseClassInterface, ilDe protected GUIFactory $gui_factory; protected SkillUsageService $skill_usage_service; protected ImportSessionRepository $import_session_repository; + protected LoggerInterface $import_logger; protected bool $create_question_mode; @@ -259,6 +261,7 @@ public function __construct() $this->additional_information_generator = $local_dic['logging.information_generator']; $this->personal_settings_exporter = $local_dic['settings.personal_templates.exporter']; $this->import_session_repository = $local_dic['exportimport.session']; + $this->import_logger = $local_dic['exportimport.logging'](); $ref_id = 0; if ($this->testrequest->hasRefId() && is_numeric($this->testrequest->getRefId())) { @@ -1469,20 +1472,31 @@ private function buildImportStageRunner(): ImportStageRunner return new ImportStageRunner( [ - new UploadValidationStage($this->archives, $this->lng, 'components/ILIAS/Test'), - new DetectLegacyImportStage(), + new UploadValidationStage( + $this->archives, + $this->lng, + $this->import_logger, + 'components/ILIAS/Test' + ), + new DetectLegacyImportStage($this->import_logger), new QuestionSelectionStage( $this->lng, + $this->import_logger, $this->component_factory, $this->ui_factory, $this->request, $form_action, $this->lng->txt('import_tst') ), - new PersistStage($this->lng, $this->requested_ref_id, $this->import_session_repository), + new PersistStage( + $this->lng, + $this->import_logger, + $this->requested_ref_id, + $this->import_session_repository + ), ], $this->import_session_repository, - new CleanupStage() + new CleanupStage($this->import_logger) ); } diff --git a/components/ILIAS/Test/classes/class.ilTestExporter.php b/components/ILIAS/Test/classes/class.ilTestExporter.php index 85e741c91093..27ef39a891c4 100755 --- a/components/ILIAS/Test/classes/class.ilTestExporter.php +++ b/components/ILIAS/Test/classes/class.ilTestExporter.php @@ -34,7 +34,7 @@ public function init(): void $this->export_handler = new ExportHandler(); $this->state_holder = $local_dic['exportimport.state_holder']; $this->exporter = $local_dic['exportimport.exporter']; - $this->logger = $local_dic['logging.logger']; + $this->logger = $local_dic['exportimport.logging'](); } /** diff --git a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php index b0dad1fac42a..14b68aba884f 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php +++ b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php @@ -29,6 +29,7 @@ use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; use ilImport; +use Psr\Log\LoggerInterface; /** * Final stage of the test import process. Imports the head dependencies (user and resource mappings) and then @@ -39,6 +40,7 @@ class PersistStage implements ImportStage { public function __construct( private readonly Language $lng, + private readonly LoggerInterface $log, private readonly int $requested_ref_id, private readonly ImportSessionRepository $session ) { @@ -61,8 +63,8 @@ public function getDescription(): ?string public function process(ImportContext $context): StageResult { - if(!DetectLegacyImportStage::isLegacyImport($context)) { - if($result = $this->importMappingsFile($context)) { + if (!DetectLegacyImportStage::isLegacyImport($context)) { + if ($result = $this->importMappingsFile($context)) { return $result; } } @@ -81,10 +83,12 @@ public function process(ImportContext $context): StageResult return StageResult::complete($this->session->getContext()); } - private function importMappingsFile(ImportContext $context): ?StageResult { + private function importMappingsFile(ImportContext $context): ?StageResult + { $component_import_dir = dirname($context->get(UploadValidationStage::COMPONENT_IMPORT_FILE)); $mappings_file = "{$component_import_dir}/mappings.xml"; if (!file_exists($mappings_file) || !is_file($mappings_file)) { + $this->log->error("Mappings file not found: {$mappings_file}"); return StageResult::error($context, $this->lng->txt('obj_import_file_error')); } @@ -92,7 +96,9 @@ private function importMappingsFile(ImportContext $context): ?StageResult { $deserializer->addHandler('mappings', function (array $mappings) use (&$context) { $context = $context->with('mappings', $mappings); }); + $deserializer->process(); + $this->log->info("Processed mappings file: {$mappings_file}"); $this->session->setContext($context); return null; diff --git a/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php b/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php index 0c9cf976ba0c..697e0f32e202 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php @@ -20,13 +20,13 @@ namespace ILIAS\Test\ExportImport\Import; -use ilBasicSkill; use ilDBInterface; use ilImportMapping; use ilSkillTreeRepository; use ilTestSkillLevelThreshold; use ilTestSkillLevelThresholdList; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use Psr\Log\LoggerInterface; /** * Imports skill level thresholds from normalized data. It maps imported skill level ids to local skill level ids using @@ -40,6 +40,7 @@ class SkillLevelThresholdsImporter { public function __construct( + private readonly LoggerInterface $log, private readonly ilDBInterface $db, private readonly ilSkillTreeRepository $skill_repo, private readonly string $component, @@ -69,9 +70,11 @@ public function import( $local_level_id = $this->getLevelIdMapping($import_install_id, $threshold->getSkillLevelId()); if ($local_level_id === null) { + $this->log->warning("Failed to find skill level id mapping for threshold: {$threshold->getSkillLevelId()}"); $result['failed'][] = $this->buildResultData($threshold); continue; } + $this->log->debug("Found skill level id mapping for threshold: {$threshold->getSkillLevelId()} -> {$local_level_id}"); $mapping->addMapping( $this->component, @@ -86,6 +89,8 @@ public function import( } $threshold_list->saveToDb(); + $this->log->debug("Saved skill level thresholds"); + return $result; } diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index 553e0c9b37c3..8df3d140781f 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -71,6 +71,7 @@ public function __construct( */ public function prepare(ExportState $state): void { + $state->logger()->info('Preparing test export (1/3)...'); $state->assertStep(ExportStep::INIT); $state->setStep(ExportStep::PREPARE); @@ -108,6 +109,7 @@ public function prepare(ExportState $state): void ->create(); $state->setTransformations($transformations); + $state->logger()->info('...Finished preparing test export (1/3)'); } private function extractObjectId(ExportState $state): ?ObjectId @@ -133,6 +135,7 @@ private function extractObjectId(ExportState $state): ?ObjectId */ public function process(ExportState $state): void { + $state->logger()->info('Processing test export (2/3)...'); $state->assertStep(ExportStep::PREPARE); $state->setStep(ExportStep::PROCESS); @@ -197,8 +200,12 @@ public function process(ExportState $state): void ); if ($state->getOption() === Types::XML_WITH_RESULTS->value) { + $state->logger()->info('Processing test results export ...'); $this->processResults($state); + $state->logger()->info('...Finished processing test results export'); } + + $state->logger()->info('...Finished processing test export (2/3)'); } private function processResults(ExportState $state): void @@ -226,6 +233,7 @@ private function processResults(ExportState $state): void */ public function write(ExportState $state): void { + $state->logger()->info('Writing test export (3/3)...'); $state->assertStep(ExportStep::PROCESS); $state->setStep(ExportStep::WRITE); @@ -239,6 +247,7 @@ public function write(ExportState $state): void $file['from'], "{$export_dir}/" . $file['to'] ); + $state->logger()->debug("Copied question image {$file['from']} to {$export_dir}/{$file['to']}"); } else { $state->logger()->warning('Question image file not found: ' . $file['from']); } @@ -252,7 +261,7 @@ public function write(ExportState $state): void $id, "{$export_dir}/resources/{$file}" ); - + $state->logger()->debug("Copied resource {$id} to {$export_dir}/resources/{$file}"); } $this->writeMappings( @@ -260,6 +269,9 @@ public function write(ExportState $state): void $state->transformations(), $state ); + $state->logger()->debug('Stored test export mappings'); + + $state->logger()->info('...Finished writing test export (3/3)'); } diff --git a/components/ILIAS/Test/src/TestDIC.php b/components/ILIAS/Test/src/TestDIC.php index 81ed3b4b400b..e8641b1a9b71 100755 --- a/components/ILIAS/Test/src/TestDIC.php +++ b/components/ILIAS/Test/src/TestDIC.php @@ -275,6 +275,7 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic['exportimport.skill_assignments_importer'] = static fn($c): SkillAssignmentsImporter => new SkillAssignmentsImporter( + $c['exportimport.logging'](), $DIC->skills()->internal()->repo()->getTreeRepo(), $DIC->skills()->usage(), 'components/ILIAS/Test', @@ -283,6 +284,7 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic['exportimport.skill_level_thresholds_importer'] = static fn($c): SkillLevelThresholdsImporter => new SkillLevelThresholdsImporter( + $c['exportimport.logging'](), $DIC->database(), $DIC->skills()->internal()->repo()->getTreeRepo(), 'components/ILIAS/Test', diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php index 8d9651b03ebf..f4a987803a35 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php @@ -47,6 +47,7 @@ use ILIAS\FileUpload\MimeType; use ILIAS\UI\Component\Modal\RoundTrip as RoundTripModal; use ILIAS\Style\Content\Service as ContentStyle; +use Psr\Log\LoggerInterface; /** * Class ilObjQuestionPoolGUI @@ -105,6 +106,7 @@ class ilObjQuestionPoolGUI extends ilObjectGUI implements ilCtrlBaseClassInterfa protected GeneralQuestionPropertiesRepository $questionrepository; protected GlobalTestSettings $global_test_settings; protected ImportSessionRepository $import_session_repository; + protected LoggerInterface $import_logger; public function __construct() { @@ -132,6 +134,7 @@ public function __construct() $this->questionrepository = $local_dic['question.general_properties.repository']; $this->global_test_settings = $local_dic['global_test_settings']; $this->import_session_repository = $local_dic['exportimport.session']; + $this->import_logger = $local_dic['exportimport.logging'](); parent::__construct('', $this->request_data_collector->getRefId(), true, false); @@ -1143,20 +1146,30 @@ private function buildImportStageRunner(): ImportStageRunner return new ImportStageRunner( [ - new UploadValidationStage($this->archives, $this->lng, 'components/ILIAS/TestQuestionPool'), - new DetectLegacyImportStage(), + new UploadValidationStage( + $this->archives, + $this->lng, + $this->import_logger, + 'components/ILIAS/TestQuestionPool' + ), + new DetectLegacyImportStage($this->import_logger), new QuestionSelectionStage( $this->lng, + $this->import_logger, $this->component_factory, $this->ui_factory, $this->request, $form_action, $this->lng->txt('import_qpl') ), - new PersistStage($this->lng, $this->request_data_collector, $this->import_session_repository), + new PersistStage( + $this->lng, + $this->request_data_collector, + $this->import_session_repository + ), ], $this->import_session_repository, - new CleanupStage() + new CleanupStage($this->import_logger) ); } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php index ba4e24df7c65..54e4513695f7 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php @@ -26,13 +26,12 @@ class ilTestQuestionPoolExporter extends ilXmlExporter public function init(): void { - global $DIC; $local_dic = QuestionPoolDIC::dic(); $this->export_handler = new ExportHandler(); $this->state_holder = $local_dic['exportimport.state_holder']; $this->exporter = $local_dic['exportimport.exporter']; - $this->logger = $DIC->logger()->qpl()->getLogger(); + $this->logger = $local_dic['exportimport.logging'](); } /** diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php index b4d72eab3b86..f472d0a43f81 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php @@ -56,11 +56,13 @@ public function __construct( */ public function prepare(ExportState $state): void { + $state->logger()->info('Preparing question pool export (1/3)...'); $state->assertStep(ExportStep::INIT); $state->setStep(ExportStep::PREPARE); $pool_id = $this->extractObjectId($state); if ($pool_id === null) { + $state->logger()->warning('No question pool object ID found for export'); return; } @@ -80,6 +82,8 @@ public function prepare(ExportState $state): void ->withAdditionalPipes([$question_image_pipe]) ->create(); $state->setTransformations($transformations); + + $state->logger()->info('...Finished preparing question pool export (1/3)'); } /** @@ -88,6 +92,7 @@ public function prepare(ExportState $state): void */ public function process(ExportState $state): void { + $state->logger()->info('Processing question pool export (2/3)...'); $state->assertStep(ExportStep::PREPARE); $state->setStep(ExportStep::PROCESS); @@ -117,6 +122,8 @@ public function process(ExportState $state): void $state->serializer(), ) ); + + $state->logger()->info('...Finished processing question pool export (2/3)'); } /** @@ -124,18 +131,23 @@ public function process(ExportState $state): void */ public function write(ExportState $state): void { + $state->logger()->info('Writing question pool export (3/3)...'); $state->assertStep(ExportStep::PROCESS); $state->setStep(ExportStep::WRITE); $export_dir = $state->path()->getPathToComponentExpDirInContainer(); $question_image_pipe = $state->transformations()->context(CollectQuestionImages::class); + $state->logger()->debug("Copying question images to export directory {$export_dir}"); foreach ($question_image_pipe->getFiles() as $file) { $state->writer()->writeFileByFilePath( $file['from'], "{$export_dir}/" . $file['to'] ); + $state->logger()->debug("Copied question image {$file['from']} to {$export_dir}/{$file['to']}"); } + + $state->logger()->info('...Finished writing question pool export (3/3)'); } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php index 9b926ec14f5a..f13f3548247c 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php @@ -23,6 +23,7 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ImportStage; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResult; +use Psr\Log\LoggerInterface; /** * Final import stage that cleans up the temporary files and directories after successful import or @@ -30,6 +31,11 @@ */ class CleanupStage implements ImportStage { + public function __construct( + private readonly LoggerInterface $log, + ) { + } + public function getIdentifier(): string { return 'cleanup'; @@ -52,12 +58,18 @@ public function process(ImportContext $context): StageResult $temp_dir = dirname($file_to_import); if ($temp_dir && file_exists($temp_dir) && is_dir($temp_dir)) { $this->removeDirectory($temp_dir); + $this->log->info("Removed temporary import directory: {$temp_dir}"); + } else { + $this->log->warning("Temporary import directory does not exist: {$temp_dir}"); } } $import_base_dir = $context->get(UploadValidationStage::IMPORT_BASE_DIR); if ($import_base_dir && file_exists($import_base_dir) && is_dir($import_base_dir)) { $this->removeDirectory($import_base_dir); + $this->log->info("Removed import target base directory: {$import_base_dir}"); + } else { + $this->log->warning("Import target base directory does not exist: {$import_base_dir}"); } return StageResult::complete($context); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php index 1cb180a77c3e..cb331f69b8d6 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php @@ -23,6 +23,7 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ImportStage; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResult; +use Psr\Log\LoggerInterface; /** * @deprecated This stage is only used for legacy imports and will be removed with further ILIAS versions. @@ -32,6 +33,11 @@ class DetectLegacyImportStage implements ImportStage public const string LEGACY_QTI_FILE = 'legacy_qti_file'; public const string LEGACY_XML_FILE = 'legacy_xml_file'; + public function __construct( + private readonly LoggerInterface $log, + ) { + } + public function getIdentifier(): string { return 'detect_legacy_import'; @@ -56,9 +62,11 @@ public function process(ImportContext $context): StageResult $qti_file = $import_base_dir . DIRECTORY_SEPARATOR . str_replace(['_qpl_', '_tst_'], '_qti_', $import_name) . '.xml'; if (!file_exists($qti_file) || !file_exists($xml_file)) { + $this->log->debug("No legacy import files found for {$import_name}"); return StageResult::advance($context); } + $this->log->info("Detected legacy import files for {$import_name}"); return StageResult::advance( $context->with(self::LEGACY_QTI_FILE, $qti_file) ->with(self::LEGACY_XML_FILE, $xml_file) diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php index 8c9ef8e678ce..dbcfa852510b 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php @@ -47,9 +47,6 @@ class QuestionPoolImporter { public function __construct( private readonly Builder $builder, - private readonly ilCtrl $ctrl, - private readonly ilDBInterface $database, - private readonly Language $language, private readonly LoggerInterface $log, private readonly DataFactory $data_factory, private readonly QuestionsImporter $questions_importer, @@ -113,16 +110,20 @@ function (array $assignments) use ($tt, $mapping, &$context): void { } ); + $this->log->info('Importing question pool export file...'); $deserializer->process(); + $this->log->info('...Finished importing question pool export file'); - // Copy the question images from the temporary import directory to the question pool directory + $this->log->info('Importing question images...'); $this->questions_importer->importQuestionImages( $context->get('pool_obj_id'), $mapping, $context, $images_pipe ); + $this->log->info('...Finished importing question images'); + $this->log->info("Finished importing question pool {$context->get('pool_obj_id')} (Object ID)"); return $context; } @@ -132,7 +133,9 @@ function (array $assignments) use ($tt, $mapping, &$context): void { */ public function finalize(ilImportMapping $mapping): void { + $this->log->info('Finalizing question pool import...'); $this->questions_importer->finalizeQuestionPages($mapping); + $this->log->info('...Finished finalizing question pool'); } protected function importQuestionPool( @@ -150,10 +153,12 @@ protected function importQuestionPool( $pool_object->getObjectProperties()->getPropertyIsOnline()->withOffline() ); $pool_object->saveToDb(); + $this->log->debug("Created new pool object: {$old_pool_id} -> {$new_pool_id}"); $pool_object->createReference(); $pool_object->putInTree($parent_id->toInt()); $pool_object->setPermissions($parent_id->toInt()); + $this->log->debug("Stored pool object in tree: {$parent_id->toInt()} (Parent Ref) -> {$pool_object->getRefId()} (Pool Ref)"); $mapping->addMapping('components/ILIAS/TestQuestionPool', 'qpl', (string) $old_pool_id, (string) $new_pool_id); $mapping->addMapping('components/ILIAS/TestQuestionPool', 'object', (string) $old_pool_id, (string) $new_pool_id); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php index bb890f483b9f..7bffa9f3a7b1 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php @@ -29,6 +29,7 @@ use ILIAS\UI\Component\Input\Container\Form\Form; use ILIAS\UI\Factory as UIFactory; use Psr\Http\Message\ServerRequestInterface; +use Psr\Log\LoggerInterface; /** * Second stage of the question pool import process. It displays the list of questions found in the imported file and @@ -55,6 +56,7 @@ class QuestionSelectionStage implements ImportStage public function __construct( private readonly Language $lng, + private readonly LoggerInterface $log, private readonly ilComponentFactory $component_factory, private readonly UIFactory $ui_factory, private readonly ServerRequestInterface $request, @@ -96,6 +98,7 @@ public function process(ImportContext $context): StageResult } if (!$context->has(UploadValidationStage::COMPONENT_IMPORT_FILE)) { + $this->log->error("No component import file found in context"); return StageResult::error($context, $this->lng->txt('qpl_import_file_not_found')); } @@ -104,6 +107,7 @@ public function process(ImportContext $context): StageResult : $this->readQuestions($context); if ($options === []) { + $this->log->error("No questions found in import file"); return StageResult::error($context, $this->lng->txt('qpl_import_no_items')); } @@ -150,6 +154,9 @@ private function readQuestions(ImportContext $context): array }); $deserializer->process(); + $count = count($options); + $this->log->info("Found {$count} questions in import file"); + return $options; } @@ -171,6 +178,9 @@ private function readQuestionsFromQTI(ImportContext $context): array $options[$item['ident']] = "{$item['title']} ({$this->getLabelForQuestionType($item['type'])})"; } + $count = count($options); + $this->log->info("Found {$count} questions in legacy import file"); + return $options; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php index a3d84b6531a0..238daf17c287 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php @@ -76,6 +76,7 @@ public function importQuestion( $question = $transformations->denormalize($normalized, new $question_class()); $old_question_id = $question->getId(); if (!in_array($old_question_id, $selected_questions)) { + $this->log->debug("Skipping question import for ID {$old_question_id} (not selected)"); return null; } @@ -85,6 +86,7 @@ public function importQuestion( // Create new question and store basic question properties $new_question_id = $question->createNewQuestion(false); + $this->log->debug("Created new question: {$old_question_id} -> {$new_question_id}"); $this->storeQuestionMappings($mapping, $old_question_id, $new_question_id, $question->getObjId()); if ($question instanceof assFormulaQuestion) { @@ -93,6 +95,7 @@ public function importQuestion( // Save question-specific properties $question->saveToDb(); + $this->log->debug("Imported question {$new_question_id} (type: {$question->getQuestionType()})"); $feedback = $transformations->denormalize($normalized['feedback'], Feedback::class); $this->importFeedback($feedback, $question); @@ -187,6 +190,7 @@ public function finalizeQuestionPages(ilImportMapping $mapping): void continue; } $new_question_id = $new_matches[1]; + $this->log->debug("Finalizing question page: {$old_question_id} -> {$new_question_id}"); $page = new ilAssQuestionPage((int) $new_question_id); $xml = preg_replace( @@ -209,6 +213,7 @@ public function finalizeQuestionPages(ilImportMapping $mapping): void } $page->updateFromXML(); + $this->log->debug("Updated question page: {$page->getId()}"); unset($page); } } @@ -265,6 +270,8 @@ private function importFeedback(Feedback $feedback, assQuestion $question): void $specific_feedback['feedback'] ); } + + $this->log->debug("Imported feedback for question: {$question_id}"); } private function importFormulaQuestion( @@ -282,6 +289,7 @@ private function importFormulaQuestion( $old_category_id = $category->getId(); $repository->saveNewUnitCategory($category); + $this->log->debug("Imported formula question unit category: {$old_category_id} -> {$category->getId()}"); $mapping->addMapping($this->component, 'unit_category', (string) $old_category_id, (string) $category->getId()); } @@ -297,6 +305,7 @@ private function importFormulaQuestion( $unit = $transformations->denormalize($normalized_unit, $unit); $repository->saveUnit($unit); + $this->log->debug("Imported formula question unit: {$old_unit_id} -> {$unit->getId()}"); } // The question object is denormalized again to ensure the new unit ids are set in the variables and results. diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php index 04eec3458349..47ccd6d6831e 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php @@ -25,6 +25,7 @@ use ILIAS\Skill\Service\SkillUsageService; use ilImportMapping; use ilSkillTreeRepository; +use Psr\Log\LoggerInterface; /** * Imports skill assignments from normalized data. It will map imported skill using the source installation id. If a @@ -35,6 +36,7 @@ class SkillAssignmentsImporter { public function __construct( + private readonly LoggerInterface $log, private readonly ilSkillTreeRepository $skill_repo, private readonly SkillUsageService $skill_usage_service, private readonly string $component, @@ -67,6 +69,7 @@ public function import( $import_install_id ); if ($skill_data === null) { + $this->log->warning("Failed to find skill id mapping for assignment: {$assignment->getSkillBaseId()}/{$assignment->getSkillTrefId()}"); $result['failed'][] = $this->buildResultData($assignment); continue; } @@ -85,6 +88,7 @@ public function import( ); $assignment->setSkillBaseId($skill_data['skill_id']); $assignment->setSkillTrefId($skill_data['tref_id']); + $this->log->debug("Found skill assignment: {$assignment->getSkillBaseId()}/{$assignment->getSkillTrefId()} -> {$skill_data['skill_id']}/{$skill_data['tref_id']}"); $assignment->initSolutionComparisonExpressionList(); foreach ($assignment->getSolutionComparisonExpressionList()->get() as $expression) { @@ -94,6 +98,7 @@ public function import( $assignment->saveToDb(); $assignment->saveComparisonExpressions(); + $this->log->debug("Saved skill assignment: {$assignment->getSkillBaseId()}/{$assignment->getSkillTrefId()}"); $this->skill_usage_service->addUsage( $assignment->getParentObjId(), diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php index ea73b375c871..4a26199c8a5c 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php @@ -28,6 +28,7 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResult; use ilManifestParser; +use Psr\Log\LoggerInterface; /** * First stage of the question pool import pipeline. Receives the uploaded file path from the context, extracts ZIP @@ -45,6 +46,7 @@ class UploadValidationStage implements ImportStage public function __construct( private readonly Archives $archives, private readonly Language $lng, + private readonly LoggerInterface $log, private readonly string $component ) { } @@ -72,6 +74,7 @@ public function process(ImportContext $context): StageResult || !is_file($file_to_import) || !str_ends_with(strtolower($file_to_import), '.zip') ) { + $this->log->error("Invalid import file: {$file_to_import}"); return StageResult::error($context, $this->lng->txt('obj_import_file_error')); } @@ -81,6 +84,7 @@ public function process(ImportContext $context): StageResult $options = (new UnzipOptions())->withZipOutputPath(self::IMPORT_TEMP_DIR); $unzip = $this->archives->unzip(Streams::ofResource(fopen($file_to_import, 'r')), $options); $unzip->extract(); + $this->log->info("Extracted import file: {$file_to_import} -> {$import_base_dir}"); $manifest = new ilManifestParser($import_base_dir . DIRECTORY_SEPARATOR . 'manifest.xml'); $export_file = array_find( @@ -89,11 +93,16 @@ public function process(ImportContext $context): StageResult ); if ($export_file === null) { + $this->log->error("No export file found for component: {$this->component}"); return StageResult::error($context, $this->lng->txt('obj_import_file_error')); } + $component_import_file = $import_base_dir . DIRECTORY_SEPARATOR . $export_file['path']; + $this->log->info("Found export file for {$this->component}: -> {$component_import_file}"); + $this->log->info("Found valid export file from installation: {$manifest->getInstallId()}"); + return StageResult::advance( - $context->with(self::COMPONENT_IMPORT_FILE, $import_base_dir . DIRECTORY_SEPARATOR . $export_file['path']) + $context->with(self::COMPONENT_IMPORT_FILE, $component_import_file) ->with(self::IMPORT_BASE_DIR, $import_base_dir) ->with(self::INSTALL_ID, $manifest->getInstallId()) ); diff --git a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php index cb89f529776d..18dcd71d7c64 100755 --- a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php +++ b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php @@ -29,7 +29,6 @@ use ILIAS\TestQuestionPool\ExportImport\Import\QuestionsImporter; use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter; use ILIAS\TestQuestionPool\ExportImport\LoggingProvider; -use ilLoggerFactory; use Pimple\Container as PimpleContainer; use ILIAS\DI\Container as ILIASContainer; use ILIAS\TestQuestionPool\Questions\SuggestedSolution\SuggestedSolutionsDatabaseRepository; @@ -99,6 +98,7 @@ protected static function buildDIC(ILIASContainer $DIC): self new ImportSessionRepository('qpl'); $dic['exportimport.skill_assignments_importer'] = static fn($c): SkillAssignmentsImporter => new SkillAssignmentsImporter( + $c['exportimport.logging'](), $DIC->skills()->internal()->repo()->getTreeRepo(), $DIC->skills()->usage(), 'components/ILIAS/TestQuestionPool', @@ -118,9 +118,6 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic['exportimport.importer'] = static fn($c): QuestionPoolImporter => new QuestionPoolImporter( $c['exportimport.builder'], - $DIC->ctrl(), - $DIC->database(), - $DIC->language(), $c['exportimport.logging'](), new DataFactory(), $c['exportimport.questions_importer'], From b7a9df1179b5268c67fa684603060cc931c498c1 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Wed, 13 May 2026 09:53:05 +0200 Subject: [PATCH 41/66] style: fix coding style --- .../Foundation/Bridge/XmlExporterBridge.php | 12 ++++++------ .../Foundation/Importing/StageResultType.php | 2 +- .../src/ExportImport/LoggingProvider.php | 8 +++++--- .../Normalizer/SuggestedSolutionNormalizer.php | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php index 546b7c0f0828..2ce4297e0741 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php @@ -111,12 +111,12 @@ private function initExportState( ); $this->logger->debug(sprintf( - "Export state created for component %s with release %s, type %s, class %s, object ids %s, option %s", - $target->getComponent(), - $target->getTargetRelease(), - $target->getType(), - $target->getClassname(), - implode(', ', $object_ids), + "Export state created for component %s with release %s, type %s, class %s, object ids %s, option %s", + $target->getComponent(), + $target->getTargetRelease(), + $target->getType(), + $target->getClassname(), + implode(', ', $object_ids), $option )); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/StageResultType.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/StageResultType.php index 55e42ea50fa3..26d418ec7ed3 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/StageResultType.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/StageResultType.php @@ -25,7 +25,7 @@ */ enum StageResultType { - /** + /** * The stage has encountered an error and the workflow should display an error message. */ case ERROR; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/LoggingProvider.php b/components/ILIAS/TestQuestionPool/src/ExportImport/LoggingProvider.php index 98817b30cfc7..e0b39c6bd0dc 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/LoggingProvider.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/LoggingProvider.php @@ -30,12 +30,14 @@ class LoggingProvider { private const string COMPONENT_ID = 'exp'; - public function getLogger(?string $component_id = null): LoggerInterface { + public function getLogger(?string $component_id = null): LoggerInterface + { $component_id = $component_id ?? self::COMPONENT_ID; return ilLoggerFactory::getLogger($component_id)->getLogger(); } - public function __invoke() { + public function __invoke() + { return $this->getLogger(); } -} \ No newline at end of file +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/SuggestedSolutionNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/SuggestedSolutionNormalizer.php index 5cefef06f6aa..3f962e121309 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/SuggestedSolutionNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/SuggestedSolutionNormalizer.php @@ -73,7 +73,7 @@ public function normalize($value): array|float|bool|int|string|null $normalized['file'] = $this->tt->normalize( new QuestionImage($value->getFilename(), $value->getQuestionId(), QuestionImage::TYPE_SOLUTION) ); - + } return $normalized; From 6cd0fd3b5770e170c1c97049ec83150cbf9339f8 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Wed, 13 May 2026 10:16:50 +0200 Subject: [PATCH 42/66] refactor: remove legacy test classes --- .../Test/classes/class.ilTestResultsToXML.php | 300 ------------------ .../ExportFixedQuestionSetTest.php | 60 ---- .../ExportRandomQuestionSetTest.php | 59 ---- .../Test/tests/ilTestResultsToXMLTest.php | 57 ---- .../tests/ilQuestionpoolExportTest.php | 48 --- 5 files changed, 524 deletions(-) delete mode 100755 components/ILIAS/Test/classes/class.ilTestResultsToXML.php delete mode 100755 components/ILIAS/Test/tests/ExportImport/ExportFixedQuestionSetTest.php delete mode 100755 components/ILIAS/Test/tests/ExportImport/ExportRandomQuestionSetTest.php delete mode 100755 components/ILIAS/Test/tests/ilTestResultsToXMLTest.php delete mode 100644 components/ILIAS/TestQuestionPool/tests/ilQuestionpoolExportTest.php diff --git a/components/ILIAS/Test/classes/class.ilTestResultsToXML.php b/components/ILIAS/Test/classes/class.ilTestResultsToXML.php deleted file mode 100755 index bc3a6d67888d..000000000000 --- a/components/ILIAS/Test/classes/class.ilTestResultsToXML.php +++ /dev/null @@ -1,300 +0,0 @@ -include_random_test_questions_enabled; - } - - public function setIncludeRandomTestQuestionsEnabled(bool $include_random_test_questions_enabled): void - { - $this->include_random_test_questions_enabled = $include_random_test_questions_enabled; - } - - protected function exportActiveIDs(): void - { - $user_criteria = (new ilSetting('assessment'))->get('user_criteria', 'usr_id'); - - $query = $this->test_obj->getAnonymity() - ? 'SELECT * FROM tst_active WHERE test_fi = %s' - : "SELECT tst_active.*, usr_data.{$user_criteria} FROM tst_active, usr_data WHERE tst_active.test_fi = %s AND tst_active.user_fi = usr_data.usr_id"; - $result = $this->db->queryF($query, [ilDBConstants::T_INTEGER], [$this->test_obj->getTestId()]); - - $test_participant_list = new ilTestParticipantList($this->test_obj, $this->user, $this->lng, $this->db); - $test_participant_list->initializeFromDbRows($this->test_obj->getTestParticipants()); - - $this->xmlStartTag('tst_active', null); - while ($row = $this->db->fetchAssoc($result)) { - $this->active_ids[] = $row['active_id']; - $participant = $test_participant_list->getParticipantByActiveId($row['active_id']); - - $attrs = [ - 'active_id' => $row['active_id'], - 'user_fi' => $this->test_obj->getAnonymity() ? '' : ($row['user_fi'] ?? ''), - 'fullname' => $participant ? $test_participant_list->buildFullname($participant) : '', - 'anonymous_id' => $row['anonymous_id'] ?? '', - 'test_fi' => $row['test_fi'], - 'lastindex' => $row['lastindex'] ?? '', - 'tries' => $row['tries'] ?? '', - 'last_started_pass' => $row['last_started_pass'] ?? '', - 'last_finished_pass' => $row['last_finished_pass'] ?? '', - 'submitted' => $row['submitted'] ?? '', - 'submittimestamp' => $row['submittimestamp'] ?? '', - 'tstamp' => $row['tstamp'] ?? '' - ]; - - if (!$this->test_obj->getAnonymity()) { - $attrs['user_criteria'] = $user_criteria; - $attrs[$user_criteria] = $row[$user_criteria]; - } - - $this->xmlElement('row', $attrs); - } - $this->xmlEndTag('tst_active'); - } - - protected function exportPassResult(): void - { - $query = 'SELECT * FROM tst_pass_result WHERE ' . $this->db->in('active_fi', $this->active_ids, false, 'integer') . ' ORDER BY active_fi, pass'; - $result = $this->db->query($query); - $this->xmlStartTag('tst_pass_result', null); - while ($row = $this->db->fetchAssoc($result)) { - $attrs = [ - 'active_fi' => $row['active_fi'], - 'pass' => $row['pass'] ?? '', - 'points' => $row['points'] ?? '', - 'maxpoints' => $row['maxpoints'] ?? '', - 'questioncount' => $row['questioncount'] ?? '', - 'answeredquestions' => $row['answeredquestions'] ?? '', - 'workingtime' => $row['workingtime'] ?? '', - 'tstamp' => $row['tstamp'] ?? '' - ]; - $this->xmlElement('row', $attrs); - } - $this->xmlEndTag('tst_pass_result'); - } - - protected function exportResultCache(): void - { - $query = 'SELECT * FROM tst_result_cache WHERE ' . $this->db->in('active_fi', $this->active_ids, false, 'integer') . ' ORDER BY active_fi'; - $result = $this->db->query($query); - $this->xmlStartTag('tst_result_cache', null); - while ($row = $this->db->fetchAssoc($result)) { - $attrs = [ - 'active_fi' => $row['active_fi'], - 'pass' => $row['pass'], - 'max_points' => $row['max_points'], - 'reached_points' => $row['reached_points'], - 'mark_short' => $row['mark_short'], - 'mark_official' => $row['mark_official'], - 'passed' => $row['passed'], - 'failed' => $row['failed'], - 'tstamp' => $row['tstamp'] - ]; - $this->xmlElement('row', $attrs); - } - $this->xmlEndTag('tst_result_cache'); - } - - protected function exportTestSequence(): void - { - $query = 'SELECT * FROM tst_sequence WHERE ' . $this->db->in('active_fi', $this->active_ids, false, 'integer') . ' ORDER BY active_fi, pass'; - $result = $this->db->query($query); - $this->xmlStartTag('tst_sequence', null); - while ($row = $this->db->fetchAssoc($result)) { - $attrs = [ - 'active_fi' => $row['active_fi'], - 'pass' => $row['pass'] ?? '', - 'sequence' => $row['sequence'] ?? '', - 'postponed' => $row['postponed'] ?? '', - 'hidden' => $row['hidden'] ?? '', - 'tstamp' => $row['tstamp'] ?? '' - ]; - $this->xmlElement('row', $attrs); - } - $this->xmlEndTag('tst_sequence'); - } - - protected function exportTestSolutions(): void - { - $query = 'SELECT * FROM tst_solutions WHERE ' . $this->db->in('active_fi', $this->active_ids, false, 'integer') . ' ORDER BY solution_id'; - $result = $this->db->query($query); - $this->xmlStartTag('tst_solutions', null); - while ($row = $this->db->fetchAssoc($result)) { - $attrs = [ - 'solution_id' => $row['solution_id'], - 'active_fi' => $row['active_fi'], - 'question_fi' => $row['question_fi'], - 'points' => $row['points'] ?? '', - 'pass' => $row['pass'] ?? '', - 'value1' => $row['value1'] ?? '', - 'value2' => $row['value2'] ?? '', - 'tstamp' => $row['tstamp'] ?? '' - ]; - $this->xmlElement('row', $attrs); - } - $this->xmlEndTag('tst_solutions'); - } - - protected function exportRandomTestQuestions(): void - { - $result = $this->db->query(" - SELECT * FROM tst_test_rnd_qst - WHERE {$this->db->in('active_fi', $this->active_ids, false, 'integer')} - ORDER BY test_random_question_id - "); - - $this->xmlStartTag('tst_test_rnd_qst', null); - while ($row = $this->db->fetchAssoc($result)) { - $attrs = []; - - foreach ($row as $field => $value) { - $attrs[$field] = $value; - } - - $this->xmlElement('row', $attrs); - } - $this->xmlEndTag('tst_test_rnd_qst'); - } - - - protected function exportTestResults(): void - { - $query = 'SELECT * FROM tst_test_result WHERE ' . $this->db->in('active_fi', $this->active_ids, false, 'integer') . ' ORDER BY active_fi'; - $result = $this->db->query($query); - $this->xmlStartTag('tst_test_result', null); - while ($row = $this->db->fetchAssoc($result)) { - $active_fi = $row['active_fi']; - $pass = $row['pass'] ?? ''; - $attrs = [ - 'test_result_id' => $row['test_result_id'], - 'active_fi' => $active_fi, - 'question_fi' => $row['question_fi'], - 'points' => $row['points'] ?? '', - 'pass' => $pass, - 'manual' => $row['manual'] ?? '', - 'tstamp' => $row['tstamp'] ?? '' - ]; - - if (($question = assQuestion::instantiateQuestion($row['question_fi'])) instanceof assFileUpload) { - $this->exportParticipantUploadedFiles($question->getUploadedFiles($active_fi, $pass)); - } - - $this->xmlElement('row', $attrs); - } - $this->xmlEndTag('tst_test_result'); - } - - /** - * @param array{value1: string, value2: string} $uploaded_files - */ - protected function exportParticipantUploadedFiles(array $uploaded_files): void - { - foreach ($uploaded_files as $uploaded_file) { - if ($uploaded_file['value2'] !== 'rid') { - continue; - } - - $rid_string = $uploaded_file['value1']; - $rid = $this->irss->manage()->find($rid_string); - if ($rid === null) { - continue; - } - - $target_dir = "$this->objects_export_directory/resources/$rid_string"; - ilFileUtils::makeDirParents($target_dir); - file_put_contents( - "$target_dir/{$this->irss->manage()->getCurrentRevision($rid)->getTitle()}", - $this->irss->consume()->stream($rid)->getStream(), - ); - } - } - - protected function exportTestTimes(): void - { - $query = 'SELECT * FROM tst_times WHERE ' . $this->db->in('active_fi', $this->active_ids, false, 'integer') . ' ORDER BY active_fi'; - $result = $this->db->query($query); - $this->xmlStartTag('tst_times', null); - while ($row = $this->db->fetchAssoc($result)) { - $attrs = [ - 'times_id' => $row['times_id'], - 'active_fi' => $row['active_fi'], - 'started' => $row['started'], - 'finished' => $row['finished'], - 'pass' => $row['pass'], - 'tstamp' => $row['tstamp'] - ]; - $this->xmlElement('row', $attrs); - } - $this->xmlEndTag('tst_times'); - } - - public function getXML(): void - { - $this->active_ids = []; - $this->xmlHeader(); - $attrs = ['version' => '4.1.0']; - $this->xmlStartTag('results', $attrs); - $this->exportActiveIDs(); - - if ($this->isIncludeRandomTestQuestionsEnabled()) { - $this->exportRandomTestQuestions(); - } - - $this->exportPassResult(); - $this->exportResultCache(); - $this->exportTestSequence(); - $this->exportTestSolutions(); - $this->exportTestResults(); - $this->exportTestTimes(); - $this->xmlEndTag('results'); - } - - public function xmlDumpMem(bool $format = true): string - { - $this->getXML(); - return parent::xmlDumpMem($format); - } - - public function xmlDumpFile(string $file, bool $format = true): void - { - $this->getXML(); - parent::xmlDumpFile($file, $format); - } -} diff --git a/components/ILIAS/Test/tests/ExportImport/ExportFixedQuestionSetTest.php b/components/ILIAS/Test/tests/ExportImport/ExportFixedQuestionSetTest.php deleted file mode 100755 index 7266ca3cab9d..000000000000 --- a/components/ILIAS/Test/tests/ExportImport/ExportFixedQuestionSetTest.php +++ /dev/null @@ -1,60 +0,0 @@ - - */ -class ExportFixedQuestionSetTest extends \ilTestBaseTestCase -{ - private ExportFixedQuestionSet $testObj; - - protected function setUp(): void - { - global $DIC; - parent::setUp(); - - $this->addGlobal_ilErr(); - $this->addGlobal_ilias(); - $this->addGlobal_resourceStorage(); - - $this->testObj = new ExportFixedQuestionSet( - $this->createMock(\ilLanguage::class), - $this->createMock(\ilDBInterface::class), - $this->createMock(\ilBenchmark::class), - $this->createMock(\ILIAS\Test\Logging\TestLogger::class), - $this->createMock(\ilTree::class), - $this->createMock(\ilComponentRepository::class), - $this->createMock(\ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository::class), - $this->createMock(\ILIAS\FileDelivery\Services::class), - $this->createMock(\ilObjTest::class), - $DIC['resource_storage'], - $DIC['ilUser'] - ); - } - - public function test_instantiateObject_shouldReturnInstance(): void - { - $this->assertInstanceOf(ExportFixedQuestionSet::class, $this->testObj); - } -} diff --git a/components/ILIAS/Test/tests/ExportImport/ExportRandomQuestionSetTest.php b/components/ILIAS/Test/tests/ExportImport/ExportRandomQuestionSetTest.php deleted file mode 100755 index acb3bbbaffd8..000000000000 --- a/components/ILIAS/Test/tests/ExportImport/ExportRandomQuestionSetTest.php +++ /dev/null @@ -1,59 +0,0 @@ - - */ -class ExportRandomQuestionSetTest extends \ilTestBaseTestCase -{ - private ExportRandomQuestionSet $testObj; - - protected function setUp(): void - { - global $DIC; - parent::setUp(); - - $this->addGlobal_ilErr(); - $this->addGlobal_resourceStorage(); - - $this->testObj = new ExportRandomQuestionSet( - $this->createMock(\ilLanguage::class), - $this->createMock(\ilDBInterface::class), - $this->createmOck(\ilBenchmark::class), - $this->createMock(\ILIAS\Test\Logging\TestLogger::class), - $this->createMock(\ilTree::class), - $DIC['component.repository'], - $this->createMock(\ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository::class), - $this->createMock(\ILIAS\FileDelivery\Services::class), - $this->getTestObjMock(), - $DIC['resource_storage'], - $DIC['ilUser'] - ); - } - - public function test_instantiateObject_shouldReturnInstance(): void - { - $this->assertInstanceOf(ExportRandomQuestionSet::class, $this->testObj); - } -} diff --git a/components/ILIAS/Test/tests/ilTestResultsToXMLTest.php b/components/ILIAS/Test/tests/ilTestResultsToXMLTest.php deleted file mode 100755 index 9cb7405aab4f..000000000000 --- a/components/ILIAS/Test/tests/ilTestResultsToXMLTest.php +++ /dev/null @@ -1,57 +0,0 @@ - - */ -class ilTestResultsToXMLTest extends ilTestBaseTestCase -{ - private ilTestResultsToXML $testObj; - - protected function setUp(): void - { - global $DIC; - parent::setUp(); - - $this->testObj = new ilTestResultsToXML( - $this->createMock(ilObjTest::class), - $DIC['ilDB'], - $DIC['resource_storage'], - $DIC['ilUser'], - $DIC['lng'], - '' - ); - } - - public function test_instantiateObject_shouldReturnInstance(): void - { - $this->assertInstanceOf(ilTestResultsToXML::class, $this->testObj); - } - - public function testIncludeRandomTestQuestionsEnabled(): void - { - $this->testObj->setIncludeRandomTestQuestionsEnabled(false); - $this->assertFalse($this->testObj->isIncludeRandomTestQuestionsEnabled()); - - $this->testObj->setIncludeRandomTestQuestionsEnabled(true); - $this->assertTrue($this->testObj->isIncludeRandomTestQuestionsEnabled()); - } -} diff --git a/components/ILIAS/TestQuestionPool/tests/ilQuestionpoolExportTest.php b/components/ILIAS/TestQuestionPool/tests/ilQuestionpoolExportTest.php deleted file mode 100644 index 76eb801d3b85..000000000000 --- a/components/ILIAS/TestQuestionPool/tests/ilQuestionpoolExportTest.php +++ /dev/null @@ -1,48 +0,0 @@ - -* -* @ingroup components\ILIASTestQuestionPool -* -* This test was automatically generated. -*/ -class ilQuestionpoolExportTest extends assBaseTestCase -{ - protected $backupGlobals = false; - - private ilQuestionpoolExport $object; - - protected function setUp(): void - { - parent::setUp(); - - $this->addGlobal_ilErr(); - $this->addGlobal_ilias(); - - $this->object = new ilQuestionpoolExport($this->createMock(ilObjQuestionPool::class), 'xml', null); - } - - public function testConstruct(): void - { - $this->assertInstanceOf(ilQuestionpoolExport::class, $this->object); - } -} From 42fa9a36b380e23cad024aacff4a2070884a5049 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Fri, 19 Jun 2026 09:31:46 +0200 Subject: [PATCH 43/66] fix: remove imported object placeholder after testing --- components/ILIAS/Test/src/ExportImport/Import/TestImporter.php | 2 +- .../src/ExportImport/Import/QuestionPoolImporter.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php index c737b428b1f7..b7f059b6fb6c 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php @@ -289,7 +289,7 @@ private function importTest( $old_test_id = $test_object->getTestId(); $test_object->setTestId(-1); - $test_object->setTitle("{$test_object->getTitle()} (Imported)"); //TODO: Remove after testing + $test_object->setTitle($test_object->getTitle()); $new_obj_id = $test_object->create(); $test_object->saveToDb(true); $this->log->debug("Created new test object: {$old_test_id} -> {$test_object->getTestId()} (Test ID), {$old_obj_id} -> {$new_obj_id} (Object ID)"); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php index dbcfa852510b..440589ed058a 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php @@ -147,7 +147,7 @@ protected function importQuestionPool( $pool_object = $transformations->denormalize($normalized, ilObjQuestionPool::class); $old_pool_id = $pool_object->getId(); - $pool_object->setTitle('Imported'); //TODO: Remove after testing + $pool_object->setTitle($pool_object->getTitle()); $new_pool_id = $pool_object->create(true); $pool_object->getObjectProperties()->storePropertyIsOnline( $pool_object->getObjectProperties()->getPropertyIsOnline()->withOffline() From 49eb2e26b5a58f16372c59f6177891ff19fc43fe Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Fri, 19 Jun 2026 09:42:41 +0200 Subject: [PATCH 44/66] fix: redirect after question pool import --- .../TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php index f4a987803a35..82aefbd088de 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php @@ -1188,8 +1188,11 @@ private function renderImportError(ImportStageRunner $runner, StageResult $resul private function renderImportSuccess(ImportStageRunner $runner, StageResult $result): void { + $pool_obj_id = $this->data_factory->objId($result->context->get('pool_obj_id')); + $pool_ref_id = $pool_obj_id->toReferenceIds()[0]->toInt(); + $this->tpl->setOnScreenMessage('success', $this->lng->txt('object_imported'), true); - $this->ctrl->setParameter($this, 'ref_id', $result->context->get('pool_obj_id')); + $this->ctrl->setParameter($this, 'ref_id', $pool_ref_id); $this->ctrl->redirectByClass(self::class, self::DEFAULT_CMD); } From e6f204271712f675a9da4dec7a438ce6c1d1230f Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Wed, 1 Jul 2026 09:42:13 +0200 Subject: [PATCH 45/66] fix: null cast exception --- .../ILIAS/TestQuestionPool/classes/class.assClozeTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php b/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php index c92d9ccb7ad1..00a80a5af323 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php @@ -1800,7 +1800,7 @@ public function fromNormalized(Transformations $tt): Transformation $clone->cloze_text = $tt->string($normalized['cloze_text']); $clone->textgap_rating = $tt->string($normalized['textgap_rating']); $clone->identical_scoring = $tt->bool($normalized['identical_scoring']); - $clone->fixed_text_length = $tt->int($normalized['fixed_text_length']); + $clone->fixed_text_length = $tt->nullableInt($normalized['fixed_text_length']); $clone->gap_combinations_exist = $tt->bool($normalized['gap_combinations_exist']); $clone->gap_combinations = $normalized['gap_combinations']; From d071d510b3e820f0d2d07eb446d848b744130f94 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Wed, 1 Jul 2026 10:35:43 +0200 Subject: [PATCH 46/66] fix: content cast exception in ordering element export --- .../classes/class.assOrderingQuestion.php | 5 +++-- .../classes/questions/class.ilAssOrderingElement.php | 12 +++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php index 7837a560ff73..e0e31c3841dc 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php @@ -1390,10 +1390,11 @@ public function fromNormalized(Transformations $tt): Transformation return $tt->custom()->transformation(function (array $normalized) use ($tt): self { $clone = parent::fromNormalized($tt)->transform($normalized); $clone->ordering_type = $tt->int($normalized['ordering_type']); - $clone->getOrderingElementList()->setElements(array_map( + $denormalized_elements = array_map( fn(array $element) => $tt->denormalize($element, new ilAssOrderingElement()), $normalized['ordering_elements'] - )); + ); + $clone->element_list_for_deferred_saving = new ilAssOrderingElementList(null, $denormalized_elements); return $clone; }); diff --git a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php index 2f65ab116890..8b7c174f35b4 100755 --- a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php +++ b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php @@ -17,11 +17,12 @@ *********************************************************************/ declare(strict_types=1); + +use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; -use ILIAS\Refinery\Transformation; -use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; /** * Class represents an ordering element for assOrderingQuestion @@ -474,7 +475,12 @@ public function fromNormalized(Transformations $tt): Transformation ->withPosition($tt->int($normalized['position'])) ->withIndentation($tt->int($normalized['indentation'])); - $clone->setContent($tt->denormalize($normalized['content'], QuestionImage::class)?->getFilename()); + if (is_array($normalized['content'])) { + $clone->setContent($tt->denormalize($normalized['content'], QuestionImage::class)?->getFilename()); + } else { + $clone->setContent((string) $normalized['content']); + } + $clone->setId($tt->denormalize($normalized['id'], Id::class)->getId()); return $clone; From 1ed462a19991fc104fe687e3ce81ed7f2fb749de Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Fri, 31 Jul 2026 10:39:46 +0200 Subject: [PATCH 47/66] refactor: cleanup test classes --- .../ILIAS/Test/classes/class.ilObjTest.php | 30 --------------- .../ILIAS/Test/classes/class.ilObjTestGUI.php | 37 ------------------- 2 files changed, 67 deletions(-) diff --git a/components/ILIAS/Test/classes/class.ilObjTest.php b/components/ILIAS/Test/classes/class.ilObjTest.php index de177f886df7..67601ecbabd0 100755 --- a/components/ILIAS/Test/classes/class.ilObjTest.php +++ b/components/ILIAS/Test/classes/class.ilObjTest.php @@ -407,36 +407,6 @@ public function getExportFiles(string $dir = ''): array return $files; } - /** - * creates data directory for import files - * (data_dir/tst_data/tst_/import, depending on data - * directory that is set in ILIAS setup/ini) - */ - public static function _createImportDirectory(): string - { - global $DIC; - $ilias = $DIC['ilias']; - $tst_data_dir = ilFileUtils::getDataDir() . "/tst_data"; - ilFileUtils::makeDir($tst_data_dir); - - if (!is_writable($tst_data_dir)) { - $ilias->raiseError("Test Data Directory (" . $tst_data_dir - . ") not writeable.", $ilias->error_obj->FATAL); - } - - // create test directory (data_dir/tst_data/tst_import) - $tst_dir = $tst_data_dir . "/tst_import"; - ilFileUtils::makeDir($tst_dir); - if (!@is_dir($tst_dir)) { - $ilias->raiseError("Creation of test import directory failed.", $ilias->error_obj->FATAL); - } - - // assert that this is empty and does not contain old data - ilFileUtils::delDir($tst_dir, true); - - return $tst_dir; - } - final public function isComplete(ilTestQuestionSetConfig $test_question_set_config): bool { if ($this->getMarkSchema() === null diff --git a/components/ILIAS/Test/classes/class.ilObjTestGUI.php b/components/ILIAS/Test/classes/class.ilObjTestGUI.php index 477e5e219f5d..8b1e6c63f5f2 100755 --- a/components/ILIAS/Test/classes/class.ilObjTestGUI.php +++ b/components/ILIAS/Test/classes/class.ilObjTestGUI.php @@ -1542,17 +1542,6 @@ public function getTestObject(): ?ilObjTest return $this->object; } - /** - * download file - */ - public function downloadFileObject() - { - $file = explode("_", $this->testrequest->raw("file_id")); - $fileObj = new ilObjFile((int) $file[count($file) - 1], false); - $fileObj->sendFile(); - exit; - } - /** * show fullscreen view */ @@ -2062,22 +2051,6 @@ public function createTemplateObject(): void $this->ctrl->redirectByClass(self::class, 'showTemplates'); } - public function importTemplateObject(): void - { - $this->protectByWritePermission(); - - try { - $this->buildPersonalSettingsImportAction() - ->perform($this->request); - - $this->tpl->setOnScreenMessage('success', $this->lng->txt('personal_settings_import_success'), true); - } catch (\InvalidArgumentException $e) { - $this->tpl->setOnScreenMessage('failure', $this->lng->txt($e->getMessage()), true); - } - - $this->ctrl->redirectByClass(self::class, 'showTemplates'); - } - public function executeTemplatesActionObject(): void { $this->protectByWritePermission(); @@ -2279,16 +2252,6 @@ private function forwardToInfoScreen(): void $this->ctrl->forwardCommand($info); } - protected function removeImportFailsObject() - { - $qsaImportFails = new ilAssQuestionSkillAssignmentImportFails($this->getTestObject()->getId()); - $qsaImportFails->deleteRegisteredImportFails(); - $sltImportFails = new ilTestSkillLevelThresholdImportFails($this->getTestObject()->getId()); - $sltImportFails->deleteRegisteredImportFails(); - - $this->ctrl->redirectByClass([ilRepositoryGUI::class, self::class, ilInfoScreenGUI::class]); - } - public function addLocatorItems(): void { $next_class = $this->ctrl->getNextClass(); From 9563f7f96fef0557ef6f92a930cb3e41762547e7 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Thu, 20 Aug 2026 11:45:08 +0200 Subject: [PATCH 48/66] refactor: improve code structure after internal review --- .../ILIAS/Test/classes/class.ilObjTestGUI.php | 95 +++--- .../Test/classes/class.ilTestExportGUI.php | 4 - .../Test/classes/class.ilTestExporter.php | 2 +- .../Test/classes/class.ilTestImporter.php | 31 +- .../classes/class.ilTestLegacyImporter.php | 35 +- ...tRandomQuestionSetSourcePoolDefinition.php | 8 +- .../Test/classes/class.ilTestResultsToXML.php | 303 ++++++++++++++++++ .../ExportOptions/XMLWithResultsOption.php | 12 +- .../src/ExportImport/Import/PersistStage.php | 5 +- .../Import/RandomTestConfigImporter.php | 5 +- .../Import/SkillLevelThresholdsImporter.php | 4 +- .../src/ExportImport/Import/TestImporter.php | 32 +- .../Import/TestResultsImporter.php | 22 +- .../Import/UserImportResolver.php | 13 +- .../QuestionSetConfigNormalizer.php | 23 +- .../Normalizer/ilTestSequenceNormalizer.php | 4 +- .../src/ExportImport/Pipes/CollectUserIds.php | 10 +- .../Test/src/ExportImport/TestCollector.php | 35 +- .../Test/src/ExportImport/TestExporter.php | 17 +- .../Test/src/Results/Data/Repository.php | 4 +- components/ILIAS/Test/src/TestDIC.php | 3 + .../class.assAnswerBinaryStateImage.php | 22 +- .../classes/class.assAnswerMatchingTerm.php | 6 +- .../class.assAnswerMultipleResponseImage.php | 6 +- .../classes/class.assAnswerSimple.php | 2 +- .../classes/class.assClozeGap.php | 1 - .../classes/class.assFormulaQuestion.php | 1 - .../class.assFormulaQuestionResult.php | 9 +- .../classes/class.assImagemapQuestion.php | 12 +- .../classes/class.assKprimChoice.php | 2 +- .../classes/class.assMultipleChoice.php | 2 +- .../classes/class.assOrderingQuestion.php | 15 +- .../classes/class.assQuestion.php | 49 +-- .../classes/class.assSingleChoice.php | 2 +- .../classes/class.assTextQuestion.php | 12 +- .../classes/class.assTextSubset.php | 11 +- .../classes/class.ilAssKprimChoiceAnswer.php | 10 +- .../classes/class.ilObjQuestionPool.php | 2 - .../classes/class.ilObjQuestionPoolGUI.php | 58 ++-- .../class.ilObjQuestionPoolXMLParser.php | 2 +- .../classes/class.ilQuestionpoolExport.php | 188 +++++++++++ .../class.ilTestQuestionPoolExporter.php | 12 +- .../class.ilTestQuestionPoolImporter.php | 30 +- ...class.ilTestQuestionPoolLegacyImporter.php | 45 +-- .../class.ilAssQuestionLifecycle.php | 2 +- .../ExportImport/Envelopes/QuestionImage.php | 7 +- .../ExportImport/Export/CollectsQuestions.php | 26 +- .../Export/QuestionPoolCollector.php | 5 +- .../Export/QuestionPoolExporter.php | 18 +- .../Foundation/Bridge/ExportState.php | 3 +- .../Foundation/Bridge/StateHolder.php | 3 +- .../Foundation/Bridge/XmlExporterBridge.php | 8 +- .../src/ExportImport/Foundation/Builder.php | 25 +- .../Importing/ImportSessionRepository.php | 26 +- .../Importing/ImportStageRunner.php | 15 +- .../Normalizing/Attributes/Normalizes.php | 10 +- .../Foundation/Normalizing/Envelopes/Id.php | 33 +- .../Normalizer/DateTimeNormalizer.php | 2 +- .../Normalizer/IlObjectNormalizer.php | 8 +- .../Normalizer/Legacy11UUIDNormalizer.php | 4 +- .../Normalizing/Normalizer/Registry.php | 2 +- .../Normalizer/ResourceNormalizer.php | 20 +- .../Normalizer/TransformationNormalizer.php | 2 +- .../Normalizing/Pipes/CollectResources.php | 21 +- .../Normalizing/Pipes/DenormalizeCarry.php | 1 + .../Normalizing/Transformations.php | 4 +- .../src/ExportImport/Foundation/Pipeline.php | 18 +- .../Serializing/JSONMemoryDeserializer.php | 2 +- .../Serializing/JSONMemorySerializer.php | 42 +-- .../Serializing/SimpleXMLSerializer.php | 8 +- .../Serializing/XMLDeserializerTrait.php | 8 +- .../Serializing/XMLFileDeserializer.php | 12 +- .../Serializing/XMLMemoryDeserializer.php | 3 +- .../Setup/NormalizerArtifactObjective.php | 44 +-- .../src/ExportImport/Import/CleanupStage.php | 3 +- .../Import/DetectLegacyImportStage.php | 5 +- .../Import/QuestionPoolImporter.php | 5 - .../Import/QuestionSelectionStage.php | 35 +- .../ExportImport/Import/QuestionsImporter.php | 2 +- .../Import/SkillAssignmentsImporter.php | 2 +- .../Import/UploadValidationStage.php | 9 +- .../src/ExportImport/LoggingProvider.php | 7 +- .../Pipes/CollectQuestionImages.php | 7 +- lang/ilias_de.lang | 2 +- lang/ilias_en.lang | 2 +- 85 files changed, 1066 insertions(+), 561 deletions(-) create mode 100644 components/ILIAS/Test/classes/class.ilTestResultsToXML.php create mode 100644 components/ILIAS/TestQuestionPool/classes/class.ilQuestionpoolExport.php diff --git a/components/ILIAS/Test/classes/class.ilObjTestGUI.php b/components/ILIAS/Test/classes/class.ilObjTestGUI.php index 8b1e6c63f5f2..1c9fd1a9fe13 100755 --- a/components/ILIAS/Test/classes/class.ilObjTestGUI.php +++ b/components/ILIAS/Test/classes/class.ilObjTestGUI.php @@ -18,14 +18,43 @@ declare(strict_types=1); +use ILIAS\Data\Factory as DataFactory; +use ILIAS\Filesystem\Util\Archive\Archives; +use ILIAS\GlobalScreen\Services as GlobalScreen; +use ILIAS\ResourceStorage\Services as IRSS; +use ILIAS\Skill\Service\SkillService; +use ILIAS\Skill\Service\SkillUsageService; +use ILIAS\Style\Content\Service as ContentStyle; +use ILIAS\Taxonomy\DomainService as TaxonomyService; +use ILIAS\Test\ExportImport\DBRepository as ExportRepository; +use ILIAS\Test\ExportImport\Factory as ExportImportFactory; use ILIAS\Test\ExportImport\Import\PersistStage; +use ILIAS\Test\GUIFactory; use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\Skill\Service\SkillUsageService; +use ILIAS\Test\Logging\LogTable; +use ILIAS\Test\Logging\TestAdministrationInteractionTypes; +use ILIAS\Test\Logging\TestQuestionAdministrationInteractionTypes; +use ILIAS\Test\Participants\ParticipantRepository; +use ILIAS\Test\Presentation\TabsManager; +use ILIAS\Test\Presentation\TestScreenGUI; +use ILIAS\Test\Questions\Presentation\Printer as QuestionPrinter; +use ILIAS\Test\Questions\Presentation\QuestionsTable; +use ILIAS\Test\Questions\Presentation\QuestionsTableActions; +use ILIAS\Test\Questions\Presentation\QuestionsTableQuery; +use ILIAS\Test\Questions\Properties\Repository as TestQuestionsRepository; +use ILIAS\Test\RequestDataCollector; +use ILIAS\Test\ResponseHandler; +use ILIAS\Test\Results\Data\Factory as ResultsDataFactory; use ILIAS\Test\Results\Data\Repository as TestResultRepository; +use ILIAS\Test\Results\Presentation\Factory as ResultsPresentationFactory; +use ILIAS\Test\Scoring\Manual\ConsecutiveScoringGUI; use ILIAS\Test\Scoring\Marks\MarkSchemaFactory; +use ILIAS\Test\Scoring\Marks\MarkSchemaGUI; use ILIAS\Test\Scoring\Marks\MarksRepository; use ILIAS\Test\Settings\MainSettings\MainSettingsRepository; +use ILIAS\Test\Settings\MainSettings\SettingsMainGUI; use ILIAS\Test\Settings\ScoreReporting\ScoreSettingsRepository; +use ILIAS\Test\Settings\ScoreReporting\SettingsScoringGUI; use ILIAS\Test\Settings\SettingsFactory; use ILIAS\Test\Settings\SettingsNotFoundException; use ILIAS\Test\Settings\Templates\PersonalSettingsCreateAction; @@ -39,28 +68,7 @@ use ILIAS\Test\Settings\Templates\PersonalSettingsTableExportAction; use ILIAS\Test\Settings\Templates\PersonalSettingsTableShowAction; use ILIAS\Test\TestDIC; -use ILIAS\Test\RequestDataCollector; -use ILIAS\Test\ResponseHandler; use ILIAS\Test\Utilities\TitleColumnsBuilder; -use ILIAS\Test\Questions\Presentation\QuestionsTable; -use ILIAS\Test\Questions\Presentation\QuestionsTableQuery; -use ILIAS\Test\Questions\Presentation\QuestionsTableActions; -use ILIAS\Test\Questions\Presentation\Printer as QuestionPrinter; -use ILIAS\Test\Questions\Properties\Repository as TestQuestionsRepository; -use ILIAS\Test\Participants\ParticipantRepository; -use ILIAS\Test\Settings\MainSettings\SettingsMainGUI; -use ILIAS\Test\Settings\ScoreReporting\SettingsScoringGUI; -use ILIAS\Test\Scoring\Marks\MarkSchemaGUI; -use ILIAS\Test\Scoring\Manual\ConsecutiveScoringGUI; -use ILIAS\Test\Logging\LogTable; -use ILIAS\Test\Logging\TestQuestionAdministrationInteractionTypes; -use ILIAS\Test\Logging\TestAdministrationInteractionTypes; -use ILIAS\Test\Presentation\TestScreenGUI; -use ILIAS\Test\Presentation\TabsManager; -use ILIAS\Test\Results\Data\Factory as ResultsDataFactory; -use ILIAS\Test\Results\Presentation\Factory as ResultsPresentationFactory; -use ILIAS\Test\ExportImport\Factory as ExportImportFactory; -use ILIAS\Test\ExportImport\DBRepository as ExportRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportStageRunner; @@ -69,26 +77,18 @@ use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionSelectionStage; use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; +use ILIAS\TestQuestionPool\Import\TestQuestionsImportTrait; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ILIAS\TestQuestionPool\RequestDataCollector as QPLRequestDataCollector; -use ILIAS\TestQuestionPool\Import\TestQuestionsImportTrait; -use ILIAS\Data\Factory as DataFactory; -use ILIAS\UI\Component\Modal\Modal; -use ILIAS\UI\URLBuilder; use ILIAS\UI\Component\Input\Container\Form\Form; -use ILIAS\UI\Component\Input\Input; -use ILIAS\UI\Component\Input\Field\Select; use ILIAS\UI\Component\Input\Field\Radio; +use ILIAS\UI\Component\Input\Field\Select; use ILIAS\UI\Component\Input\Field\SwitchableGroup; -use ILIAS\GlobalScreen\Services as GlobalScreen; -use ILIAS\Filesystem\Stream\Streams; -use ILIAS\Filesystem\Util\Archive\Archives; -use ILIAS\Skill\Service\SkillService; -use ILIAS\ResourceStorage\Services as IRSS; -use ILIAS\Taxonomy\DomainService as TaxonomyService; -use ILIAS\Style\Content\Service as ContentStyle; +use ILIAS\UI\Component\Input\Input; +use ILIAS\UI\Component\Modal\Modal; +use ILIAS\UI\URLBuilder; +use ILIAS\UICore\GlobalTemplate; use ILIAS\User\Profile\PublicProfileGUI; -use ILIAS\Test\GUIFactory; use Psr\Log\LoggerInterface; /** @@ -397,8 +397,6 @@ public function executeCommand(): void $this->ui_renderer, $this->irss, $this->request, - $this->export_repository, - $this->temp_file_system, $this->participant_access_filter_factory, $this->test_pass_result_repository, new ilTestHTMLGenerator() @@ -1411,8 +1409,7 @@ public function processImportObject(): void return; } - $runner = $this->buildImportStageRunner(); - $result = $runner->run(); + $result = $this->buildImportStageRunner()->run(); switch ($result->type) { case StageResultType::INTERACT: @@ -1426,7 +1423,11 @@ public function processImportObject(): void break; case StageResultType::ERROR: - $this->tpl->setOnScreenMessage('failure', $result->error_message, true); + $this->tpl->setOnScreenMessage( + GlobalTemplate::MESSAGE_TYPE_FAILURE, + $result->error_message, + true + ); break; case StageResultType::COMPLETE: @@ -1455,21 +1456,23 @@ private function afterImportCompleted(ImportContext $context): void $question_skill_assignments_import_fails = new ilAssQuestionSkillAssignmentImportFails($new_obj->getId()); if ($question_skill_assignments_import_fails->failedImportsRegistered()) { $this->tpl->setOnScreenMessage( - 'info', + GlobalTemplate::MESSAGE_TYPE_INFO, $question_skill_assignments_import_fails->getFailedImportsMessage($this->lng), true ); } - $this->tpl->setOnScreenMessage('success', $this->lng->txt('object_imported'), true); + $this->tpl->setOnScreenMessage( + GlobalTemplate::MESSAGE_TYPE_SUCCESS, + $this->lng->txt('object_imported'), + true + ); $this->ctrl->setParameterByClass(ilObjTestGUI::class, 'ref_id', $new_obj->getRefId()); $this->ctrl->redirectByClass(self::class, self::SHOW_QUESTIONS_CMD); } private function buildImportStageRunner(): ImportStageRunner { - $form_action = $this->ctrl->getFormActionByClass(self::class, 'processImport'); - return new ImportStageRunner( [ new UploadValidationStage( @@ -1485,7 +1488,7 @@ private function buildImportStageRunner(): ImportStageRunner $this->component_factory, $this->ui_factory, $this->request, - $form_action, + $this->ctrl->getFormActionByClass(self::class, 'processImport'), $this->lng->txt('import_tst') ), new PersistStage( diff --git a/components/ILIAS/Test/classes/class.ilTestExportGUI.php b/components/ILIAS/Test/classes/class.ilTestExportGUI.php index c2d5cb6058b9..ae932ca0d0ad 100755 --- a/components/ILIAS/Test/classes/class.ilTestExportGUI.php +++ b/components/ILIAS/Test/classes/class.ilTestExportGUI.php @@ -19,12 +19,10 @@ declare(strict_types=1); use ILIAS\Test\Scoring\Manual\TestScoring; -use ILIAS\Test\ExportImport\DBRepository; use ILIAS\Test\Results\Data\Repository as TestResultsRepository; use ILIAS\UI\Factory as UIFactory; use ILIAS\UI\Renderer as UIRenderer; use ILIAS\ResourceStorage\Services as IRSS; -use ILIAS\Filesystem\Filesystem; use Psr\Http\Message\ServerRequestInterface; /** @@ -45,8 +43,6 @@ public function __construct( private readonly UIRenderer $ui_renderer, private readonly IRSS $irss, private readonly ServerRequestInterface $request, - private readonly DBRepository $export_repository, - private readonly Filesystem $temp_file_system, private readonly ilTestParticipantAccessFilterFactory $participant_access_filter_factory, private readonly TestResultsRepository $test_results_repository, private readonly ilTestHTMLGenerator $html_generator diff --git a/components/ILIAS/Test/classes/class.ilTestExporter.php b/components/ILIAS/Test/classes/class.ilTestExporter.php index 27ef39a891c4..9dbcb826d5ec 100755 --- a/components/ILIAS/Test/classes/class.ilTestExporter.php +++ b/components/ILIAS/Test/classes/class.ilTestExporter.php @@ -65,7 +65,7 @@ public function getXmlExportTailDependencies(string $a_entity, string $a_target_ } // If the default export option was used, the state is not initialized yet. - if ($this->state_holder->exists() === false) { + if (!$this->state_holder->exists()) { $this->initExportState( 'components/ILIAS/Test', $a_target_release, diff --git a/components/ILIAS/Test/classes/class.ilTestImporter.php b/components/ILIAS/Test/classes/class.ilTestImporter.php index 7922e637ec52..e4ffba4da970 100755 --- a/components/ILIAS/Test/classes/class.ilTestImporter.php +++ b/components/ILIAS/Test/classes/class.ilTestImporter.php @@ -20,10 +20,10 @@ use ILIAS\Data\ReferenceId; use ILIAS\Test\ExportImport\Import\TestImporter; +use ILIAS\Test\TestDIC; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XMLMemoryDeserializer; use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; -use ILIAS\Test\TestDIC; class ilTestImporter extends ilXmlImporter { @@ -34,11 +34,12 @@ class ilTestImporter extends ilXmlImporter public function __construct() { parent::__construct(); - $this->legacy_importer = new ilTestLegacyImporter(); $local_dic = TestDIC::dic(); $this->session = $local_dic['exportimport.session']; $this->importer = $local_dic['exportimport.importer']; + + $this->legacy_importer = new ilTestLegacyImporter(); } public function init(): void @@ -96,18 +97,20 @@ private function finalizeTaxonomyUsage(ilImportMapping $a_mapping): void $tst_mappings = $a_mapping->getMappingsOfEntity('components/ILIAS/Test', 'tst'); foreach ($tst_mappings as $old => $new) { - if ($old !== 'new_id' && (int) $old > 0) { - $new_tax_ids = $a_mapping->getMapping( - 'components/ILIAS/Taxonomy', - 'tax_usage_of_obj', - (string) $old - ); - - if ($new_tax_ids !== null) { - $tax_ids = explode(':', $new_tax_ids); - foreach ($tax_ids as $tid) { - ilObjTaxonomy::saveUsage((int) $tid, (int) $new); - } + if ($old === 'new_id' || (int) $old <= 0) { + continue; + } + + $new_tax_ids = $a_mapping->getMapping( + 'components/ILIAS/Taxonomy', + 'tax_usage_of_obj', + (string) $old + ); + + if ($new_tax_ids !== null) { + $tax_ids = explode(':', $new_tax_ids); + foreach ($tax_ids as $tid) { + ilObjTaxonomy::saveUsage((int) $tid, (int) $new); } } } diff --git a/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php b/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php index 8a44bafdc327..5a7f54645879 100755 --- a/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php +++ b/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php @@ -22,7 +22,6 @@ use ILIAS\Test\RequestDataCollector; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; -use ILIAS\TestQuestionPool\ExportImport\Import\QuestionSelectionStage; use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; use ILIAS\TestQuestionPool\Import\TestQuestionsImportTrait; use ILIAS\Test\TestDIC; @@ -31,10 +30,11 @@ class ilTestLegacyImporter extends ilXmlImporter { use TestQuestionsImportTrait; + /** * @var array */ - public static $finallyProcessedTestsRegistry = []; + public static array $finally_processed_tests_registry = []; private readonly TestLogger $logger; private readonly ilDBInterface $db; @@ -64,10 +64,12 @@ public function importXmlRepresentation( $new_obj = new ilObjTest(0, true); $new_obj->setTitle('dummy'); $new_obj->setDescription('test import'); - $new_obj->create(true); + $new_obj->create(); $new_obj->createReference(); - $new_obj->putInTree($this->request_data_collector->getRefId()); - $new_obj->setPermissions($this->request_data_collector->getRefId()); + + $ref_id = $this->request_data_collector->getRefId(); + $new_obj->putInTree($ref_id); + $new_obj->setPermissions($ref_id); $new_obj->saveToDb(); $a_mapping->addMapping('components/ILIAS/Test', 'tst', 'new_id', (string) $new_obj->getId()); @@ -123,10 +125,10 @@ public function importXmlRepresentation( ); $a_mapping->addMapping( - "components/ILIAS/MetaData", - "md", - $a_id . ":0:tst", - $new_obj->getId() . ":0:tst" + 'components/ILIAS/MetaData', + 'md', + "{$a_id}:0:tst", + "{$new_obj->getId()}:0:tst" ); $context = $context->with('test_obj_id', $new_obj->getId())->with('test_ref_id', $new_obj->getRefId()); @@ -169,20 +171,21 @@ public function finalProcessing(ilImportMapping $a_mapping): void $maps = $a_mapping->getMappingsOfEntity("components/ILIAS/Test", "tst"); foreach ($maps as $old => $new) { - if ($old == "new_id" || (int) $old <= 0) { + if ($old === 'new_id' || (int) $old <= 0) { continue; } - if (isset(self::$finallyProcessedTestsRegistry[$new])) { + if (isset(self::$finally_processed_tests_registry[$new])) { continue; } + /** @var ?ilObjTest $test_obj */ $test_obj = ilObjectFactory::getInstanceByObjId((int) $new, false); if ($test_obj->isRandomTest()) { $this->finalRandomTestTaxonomyProcessing($a_mapping, (string) $old, $new, $test_obj); } - self::$finallyProcessedTestsRegistry[$new] = true; + self::$finally_processed_tests_registry[$new] = true; } } @@ -297,8 +300,8 @@ protected function importQuestionSkillAssignments( $importer->import(); if ($importer->getFailedImportAssignmentList()->assignmentsExist()) { - $qsaImportFails = new ilAssQuestionSkillAssignmentImportFails($test_obj->getId()); - $qsaImportFails->registerFailedImports($importer->getFailedImportAssignmentList()); + $qsa_import_fails = new ilAssQuestionSkillAssignmentImportFails($test_obj->getId()); + $qsa_import_fails->registerFailedImports($importer->getFailedImportAssignmentList()); $test_obj->getObjectProperties()->storePropertyIsOnline( $test_obj->getObjectProperties()->getPropertyIsOnline()->withOffline() @@ -327,8 +330,8 @@ protected function importSkillLevelThresholds( $importer->import(); if ($importer->getFailedThresholdImportSkillList()->skillsExist()) { - $sltImportFails = new ilTestSkillLevelThresholdImportFails($test_obj->getId()); - $sltImportFails->registerFailedImports($importer->getFailedThresholdImportSkillList()); + $slt_import_fails = new ilTestSkillLevelThresholdImportFails($test_obj->getId()); + $slt_import_fails->registerFailedImports($importer->getFailedThresholdImportSkillList()); $test_obj->setOfflineStatus(true); } diff --git a/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php b/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php index af218dfcdfbc..eef6e4391bbb 100755 --- a/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php +++ b/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php @@ -499,7 +499,7 @@ public function toNormalized(Transformations $tt): Transformation $normalized['taxonomy_filter'][] = [ 'tax_id' => $tt->normalize(new Id($tax_id, 'tax')), 'node_ids' => array_map( - fn($node_id) => $tt->normalize(new Id($node_id, 'tax_node')), + static fn($node_id): mixed => $tt->normalize(new Id($node_id, 'tax_node')), $node_ids ), ]; @@ -509,7 +509,7 @@ public function toNormalized(Transformations $tt): Transformation $normalized['mapped_taxonomy_filter'][] = [ 'tax_id' => $tt->normalize(new Id($tax_id, 'mapped_tax')), 'node_ids' => array_map( - fn($node_id) => $tt->normalize(new Id($node_id, 'mapped_tax_node')), + static fn($node_id): mixed => $tt->normalize(new Id($node_id, 'mapped_tax_node')), $node_ids ), ]; @@ -540,7 +540,7 @@ public function fromNormalized(Transformations $tt): Transformation foreach ($normalized['taxonomy_filter'] as $item) { $tax_id = $tt->denormalize($item['tax_id'], Id::class)->getId(); $taxonomy_filter[$tax_id] = array_map( - fn($node_id) => $tt->denormalize($node_id, Id::class)->getId(), + static fn($node_id): mixed => $tt->denormalize($node_id, Id::class)->getId(), $item['node_ids'] ); } @@ -550,7 +550,7 @@ public function fromNormalized(Transformations $tt): Transformation foreach (($normalized['mapped_taxonomy_filter'] ?? []) as $item) { $tax_id = $tt->denormalize($item['tax_id'], Id::class)->getId(); $mapped_taxonomy_filter[$tax_id] = array_map( - fn($node_id) => $tt->denormalize($node_id, Id::class)->getId(), + static fn($node_id): mixed => $tt->denormalize($node_id, Id::class)->getId(), $item['node_ids'] ); } diff --git a/components/ILIAS/Test/classes/class.ilTestResultsToXML.php b/components/ILIAS/Test/classes/class.ilTestResultsToXML.php new file mode 100644 index 000000000000..55bd7f87c0eb --- /dev/null +++ b/components/ILIAS/Test/classes/class.ilTestResultsToXML.php @@ -0,0 +1,303 @@ +include_random_test_questions_enabled; + } + + public function setIncludeRandomTestQuestionsEnabled(bool $include_random_test_questions_enabled): void + { + $this->include_random_test_questions_enabled = $include_random_test_questions_enabled; + } + + protected function exportActiveIDs(): void + { + $user_criteria = (new ilSetting('assessment'))->get('user_criteria', 'usr_id'); + + $query = $this->test_obj->getAnonymity() + ? 'SELECT * FROM tst_active WHERE test_fi = %s' + : "SELECT tst_active.*, usr_data.{$user_criteria} FROM tst_active, usr_data WHERE tst_active.test_fi = %s AND tst_active.user_fi = usr_data.usr_id"; + $result = $this->db->queryF($query, [ilDBConstants::T_INTEGER], [$this->test_obj->getTestId()]); + + $test_participant_list = new ilTestParticipantList($this->test_obj, $this->user, $this->lng, $this->db); + $test_participant_list->initializeFromDbRows($this->test_obj->getTestParticipants()); + + $this->xmlStartTag('tst_active', null); + while ($row = $this->db->fetchAssoc($result)) { + $this->active_ids[] = $row['active_id']; + $participant = $test_participant_list->getParticipantByActiveId($row['active_id']); + + $attrs = [ + 'active_id' => $row['active_id'], + 'user_fi' => $this->test_obj->getAnonymity() ? '' : ($row['user_fi'] ?? ''), + 'fullname' => $participant ? $test_participant_list->buildFullname($participant) : '', + 'anonymous_id' => $row['anonymous_id'] ?? '', + 'test_fi' => $row['test_fi'], + 'lastindex' => $row['lastindex'] ?? '', + 'tries' => $row['tries'] ?? '', + 'last_started_pass' => $row['last_started_pass'] ?? '', + 'last_finished_pass' => $row['last_finished_pass'] ?? '', + 'submitted' => $row['submitted'] ?? '', + 'submittimestamp' => $row['submittimestamp'] ?? '', + 'tstamp' => $row['tstamp'] ?? '' + ]; + + if (!$this->test_obj->getAnonymity()) { + $attrs['user_criteria'] = $user_criteria; + $attrs[$user_criteria] = $row[$user_criteria]; + } + + $this->xmlElement('row', $attrs); + } + $this->xmlEndTag('tst_active'); + } + + protected function exportPassResult(): void + { + $query = 'SELECT * FROM tst_pass_result WHERE ' . $this->db->in('active_fi', $this->active_ids, false, 'integer') . ' ORDER BY active_fi, pass'; + $result = $this->db->query($query); + $this->xmlStartTag('tst_pass_result', null); + while ($row = $this->db->fetchAssoc($result)) { + $attrs = [ + 'active_fi' => $row['active_fi'], + 'pass' => $row['pass'] ?? '', + 'points' => $row['points'] ?? '', + 'maxpoints' => $row['maxpoints'] ?? '', + 'questioncount' => $row['questioncount'] ?? '', + 'answeredquestions' => $row['answeredquestions'] ?? '', + 'workingtime' => $row['workingtime'] ?? '', + 'tstamp' => $row['tstamp'] ?? '' + ]; + $this->xmlElement('row', $attrs); + } + $this->xmlEndTag('tst_pass_result'); + } + + protected function exportResultCache(): void + { + $query = 'SELECT * FROM tst_result_cache WHERE ' . $this->db->in('active_fi', $this->active_ids, false, 'integer') . ' ORDER BY active_fi'; + $result = $this->db->query($query); + $this->xmlStartTag('tst_result_cache', null); + while ($row = $this->db->fetchAssoc($result)) { + $attrs = [ + 'active_fi' => $row['active_fi'], + 'pass' => $row['pass'], + 'max_points' => $row['max_points'], + 'reached_points' => $row['reached_points'], + 'mark_short' => $row['mark_short'], + 'mark_official' => $row['mark_official'], + 'passed' => $row['passed'], + 'failed' => $row['failed'], + 'tstamp' => $row['tstamp'] + ]; + $this->xmlElement('row', $attrs); + } + $this->xmlEndTag('tst_result_cache'); + } + + protected function exportTestSequence(): void + { + $query = 'SELECT * FROM tst_sequence WHERE ' . $this->db->in('active_fi', $this->active_ids, false, 'integer') . ' ORDER BY active_fi, pass'; + $result = $this->db->query($query); + $this->xmlStartTag('tst_sequence', null); + while ($row = $this->db->fetchAssoc($result)) { + $attrs = [ + 'active_fi' => $row['active_fi'], + 'pass' => $row['pass'] ?? '', + 'sequence' => $row['sequence'] ?? '', + 'postponed' => $row['postponed'] ?? '', + 'hidden' => $row['hidden'] ?? '', + 'tstamp' => $row['tstamp'] ?? '' + ]; + $this->xmlElement('row', $attrs); + } + $this->xmlEndTag('tst_sequence'); + } + + protected function exportTestSolutions(): void + { + $query = 'SELECT * FROM tst_solutions WHERE ' . $this->db->in('active_fi', $this->active_ids, false, 'integer') . ' ORDER BY solution_id'; + $result = $this->db->query($query); + $this->xmlStartTag('tst_solutions', null); + while ($row = $this->db->fetchAssoc($result)) { + $attrs = [ + 'solution_id' => $row['solution_id'], + 'active_fi' => $row['active_fi'], + 'question_fi' => $row['question_fi'], + 'points' => $row['points'] ?? '', + 'pass' => $row['pass'] ?? '', + 'value1' => $row['value1'] ?? '', + 'value2' => $row['value2'] ?? '', + 'tstamp' => $row['tstamp'] ?? '' + ]; + $this->xmlElement('row', $attrs); + } + $this->xmlEndTag('tst_solutions'); + } + + protected function exportRandomTestQuestions(): void + { + $result = $this->db->query(" + SELECT * FROM tst_test_rnd_qst + WHERE {$this->db->in('active_fi', $this->active_ids, false, 'integer')} + ORDER BY test_random_question_id + "); + + $this->xmlStartTag('tst_test_rnd_qst', null); + while ($row = $this->db->fetchAssoc($result)) { + $attrs = []; + + foreach ($row as $field => $value) { + $attrs[$field] = $value; + } + + $this->xmlElement('row', $attrs); + } + $this->xmlEndTag('tst_test_rnd_qst'); + } + + + protected function exportTestResults(): void + { + $query = 'SELECT * FROM tst_test_result WHERE ' . $this->db->in('active_fi', $this->active_ids, false, 'integer') . ' ORDER BY active_fi'; + $result = $this->db->query($query); + $this->xmlStartTag('tst_test_result', null); + while ($row = $this->db->fetchAssoc($result)) { + $active_fi = $row['active_fi']; + $pass = $row['pass'] ?? ''; + $attrs = [ + 'test_result_id' => $row['test_result_id'], + 'active_fi' => $active_fi, + 'question_fi' => $row['question_fi'], + 'points' => $row['points'] ?? '', + 'pass' => $pass, + 'manual' => $row['manual'] ?? '', + 'tstamp' => $row['tstamp'] ?? '' + ]; + + if (($question = assQuestion::instantiateQuestion($row['question_fi'])) instanceof assFileUpload) { + $this->exportParticipantUploadedFiles($question->getUploadedFiles($active_fi, $pass)); + } + + $this->xmlElement('row', $attrs); + } + $this->xmlEndTag('tst_test_result'); + } + + /** + * @param array{value1: string, value2: string} $uploaded_files + */ + protected function exportParticipantUploadedFiles(array $uploaded_files): void + { + foreach ($uploaded_files as $uploaded_file) { + if ($uploaded_file['value2'] !== 'rid') { + continue; + } + + $rid_string = $uploaded_file['value1']; + $rid = $this->irss->manage()->find($rid_string); + if ($rid === null) { + continue; + } + + $target_dir = "$this->objects_export_directory/resources/$rid_string"; + ilFileUtils::makeDirParents($target_dir); + file_put_contents( + "$target_dir/{$this->irss->manage()->getCurrentRevision($rid)->getTitle()}", + $this->irss->consume()->stream($rid)->getStream(), + ); + } + } + + protected function exportTestTimes(): void + { + $query = 'SELECT * FROM tst_times WHERE ' . $this->db->in('active_fi', $this->active_ids, false, 'integer') . ' ORDER BY active_fi'; + $result = $this->db->query($query); + $this->xmlStartTag('tst_times', null); + while ($row = $this->db->fetchAssoc($result)) { + $attrs = [ + 'times_id' => $row['times_id'], + 'active_fi' => $row['active_fi'], + 'started' => $row['started'], + 'finished' => $row['finished'], + 'pass' => $row['pass'], + 'tstamp' => $row['tstamp'] + ]; + $this->xmlElement('row', $attrs); + } + $this->xmlEndTag('tst_times'); + } + + public function getXML(): void + { + $this->active_ids = []; + $this->xmlHeader(); + $attrs = ['version' => '4.1.0']; + $this->xmlStartTag('results', $attrs); + $this->exportActiveIDs(); + + if ($this->isIncludeRandomTestQuestionsEnabled()) { + $this->exportRandomTestQuestions(); + } + + $this->exportPassResult(); + $this->exportResultCache(); + $this->exportTestSequence(); + $this->exportTestSolutions(); + $this->exportTestResults(); + $this->exportTestTimes(); + $this->xmlEndTag('results'); + } + + public function xmlDumpMem(bool $format = true): string + { + $this->getXML(); + return parent::xmlDumpMem($format); + } + + public function xmlDumpFile(string $file, bool $format = true): void + { + $this->getXML(); + parent::xmlDumpFile($file, $format); + } +} \ No newline at end of file diff --git a/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php b/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php index 0249ba8be06b..1ce13929c0a5 100644 --- a/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php +++ b/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php @@ -32,8 +32,8 @@ class XMLWithResultsOption extends BasicLegacyExportOption { - private Language $lng; - private StateHolder $state_holder; + private readonly Language $lng; + private readonly StateHolder $state_holder; public function init( Container $DIC @@ -64,7 +64,7 @@ public function getLabel(): string $this->lng->loadLanguageModule('exp'); $this->lng->loadLanguageModule('assessment'); - return $this->lng->txt('exp_format_dropdown-xml') . ' (' . $this->lng->txt('ass_create_export_file_with_results') . ')'; + return "{$this->lng->txt('exp_format_dropdown-xml')} ({$this->lng->txt('ass_create_export_file_with_results')})"; } public function onExportOptionSelected(ConsumerContext $context): void @@ -86,11 +86,7 @@ public function onExportOptionSelected(ConsumerContext $context): void ); // Delegate the export to the manager which will call ilTestExporter - $manager->createExport( - 1, - $export_info, - '' - ); + $manager->createExport(1, $export_info, ''); $this->ctrl->redirectByClass(ilExportGUI::class, ilExportGUI::CMD_LIST_EXPORT_FILES); } diff --git a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php index 14b68aba884f..79b289c5c32e 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php +++ b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php @@ -69,8 +69,7 @@ public function process(ImportContext $context): StageResult } } - $importer = new ilImport($this->requested_ref_id); - $importer->importObject( + (new ilImport($this->requested_ref_id))->importObject( null, $context->get(UploadValidationStage::FILE_TO_IMPORT), basename($context->get(UploadValidationStage::FILE_TO_IMPORT)), @@ -87,7 +86,7 @@ private function importMappingsFile(ImportContext $context): ?StageResult { $component_import_dir = dirname($context->get(UploadValidationStage::COMPONENT_IMPORT_FILE)); $mappings_file = "{$component_import_dir}/mappings.xml"; - if (!file_exists($mappings_file) || !is_file($mappings_file)) { + if (!is_file($mappings_file)) { $this->log->error("Mappings file not found: {$mappings_file}"); return StageResult::error($context, $this->lng->txt('obj_import_file_error')); } diff --git a/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php b/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php index f9726a6454ca..2b31f75c63db 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php @@ -106,7 +106,7 @@ private function importSourcePoolDefinition( if ($old_pool_id !== $new_pool_id) { $ref_ids = $this->data_factory->objId($new_pool_id)->toReferenceIds(); - if (count($ref_ids) > 0) { + if ($ref_ids !== []) { $definition->setPoolRefId(current($ref_ids)->toInt()); $this->log->debug("Derived source pool definition from Object ID: {$old_pool_id} -> {$definition->getPoolRefId()}"); } @@ -155,8 +155,7 @@ public function finalizeTaxonomyFilters(ilImportMapping $mapping): void continue; } - $new_filter = $this->remapTaxonomyFilter($mapping, $mapped_filter); - $definition->setMappedTaxonomyFilter($new_filter); + $definition->setMappedTaxonomyFilter($this->remapTaxonomyFilter($mapping, $mapped_filter)); $definition->saveToDb(); $this->log->debug("Remapped taxonomy filter for definition {$definition->getId()}"); } diff --git a/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php b/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php index 697e0f32e202..b3009a6d9711 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php @@ -65,7 +65,7 @@ public function import( $threshold_list = new ilTestSkillLevelThresholdList($this->db); foreach ($normalized_thresholds as $item) { - // TestID and Skill BaseID/TRefID will be replaced by the mapping pipe + // The mapping pipe replaces TestID and Skill BaseID/TRefID $threshold = $transformations->denormalize($item, ilTestSkillLevelThreshold::class); $local_level_id = $this->getLevelIdMapping($import_install_id, $threshold->getSkillLevelId()); @@ -89,7 +89,7 @@ public function import( } $threshold_list->saveToDb(); - $this->log->debug("Saved skill level thresholds"); + $this->log->debug('Saved skill level thresholds'); return $result; } diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php index b7f059b6fb6c..9bc51891b6d5 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php @@ -367,12 +367,13 @@ private function importQuestions( foreach ($list as $normalized) { $question = $this->questions_importer->importQuestion($normalized, $tt, $mapping, $selected_questions); - - if ($question && $normalized['sequence'] !== null) { - $sequence = $tt->int($normalized['sequence']); - $test_object->questions[$sequence] = $question->getId(); - $this->log->debug("Stored question {$question->getId()} at sequence {$sequence} in test"); + if (!$question instanceof \assQuestion || $normalized['sequence'] === null) { + continue; } + + $sequence = $tt->int($normalized['sequence']); + $test_object->questions[$sequence] = $question->getId(); + $this->log->debug("Stored question {$question->getId()} at sequence {$sequence} in test"); } $test_object->saveQuestionsToDb(); @@ -405,7 +406,7 @@ private function importParticipants(array $list, Transformations $tt, ilImportMa $mapping->addMapping('components/ILIAS/Test', 'participant', (string) $old_active_id, (string) $new_active_id); $this->log->debug("Stored participant/test session mapping: {$old_active_id} -> {$new_active_id}"); - // TestID, UserID and ActiveID will be replaced by the mapping pipe + // The mapping pipe replaces TestID, UserID and ActiveID $participant = $tt->denormalize($normalized, Participant::class); $this->database->insert( @@ -433,16 +434,19 @@ private function importParticipants(array $list, Transformations $tt, ilImportMa private function importInvitedParticipant(array $normalized, Transformations $tt): void { - // TestID and UserID will be replaced by the mapping pipe + // The mapping pipe replaces TestID and UserID $participant = $tt->denormalize($normalized, Participant::class); - $this->database->insert('tst_invited_user', [ - 'test_fi' => [ilDBConstants::T_INTEGER, $participant->getTestId()], - 'user_fi' => [ilDBConstants::T_INTEGER, $participant->getUserId()], - 'ip_range_from' => [ilDBConstants::T_TEXT, $participant->getClientIpFrom()], - 'ip_range_to' => [ilDBConstants::T_TEXT, $participant->getClientIpTo()], - 'tstamp' => [ilDBConstants::T_INTEGER, $participant->getInvitationDate()], - ]); + $this->database->insert( + 'tst_invited_user', + [ + 'test_fi' => [ilDBConstants::T_INTEGER, $participant->getTestId()], + 'user_fi' => [ilDBConstants::T_INTEGER, $participant->getUserId()], + 'ip_range_from' => [ilDBConstants::T_TEXT, $participant->getClientIpFrom()], + 'ip_range_to' => [ilDBConstants::T_TEXT, $participant->getClientIpTo()], + 'tstamp' => [ilDBConstants::T_INTEGER, $participant->getInvitationDate()], + ] + ); $this->log->debug("Stored invited participant in database: {$participant->getUserId()} (User ID), {$participant->getTestId()} (Test ID)"); } } diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php index e01733bd9ca9..b8011e106ba3 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php @@ -74,7 +74,7 @@ public function import(array $list, Transformations $tt): void public function importTestSequences(array $list, Transformations $tt): void { foreach ($list as $normalized) { - // ActiveID and QuestionIDs will be replaced by the mapping pipe + // The mapping pipe replaces ActiveID and QuestionIDs $sequence = $tt->denormalize($normalized, ilTestSequence::class); $sequence->saveToDb(); $this->log->debug("Stored test sequence in database: {$sequence->getActiveId()} (Active ID), {$sequence->getPass()} (Pass)"); @@ -84,7 +84,7 @@ public function importTestSequences(array $list, Transformations $tt): void public function importSolutions(array $list, Transformations $tt): void { foreach ($list as $normalized) { - // ActiveID and QuestionID will be replaced by the mapping pipe + // The mapping pipe replaces ActiveID and QuestionID $solution = $tt->denormalize($normalized, Solution::class); $next_id = $this->database->nextId('tst_solutions'); @@ -110,7 +110,7 @@ public function importSolutions(array $list, Transformations $tt): void public function importQuestionResults(array $list, Transformations $tt): void { foreach ($list as $normalized) { - // ActiveID and QuestionID will be replaced by the mapping pipe + // The mapping pipe replaces ActiveID and QuestionID $result = $tt->denormalize($normalized, QuestionResult::class); $next_id = $this->database->nextId('tst_test_result'); @@ -134,7 +134,7 @@ public function importQuestionResults(array $list, Transformations $tt): void public function importAttemptResults(array $list, Transformations $tt): void { foreach ($list as $normalized) { - // ActiveID will be replaced by the mapping pipe + // The mapping pipe replaces ActiveID $attempt = $tt->denormalize($normalized, AttemptResult::class); $this->database->insert( @@ -163,7 +163,7 @@ public function importTestResult(?array $normalized, Transformations $tt): void return; } - // ActiveID will be replaced by the mapping pipe + // The mapping pipe replaces ActiveID $result = $tt->denormalize($normalized, ParticipantResult::class); $this->database->insert( @@ -186,7 +186,7 @@ public function importTestResult(?array $normalized, Transformations $tt): void public function importWorkingTimes(array $list, Transformations $tt): void { foreach ($list as $normalized) { - // ActiveID will be replaced by the mapping pipe + // The mapping pipe replaces ActiveID $working_time = $tt->denormalize($normalized, WorkingTime::class); $next_id = $this->database->nextId('tst_times'); @@ -208,7 +208,7 @@ public function importWorkingTimes(array $list, Transformations $tt): void public function importManualFeedback(array $list, Transformations $tt): void { foreach ($list as $normalized) { - // ActiveID, QuestionID and UserID will be replaced by the mapping pipe + // The mapping pipe replaces ActiveID, QuestionID and UserID $manual_feedback = $tt->denormalize($normalized, ManualFeedback::class); $next_id = $this->database->nextId('tst_manual_fb'); @@ -232,17 +232,17 @@ public function importManualFeedback(array $list, Transformations $tt): void public function importManualScoring(array $normalized, Transformations $tt): void { - // ActiveID will be replaced by the mapping pipe + // The mapping pipe replaces ActiveID, QuestionID and UserID $active_id = $tt->denormalize($normalized['active_id'], Id::class)->getId(); - new TestManScoringDoneHelper()->setDone($active_id, $tt->bool($normalized['done'])); + (new TestManScoringDoneHelper())->setDone($active_id, $tt->bool($normalized['done'])); $this->log->debug("Stored manual scoring in database: {$active_id} (Active ID)"); } public function importAdditionalWorkingTimes(array $list, Transformations $tt): void { foreach ($list as $normalized) { - // UserID and TestID will be replaced by the mapping pipe + // The mapping pipe replaces UserID and TestID $time = $tt->denormalize($normalized, AdditionalWorkingTime::class); $this->database->insert( @@ -261,7 +261,7 @@ public function importAdditionalWorkingTimes(array $list, Transformations $tt): public function importRandomTestQuestions(array $list, Transformations $tt): void { foreach ($list as $normalized) { - // ActiveID, QuestionID and SourcePoolDefinitionID will be replaced by the mapping pipe + // The mapping pipe replaces ActiveID, QuestionID and SourcePoolDefinitionID $question = $tt->denormalize($normalized, RandomTestQuestion::class); $next_id = $this->database->nextId('tst_test_rnd_qst'); diff --git a/components/ILIAS/Test/src/ExportImport/Import/UserImportResolver.php b/components/ILIAS/Test/src/ExportImport/Import/UserImportResolver.php index 90e35fa8c3e0..26730e45317b 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/UserImportResolver.php +++ b/components/ILIAS/Test/src/ExportImport/Import/UserImportResolver.php @@ -55,20 +55,17 @@ public function resolve(UserIdentifiers $criteria, array $users): array ); $query = $this->db->query("SELECT usr_id, {$criteria->value} AS identifier FROM usr_data WHERE {$in_clause}"); - $db_mapping = []; - foreach ($this->db->fetchAll($query) as $row) { - $db_mapping[$row['identifier']] = $row['usr_id']; - } + $db_mapping = array_column($this->db->fetchAll($query), 'usr_id', 'identifier'); $mapping = []; foreach ($users as $original_id => $identifier) { - if (isset($db_mapping[$identifier])) { - $this->log->debug("User identifier {$identifier} found, mapping user {$original_id} to {$db_mapping[$identifier]}"); - $mapping[$original_id] = $db_mapping[$identifier]; - } else { + if (!isset($db_mapping[$identifier])) { $this->log->warning("User identifier {$identifier} not found for user {$original_id}, using anonymous user ID"); $mapping[$original_id] = ANONYMOUS_USER_ID; } + + $this->log->debug("User identifier {$identifier} found, mapping user {$original_id} to {$db_mapping[$identifier]}"); + $mapping[$original_id] = $db_mapping[$identifier]; } return $mapping; diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php index f91a2aff589d..59e54fcafafa 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php @@ -113,7 +113,6 @@ private function normalizeTestObj(ilTestQuestionSetConfig $config): array { $reflection = new ReflectionClass($config); $property = $reflection->getProperty('test_obj'); - $property->setAccessible(true); $test_obj = $property->getValue($config); if (!$test_obj instanceof ilObjTest) { @@ -153,11 +152,19 @@ public function denormalize(array|float|bool|int|string|null $value, string $typ private function denormalizeQuestionSetConfig(array $normalized, ilObjTest $test_obj): ilTestQuestionSetConfig { - $class = $normalized['type'] === ilObjTest::QUESTION_SET_TYPE_RANDOM - ? ilTestRandomQuestionSetConfig::class - : ilTestFixedQuestionSetConfig::class; + if($normalized['type'] === ilObjTest::QUESTION_SET_TYPE_FIXED) { + return new ilTestFixedQuestionSetConfig( + $this->dic->repositoryTree(), + $this->dic->database(), + $this->dic->language(), + $this->test_dic['logging.logger'], + $this->dic['component.repository'], + $test_obj, + $this->test_dic['question.general_properties.repository'] + ); + } - $config = new $class( + $config = new ilTestRandomQuestionSetConfig( $this->dic->repositoryTree(), $this->dic->database(), $this->dic->language(), @@ -167,10 +174,6 @@ private function denormalizeQuestionSetConfig(array $normalized, ilObjTest $test $this->test_dic['question.general_properties.repository'] ); - if ($config instanceof ilTestFixedQuestionSetConfig) { - return $config; - } - $amount_mode = $this->tt->string($normalized['amount_mode']); if (!$config->isValidQuestionAmountConfigurationMode($amount_mode)) { throw new ilTestException("Invalid random test question set config amount mode given: {$amount_mode}"); @@ -201,7 +204,7 @@ private function denormalizeStagingPools(array $normalized): array $pool_id = $this->tt->denormalize($staging_pool['pool_id'], Id::class)->getId(); $staging_pools[$pool_id] = array_map( - fn($question) => $this->tt->denormalize($question, Id::class)->getId(), + fn($question): mixed => $this->tt->denormalize($question, Id::class)->getId(), $staging_pool['questions'] ); } diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSequenceNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSequenceNormalizer.php index 00762cb1e893..44cfb4556513 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSequenceNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSequenceNormalizer.php @@ -72,7 +72,7 @@ public function normalize($value): array|float|bool|int|string|null private function normalizeQuestions(array $questions): array { return array_map( - fn(int $question_id) => $this->tt->normalize(new Id($question_id, 'question')), + fn(int $question_id): mixed => $this->tt->normalize(new Id($question_id, 'question')), $questions ); } @@ -116,7 +116,7 @@ private function denormalizeSequence(array $normalized): array private function denormalizeQuestions(array $normalized): array { return array_map( - fn(mixed $item) => $this->tt->denormalize($item, Id::class)->getId(), + fn(mixed $item): mixed => $this->tt->denormalize($item, Id::class)->getId(), $normalized ); } diff --git a/components/ILIAS/Test/src/ExportImport/Pipes/CollectUserIds.php b/components/ILIAS/Test/src/ExportImport/Pipes/CollectUserIds.php index 5f727a3fd49e..b099476a60e7 100644 --- a/components/ILIAS/Test/src/ExportImport/Pipes/CollectUserIds.php +++ b/components/ILIAS/Test/src/ExportImport/Pipes/CollectUserIds.php @@ -40,10 +40,12 @@ class CollectUserIds implements Pipe */ public function handle(mixed $passable, \Closure $next): mixed { - if ($passable instanceof NormalizeCarry && $passable->value instanceof Id) { - if ($passable->value->getObject() === 'user') { - $this->ids[$passable->value->getId()] = true; - } + if ( + $passable instanceof NormalizeCarry + && $passable->value instanceof Id + && $passable->value->getObject() === 'user' + ) { + $this->ids[$passable->value->getId()] = true; } return $next($passable); diff --git a/components/ILIAS/Test/src/ExportImport/TestCollector.php b/components/ILIAS/Test/src/ExportImport/TestCollector.php index 538d194e3b6f..65f2ec8b0c66 100644 --- a/components/ILIAS/Test/src/ExportImport/TestCollector.php +++ b/components/ILIAS/Test/src/ExportImport/TestCollector.php @@ -101,11 +101,7 @@ public function getTestId(): int public function getObject(): ilObjTest { - if ($this->test === null) { - $this->test = new ilObjTest($this->object_id->toInt(), false); - } - - return $this->test; + return $this->test ??= new ilObjTest($this->object_id->toInt(), false); } public function getSettings(): array @@ -129,9 +125,7 @@ public function getUserMapping(array $user_ids): array $mapping = []; if ($export_identifier === UserIdentifiers::USER_ID) { - foreach ($user_ids as $user_id) { - $mapping[$user_id] = $user_id; - } + $mapping = array_combine($user_ids, $user_ids); } else { $in_clause = $this->db->in('usr_id', $user_ids, false, ilDBConstants::T_INTEGER); $query = $this->db->query("SELECT usr_id, {$export_identifier->value} FROM usr_data WHERE {$in_clause}"); @@ -167,10 +161,7 @@ public function getQuestionProperties(): array */ public function getTestQuestionProperties(): array { - if ($this->questions === null) { - $this->questions = $this->questions_repository->getQuestionPropertiesForTest($this->getObject()); - } - return $this->questions; + return $this->questions ??= $this->questions_repository->getQuestionPropertiesForTest($this->getObject()); } /** @@ -268,14 +259,19 @@ public function getParticipants(): Generator */ public function getParticipantsIds(): array { - if ($this->participants === null) { - $this->participants = []; - foreach ($this->getParticipants() as $participant) { - if ($participant->getActiveId() !== null) { - $this->participants[] = $participant->getActiveId(); - } + if ($this->participants !== null) { + return $this->participants; + } + + $this->participants = []; + foreach ($this->getParticipants() as $participant) { + if ($participant->getActiveId() === null) { + continue; } + + $this->participants[] = $participant->getActiveId(); } + return $this->participants; } @@ -402,7 +398,8 @@ public function getSequences(int $participant_id, array $attempts): array $test_sequence->loadFromDb(); $sequences[] = $test_sequence; } - return $sequences; + + return $sequences ?? []; } diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index 8df3d140781f..0b41ce7575e9 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -116,7 +116,7 @@ private function extractObjectId(ExportState $state): ?ObjectId { $target_ids = $state->target()->getObjectIds(); - if (count($target_ids) === 0) { + if ($target_ids === []) { $state->logger()->warning('No target object IDs found for test export'); return null; } @@ -242,15 +242,16 @@ public function write(ExportState $state): void $resource_pipe = $state->transformations()->context(CollectResources::class); foreach ($question_image_pipe->getFiles() as $file) { - if (file_exists($file['from'])) { - $state->writer()->writeFileByFilePath( - $file['from'], - "{$export_dir}/" . $file['to'] - ); - $state->logger()->debug("Copied question image {$file['from']} to {$export_dir}/{$file['to']}"); - } else { + if (!file_exists($file['from'])) { $state->logger()->warning('Question image file not found: ' . $file['from']); + continue; } + + $state->writer()->writeFileByFilePath( + $file['from'], + "{$export_dir}/" . $file['to'] + ); + $state->logger()->debug("Copied question image {$file['from']} to {$export_dir}/{$file['to']}"); } foreach ($resource_pipe->getResources() as $id => $resource) { diff --git a/components/ILIAS/Test/src/Results/Data/Repository.php b/components/ILIAS/Test/src/Results/Data/Repository.php index f7168d7850d2..71b3d65b1c9a 100644 --- a/components/ILIAS/Test/src/Results/Data/Repository.php +++ b/components/ILIAS/Test/src/Results/Data/Repository.php @@ -153,7 +153,7 @@ public function updateTestResultCache(int $active_id, ?\ilAssQuestionProcessLock public function getTestAttemptResult(int $active_id, int $attempt): ?AttemptResult { $result = $this->db->queryF( - "SELECT * FROM tst_pass_result WHERE active_fi = %s AND pass = %s", + 'SELECT * FROM tst_pass_result WHERE active_fi = %s AND pass = %s', [\ilDBConstants::T_INTEGER, \ilDBConstants::T_INTEGER], [$active_id, $attempt] ); @@ -163,7 +163,7 @@ public function getTestAttemptResult(int $active_id, int $attempt): ?AttemptResu public function getTestAttemptResults(int $active_id): array { $result = $this->db->queryF( - "SELECT * FROM tst_pass_result WHERE active_fi = %s", + 'SELECT * FROM tst_pass_result WHERE active_fi = %s', [\ilDBConstants::T_INTEGER], [$active_id] ); diff --git a/components/ILIAS/Test/src/TestDIC.php b/components/ILIAS/Test/src/TestDIC.php index e8641b1a9b71..97dd4496a5cf 100755 --- a/components/ILIAS/Test/src/TestDIC.php +++ b/components/ILIAS/Test/src/TestDIC.php @@ -302,17 +302,20 @@ protected static function buildDIC(ILIASContainer $DIC): self $DIC->fileConverters()->images(), $DIC->filesystem() ); + $dic['exportimport.test_results_importer'] = static fn($c): TestResultsImporter => new TestResultsImporter( $DIC->database(), $c['exportimport.logging']() ); + $dic['exportimport.random_test_config_importer'] = static fn($c): RandomTestConfigImporter => new RandomTestConfigImporter( $DIC->database(), $c['exportimport.logging'](), new DataFactory() ); + $dic['exportimport.importer'] = static fn($c): TestImporter => new TestImporter( $c['exportimport.builder'], diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php index 999ba3865d36..347c508aa7f0 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\Refinery\Transformation; use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; /** * Class for answers with a binary state indicator @@ -47,12 +47,18 @@ class ASS_AnswerBinaryStateImage extends ASS_AnswerBinaryState * @param string $answertext A string defining the answer text * @param double $points The number of points given for the selected answer * @param integer $order A nonnegative value representing a possible display or sort order - * @param bool $state A integer value indicating the state of the answer - * @param string $a_image The image filename + * @param bool $state A boolen value indicating the state of the answer + * @param ?string $a_image The image filename * @param integer $id The database id of the answer */ - public function __construct(string $answertext = "", float $points = 0.0, int $order = 0, bool $state = false, ?string $a_image = null, int $id = -1) - { + public function __construct( + string $answertext = '', + float $points = 0.0, + int $order = 0, + bool $state = false, + ?string $a_image = null, + int $id = -1 + ) { parent::__construct($answertext, (float) $points, $order, $state, $id); $this->setImage($a_image); } @@ -82,9 +88,9 @@ public function toNormalized(Transformations $tt): Transformation { return $tt->custom()->transformation(fn(array $context): array => [ ...$tt->normalize(parent::toNormalized($tt)), - 'image' => $this->image ? $tt->normalize( - new QuestionImage($this->image, $context['question_id'] ?? null) - ) : null, + 'image' => $this->image ? + $tt->normalize(new QuestionImage($this->image, $context['question_id'] ?? null)) + : null, ]); } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php index c2a986f71671..d8a978e28193 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php @@ -89,9 +89,9 @@ public function toNormalized(Transformations $tt): Transformation { return $tt->custom()->transformation(fn(array $context): array => [ 'text' => $this->text, - 'picture' => $this->picture ? $tt->normalize( - new QuestionImage($this->picture, $context['question_id'] ?? null) - ) : null, + 'picture' => $this->picture + ? $tt->normalize(new QuestionImage($this->picture, $context['question_id'] ?? null)) + : null, 'identifier' => $this->identifier, ]); } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php index c410a7e23463..d1945f7f6146 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php @@ -86,9 +86,9 @@ public function toNormalized(Transformations $tt): Transformation { return $tt->custom()->transformation(fn(array $context): array => [ ...$tt->normalize(parent::toNormalized($tt)), - 'image' => $this->image ? $tt->normalize( - new QuestionImage($this->image, $context['question_id'] ?? null) - ) : null, + 'image' => $this->image + ? $tt->normalize(new QuestionImage($this->image, $context['question_id'] ?? null)) + : null, ]); } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php index 72b59cab0c11..9a16c41b0b2a 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php @@ -235,7 +235,7 @@ public function toNormalized(Transformations $tt): Transformation */ public function fromNormalized(Transformations $tt): Transformation { - return $tt->custom()->transformation(fn(array $normalized) => new static( + return $tt->custom()->transformation(fn(array $normalized): static => new static( $tt->string($normalized['answertext']), $tt->float($normalized['points']), $tt->int($normalized['order']), diff --git a/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php b/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php index 1f12458585bb..99c9138e23e7 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php @@ -19,7 +19,6 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\Refinery\Transformation; -use ILIAS\Refinery\Random\Transformation\ShuffleTransformation; /** * Class for cloze question gaps diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php index 0dbd51dc5742..8ad50006b908 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php @@ -22,7 +22,6 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\Refinery\Transformation; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; -use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\Test\Logging\AdditionalInformationGenerator; /** diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php index 438a44270f2f..329dc24b70be 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php @@ -16,10 +16,10 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; /** * Formula Question Result @@ -898,7 +898,10 @@ public function fromNormalized(Transformations $tt): Transformation { return $tt->custom()->transformation(function (array $normalized) use ($tt): self { $clone = clone $this; - $clone->available_units = array_map(fn(array $unit) => $tt->denormalize($unit, new assFormulaQuestionUnit()), $normalized['available_units']); + $clone->available_units = array_map( + static fn(array $unit): assFormulaQuestionUnit => $tt->denormalize($unit, new assFormulaQuestionUnit()), + $normalized['available_units'] + ); $clone->range_min = $tt->float($normalized['range_min']); $clone->range_max = $tt->float($normalized['range_max']); $clone->range_min_txt = $tt->string($normalized['range_min_txt']); diff --git a/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php index e61177ffbf3f..34078ddbac20 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php @@ -57,19 +57,19 @@ class assImagemapQuestion extends assQuestion implements ilObjAnswerScoringAdjus 'POLY' => 'poly']; /** @var $answers array The possible answers of the imagemap question. */ - public $answers; + public array $answers; /** @var $image_filename string The image file containing the name of image file. */ - public $image_filename; + public string $image_filename; /** @var $imagemap_contents string The variable containing contents of an imagemap file. */ - public $imagemap_contents; + public string $imagemap_contents; /** @var $coords array */ - public $coords; + public array $coords; /** @var $is_multiple_choice bool Defines weather the Question is a Single or a Multiplechoice question. */ - protected $is_multiple_choice = false; + protected bool $is_multiple_choice = false; /** * assImagemapQuestion constructor @@ -955,7 +955,7 @@ public function fromNormalized(Transformations $tt): Transformation $clone->image_filename = $tt->denormalize($normalized['image'], QuestionImage::class)->getFilename(); $clone->is_multiple_choice = $tt->bool($normalized['multiple_choice']); $clone->answers = array_map( - fn(array $answer) => $tt->denormalize($answer, new ASS_AnswerImagemap()), + static fn(array $answer): ASS_AnswerImagemap => $tt->denormalize($answer, new ASS_AnswerImagemap()), $normalized['answers'] ); diff --git a/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php index d25f05d1b540..475be6e0d14d 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php @@ -957,7 +957,7 @@ public function fromNormalized(Transformations $tt): Transformation $clone->scorePartialSolutionEnabled = $tt->bool($normalized['score_partial_solution']); $clone->specific_feedback_setting = $tt->int($normalized['specific_feedback_setting']); $clone->answers = array_map( - fn(array $answer) => $tt->denormalize($answer, new ilAssKprimChoiceAnswer()), + static fn(array $answer): ilAssKprimChoiceAnswer => $tt->denormalize($answer, new ilAssKprimChoiceAnswer()), $normalized['answers'] ); diff --git a/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php index 12a4e4373195..afbe2646a5aa 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php @@ -983,7 +983,7 @@ public function fromNormalized(Transformations $tt): Transformation $clone->selection_limit = $tt->nullableInt($normalized['selection_limit']); $clone->is_singleline = $tt->bool($normalized['single_line']); $clone->answers = array_map( - fn(array $answer) => $tt->denormalize($answer, new ASS_AnswerMultipleResponseImage()), + static fn(array $answer): ASS_AnswerMultipleResponseImage => $tt->denormalize($answer, new ASS_AnswerMultipleResponseImage()), $normalized['answers'] ); diff --git a/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php index e0e31c3841dc..a6472faa4e14 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php @@ -18,13 +18,13 @@ declare(strict_types=1); +use ILIAS\Refinery\Transformation; +use ILIAS\Test\Logging\AdditionalInformationGenerator; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; -use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\Questions\Ordering\OrderingQuestionDatabaseRepository as OQRepository; -use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; +use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; /** * Class for ordering questions @@ -1378,7 +1378,10 @@ public function toNormalized(Transformations $tt): Transformation return $tt->custom()->transformation(fn(): array => [ ...$tt->normalize(parent::toNormalized($tt)), 'ordering_type' => $this->ordering_type, - 'ordering_elements' => $tt->normalize($this->getOrderingElementList()->getElements(), ['question_id' => $this->getId()]), + 'ordering_elements' => $tt->normalize( + $this->getOrderingElementList()->getElements(), + ['question_id' => $this->getId()] + ), ]); } @@ -1391,7 +1394,7 @@ public function fromNormalized(Transformations $tt): Transformation $clone = parent::fromNormalized($tt)->transform($normalized); $clone->ordering_type = $tt->int($normalized['ordering_type']); $denormalized_elements = array_map( - fn(array $element) => $tt->denormalize($element, new ilAssOrderingElement()), + static fn(array $element): ilAssOrderingElement => $tt->denormalize($element, new ilAssOrderingElement()), $normalized['ordering_elements'] ); $clone->element_list_for_deferred_saving = new ilAssOrderingElementList(null, $denormalized_elements); diff --git a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php index 0abc3787d1b8..9be93d19bd78 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php @@ -18,34 +18,34 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; +use ILIAS\DI\Container; +use ILIAS\DI\LoggingServices; +use ILIAS\HTTP\Services as HTTPServices; +use ILIAS\Notes\InternalDataService as NotesInternalDataService; +use ILIAS\Notes\Note; +use ILIAS\Notes\NoteDBRepository as NotesRepo; +use ILIAS\Notes\NotesManager; +use ILIAS\Notes\Service as NotesService; +use ILIAS\Refinery\Factory as Refinery; +use ILIAS\Refinery\Transformation; +use ILIAS\Skill\Service\SkillUsageService; +use ILIAS\Test\Logging\AdditionalInformationGenerator; +use ILIAS\Test\Logging\TestParticipantInteraction; +use ILIAS\Test\Logging\TestParticipantInteractionTypes; +use ILIAS\Test\Logging\TestQuestionAdministrationInteraction; +use ILIAS\Test\Logging\TestQuestionAdministrationInteractionTypes; use ILIAS\Test\Results\Data\Repository as TestResultRepository; use ILIAS\Test\TestDIC; -use ILIAS\TestQuestionPool\Questions\QuestionPartiallySaveable; -use ILIAS\TestQuestionPool\Questions\Question; -use ILIAS\TestQuestionPool\Questions\SuggestedSolution\SuggestedSolution; -use ILIAS\TestQuestionPool\Questions\SuggestedSolution\SuggestedSolutionsDatabaseRepository; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\TestQuestionPool\Questions\Files\QuestionFiles; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; +use ILIAS\TestQuestionPool\Questions\Question; +use ILIAS\TestQuestionPool\Questions\QuestionPartiallySaveable; +use ILIAS\TestQuestionPool\Questions\SuggestedSolution\SuggestedSolution; +use ILIAS\TestQuestionPool\Questions\SuggestedSolution\SuggestedSolutionsDatabaseRepository; use ILIAS\TestQuestionPool\RequestDataCollector; -use ILIAS\Test\Logging\TestParticipantInteraction; -use ILIAS\Test\Logging\TestQuestionAdministrationInteraction; -use ILIAS\Test\Logging\TestParticipantInteractionTypes; -use ILIAS\Test\Logging\TestQuestionAdministrationInteractionTypes; -use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\Refinery\Transformation; -use ILIAS\DI\Container; -use ILIAS\Skill\Service\SkillUsageService; -use ILIAS\Notes\Service as NotesService; -use ILIAS\Notes\InternalDataService as NotesInternalDataService; -use ILIAS\Notes\NoteDBRepository as NotesRepo; -use ILIAS\Notes\NotesManager; -use ILIAS\Notes\Note; -use ILIAS\DI\LoggingServices; -use ILIAS\Refinery\Factory as Refinery; -use ILIAS\HTTP\Services as HTTPServices; /** * @author Helmut Schottmüller @@ -3015,7 +3015,10 @@ public function fromNormalized(Transformations $tt): Transformation $clone->shuffle = $tt->bool($normalized['shuffle']); $clone->suggested_solutions = array_map( - fn(array $suggested_solution) => $tt->denormalize($suggested_solution, SuggestedSolution::class), + static fn(array $suggested_solution): SuggestedSolution => $tt->denormalize( + $suggested_solution, + SuggestedSolution::class + ), $normalized['suggested_solutions'] ); diff --git a/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php index 8a15886c81a0..150c10d4dc16 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php @@ -972,7 +972,7 @@ public function fromNormalized(Transformations $tt): Transformation $clone->is_singleline = $tt->bool($normalized['is_singleline']); $clone->feedback_setting = $tt->int($normalized['feedback_setting']); $clone->answers = array_map( - fn(array $answer) => $tt->denormalize($answer, new ASS_AnswerBinaryStateImage()), + static fn(array $answer): ASS_AnswerBinaryStateImage => $tt->denormalize($answer, new ASS_AnswerBinaryStateImage()), $normalized['answers'] ); return $clone; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php index 7228f5cb4f04..19cee4cf0744 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php @@ -18,12 +18,12 @@ declare(strict_types=1); +use ILIAS\Refinery\Transformation; +use ILIAS\Test\Logging\AdditionalInformationGenerator; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; -use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; /** * Class for text questions @@ -888,9 +888,13 @@ public function fromNormalized(Transformations $tt): Transformation $clone->matchcondition = $tt->int($normalized['matchcondition']); $clone->keyword_relation = $tt->string($normalized['keyword_relation']); $clone->answers = array_map( - fn(array $answer) => $tt->denormalize($answer, new ASS_AnswerMultipleResponseImage()), + static fn(array $answer): ASS_AnswerMultipleResponseImage => $tt->denormalize( + $answer, + new ASS_AnswerMultipleResponseImage() + ), $normalized['answers'] ); + return $clone; }); } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php b/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php index 09bb06fe7f91..41017b4d8873 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php @@ -18,12 +18,12 @@ declare(strict_types=1); +use ILIAS\Refinery\Transformation; +use ILIAS\Test\Logging\AdditionalInformationGenerator; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; -use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; /** * Class for TextSubset questions @@ -782,7 +782,10 @@ public function fromNormalized(Transformations $tt): Transformation $clone->text_rating = $tt->string($normalized['text_rating']); $clone->correctanswers = $tt->int($normalized['correct_answers']); $clone->answers = array_map( - fn(array $answer) => $tt->denormalize($answer, new ASS_AnswerBinaryStateImage()), + static fn(array $answer): ASS_AnswerBinaryStateImage => $tt->denormalize( + $answer, + new ASS_AnswerBinaryStateImage() + ), $normalized['answers'] ); return $clone; diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php b/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php index 827000926ccd..25449d7ab303 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php @@ -1,9 +1,9 @@ custom()->transformation(fn(array $context): array => [ 'position' => $this->position, 'answertext' => $this->answertext, - 'image' => $this->imageFile ? $tt->normalize( - new QuestionImage($this->imageFile, $context['question_id'] ?? null) - ) : null, + 'image' => $this->imageFile + ? $tt->normalize(new QuestionImage($this->imageFile, $context['question_id'] ?? null)) + : null, 'correctness' => $this->correctness, ]); } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPool.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPool.php index 2a666800da05..8e0ecc27941a 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPool.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPool.php @@ -33,7 +33,6 @@ class ilObjQuestionPool extends ilObject { private ilComponentRepository $component_repository; - private ilBenchmark $benchmark; private bool $skill_service_enabled; private GeneralQuestionPropertiesRepository $questionrepository; @@ -42,7 +41,6 @@ public function __construct(int $a_id = 0, bool $a_call_by_reference = true) { global $DIC; $this->component_repository = $DIC['component.repository']; - $this->benchmark = $DIC['ilBench']; $local_dic = QuestionPoolDIC::dic(); $this->questionrepository = $local_dic['question.general_properties.repository']; diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php index 82aefbd088de..757ac78ece50 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php @@ -18,35 +18,35 @@ declare(strict_types=1); +use ILIAS\Data\Factory as DataFactory; +use ILIAS\Filesystem\Util\Archive\Archives; +use ILIAS\FileUpload\MimeType; +use ILIAS\GlobalScreen\Services as GlobalScreen; use ILIAS\Skill\Service\SkillUsageService; -use ILIAS\TestQuestionPool\ExportImport\Import\CleanupStage; -use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; -use ILIAS\TestQuestionPool\QuestionPoolDIC; -use ILIAS\TestQuestionPool\Import\TestQuestionsImportTrait; -use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; -use ILIAS\TestQuestionPool\ExportImport\Import\QuestionSelectionStage; -use ILIAS\TestQuestionPool\ExportImport\Import\PersistStage; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportStageRunner; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; +use ILIAS\Style\Content\Service as ContentStyle; +use ILIAS\Taxonomy\Service; +use ILIAS\Test\Settings\GlobalSettings\GlobalTestSettings; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportStageRunner; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResult; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResultType; -use ILIAS\TestQuestionPool\RequestDataCollector; -use ILIAS\TestQuestionPool\Questions\Presentation\QuestionTable; +use ILIAS\TestQuestionPool\ExportImport\Import\CleanupStage; +use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; +use ILIAS\TestQuestionPool\ExportImport\Import\PersistStage; +use ILIAS\TestQuestionPool\ExportImport\Import\QuestionSelectionStage; +use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; +use ILIAS\TestQuestionPool\Import\TestQuestionsImportTrait; +use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; -use ILIAS\Test\Settings\GlobalSettings\GlobalTestSettings; -use ILIAS\Taxonomy\Service; +use ILIAS\TestQuestionPool\Questions\Presentation\QuestionTable; +use ILIAS\TestQuestionPool\RequestDataCollector; use ILIAS\UI\Component\Input\Container\Form\Form; use ILIAS\UI\Component\Input\Field\Select; use ILIAS\UI\Component\Input\Input; use ILIAS\UI\URLBuilder; use ILIAS\UI\URLBuilderToken; -use ILIAS\Data\Factory as DataFactory; -use ILIAS\GlobalScreen\Services as GlobalScreen; -use ILIAS\Filesystem\Util\Archive\Archives; -use ILIAS\FileUpload\MimeType; -use ILIAS\UI\Component\Modal\RoundTrip as RoundTripModal; -use ILIAS\Style\Content\Service as ContentStyle; +use ILIAS\UICore\GlobalTemplate; use Psr\Log\LoggerInterface; /** @@ -1135,8 +1135,8 @@ public function processImportObject(): void match ($result->type) { StageResultType::INTERACT => $this->renderImportStage($runner, $result), StageResultType::ADVANCE => $this->ctrl->redirectByClass(self::class, 'processImport'), - StageResultType::ERROR => $this->renderImportError($runner, $result), - StageResultType::COMPLETE => $this->renderImportSuccess($runner, $result), + StageResultType::ERROR => $this->renderImportError($result), + StageResultType::COMPLETE => $this->renderImportSuccess($result), }; } @@ -1181,17 +1181,25 @@ private function renderImportStage(ImportStageRunner $runner, StageResult $resul ); } - private function renderImportError(ImportStageRunner $runner, StageResult $result): void + private function renderImportError(StageResult $result): void { - $this->tpl->setOnScreenMessage('failure', $result->error_message, true); + $this->tpl->setOnScreenMessage( + GlobalTemplate::MESSAGE_TYPE_FAILURE, + $result->error_message, + true + ); } - private function renderImportSuccess(ImportStageRunner $runner, StageResult $result): void + private function renderImportSuccess(StageResult $result): void { $pool_obj_id = $this->data_factory->objId($result->context->get('pool_obj_id')); $pool_ref_id = $pool_obj_id->toReferenceIds()[0]->toInt(); - $this->tpl->setOnScreenMessage('success', $this->lng->txt('object_imported'), true); + $this->tpl->setOnScreenMessage( + GlobalTemplate::MESSAGE_TYPE_SUCCESS, + $this->lng->txt('object_imported'), + true + ); $this->ctrl->setParameter($this, 'ref_id', $pool_ref_id); $this->ctrl->redirectByClass(self::class, self::DEFAULT_CMD); } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolXMLParser.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolXMLParser.php index bc2632ef42f5..005a6255745e 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolXMLParser.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolXMLParser.php @@ -17,7 +17,7 @@ *********************************************************************/ /** - * @deprecated This class is only used for legacy imports and will be removed with further ILIAS versions. + * @deprecated This class is used only for legacy imports and will be removed in a future ILIAS release */ class ilObjQuestionPoolXMLParser extends ilSaxParser { diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilQuestionpoolExport.php b/components/ILIAS/TestQuestionPool/classes/class.ilQuestionpoolExport.php new file mode 100644 index 000000000000..7425310d8a8d --- /dev/null +++ b/components/ILIAS/TestQuestionPool/classes/class.ilQuestionpoolExport.php @@ -0,0 +1,188 @@ + + * + * @version $Id$ + * + * @ingroup components\ILIASTestQuestionPool + * + * @deprecated 12: Use ILIAS\TestQuestionPool\ExportImport\QuestionpoolExporter instead + */ +class ilQuestionpoolExport +{ + public ilErrorHandling $err; // error object + public ilDBInterface $db; // database object + public ILIAS $ilias; // ilias object + public string $inst_id; // installation id + public ilLanguage $lng; + + private string $export_dir = ''; + private string $subdir = ''; + private string $filename = ''; + private string $zipfilename = ''; + private string $qti_filename = ''; + private ilXmlWriter $xml; + + /** + * Constructor + * @access public + */ + public function __construct( + protected ilObjQuestionPool $qpl_obj, + protected string $mode = "xml", + protected ?array $questions = null + ) { + global $DIC; + $this->err = $DIC['ilErr']; + $this->db = $DIC['ilDB']; + $this->ilias = $DIC['ilias']; + $this->lng = $DIC['lng']; + + if (!is_array($this->questions)) { + $this->questions = $this->qpl_obj->getAllQuestionIds(); + } + + $this->inst_id = IL_INST_ID; + $date = time(); + + $this->qpl_obj->createExportDirectory(); + switch ($this->mode) { + case "xml": + $this->export_dir = $this->qpl_obj->getExportDirectory('xml'); + $this->subdir = $date . "__" . $this->inst_id . "__" . + "qpl" . "_" . $this->qpl_obj->getId(); + $this->filename = $this->subdir . ".xml"; + $this->qti_filename = $date . "__" . $this->inst_id . "__" . + "qti" . "_" . $this->qpl_obj->getId() . ".xml"; + break; + case "xlsx": + $this->export_dir = $this->qpl_obj->getExportDirectory('xlsx'); + $this->filename = $date . "__" . $this->inst_id . "__" . + "qpl" . "_" . $this->qpl_obj->getId() . ".xlsx"; + $this->zipfilename = $date . "__" . $this->inst_id . "__" . + "qpl" . "_" . $this->qpl_obj->getId() . ".zip"; + break; + default: + $this->export_dir = $this->qpl_obj->getExportDirectory('zip'); + $this->subdir = $date . "__" . $this->inst_id . "__" . + "qpl" . "_" . $this->qpl_obj->getId(); + $this->filename = $this->subdir . ".xml"; + $this->qti_filename = $date . "__" . $this->inst_id . "__" . + "qti" . "_" . $this->qpl_obj->getId() . ".xml"; + break; + } + } + + public function getInstId() + { + return $this->inst_id; + } + + + /** + * build export file (complete zip file) + */ + public function buildExportFile(): string + { + switch ($this->mode) { + case "xlsx": + return $this->buildExportFileXLSX(); + case "xml": + default: + return $this->buildExportFileXML(); + } + } + + /** + * build xml export file + */ + public function buildExportFileXML(): string + { + global $DIC; + $ilBench = $DIC['ilBench']; + + $ilBench->start("QuestionpoolExport", "buildExportFile"); + + $this->xml = new ilXmlWriter(); + $this->xml->xmlSetDtdDef(""); + $this->xml->xmlSetGenCmt("Export of ILIAS Test Questionpool " . + $this->qpl_obj->getId() . " of installation " . $this->inst_id); + $this->xml->xmlHeader(); + + ilFileUtils::makeDir($this->export_dir . "/" . $this->subdir); + ilFileUtils::makeDir($this->export_dir . "/" . $this->subdir . "/objects"); + + $expDir = $this->qpl_obj->getExportDirectory(); + ilFileUtils::makeDirParents($expDir); + + $expLog = new ilLog($expDir, "export.log"); + $expLog->delete(); + $expLog->setLogFormat(""); + $expLog->write(date("[y-m-d H:i:s] ") . "Start Export"); + + $qti_file = fopen($this->export_dir . "/" . $this->subdir . "/" . $this->qti_filename, "w"); + fwrite($qti_file, $this->qpl_obj->questionsToXML($this->questions)); + fclose($qti_file); + + $ilBench->start("QuestionpoolExport", "buildExportFile_getXML"); + $this->qpl_obj->objectToXmlWriter( + $this->xml, + $this->inst_id, + $this->export_dir . "/" . $this->subdir, + $expLog, + $this->questions + ); + $ilBench->stop("QuestionpoolExport", "buildExportFile_getXML"); + + $ilBench->start("QuestionpoolExport", "buildExportFile_dumpToFile"); + $this->xml->xmlDumpFile($this->export_dir . "/" . $this->subdir . "/" . $this->filename, false); + $ilBench->stop("QuestionpoolExport", "buildExportFile_dumpToFile"); + + $ilBench->start("QuestionpoolExport", "buildExportFile_saveAdditionalMobs"); + $this->exportXHTMLMediaObjects($this->export_dir . "/" . $this->subdir); + $ilBench->stop("QuestionpoolExport", "buildExportFile_saveAdditionalMobs"); + + $ilBench->start("QuestionpoolExport", "buildExportFile_zipFile"); + ilFileUtils::zip($this->export_dir . "/" . $this->subdir, $this->export_dir . "/" . $this->subdir . ".zip"); + + $ilBench->stop("QuestionpoolExport", "buildExportFile_zipFile"); + + $expLog->write(date("[y-m-d H:i:s] ") . "Finished Export"); + $ilBench->stop("QuestionpoolExport", "buildExportFile"); + + return $this->export_dir . "/" . $this->subdir . ".zip"; + } + + public function exportXHTMLMediaObjects($a_export_dir): void + { + foreach ($this->questions as $question_id) { + $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $question_id); + foreach ($mobs as $mob) { + if (ilObjMediaObject::_exists($mob)) { + $mob_obj = new ilObjMediaObject($mob); + $mob_obj->exportFiles($a_export_dir); + unset($mob_obj); + } + } + } + } +} \ No newline at end of file diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php index 54e4513695f7..b50fad4eeb33 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php @@ -37,8 +37,9 @@ public function init(): void /** * Returns the final XML content for the question pool. * - * This method is called after `getXmlExportTailDependencies()`. At this point the export writer and export - * directory are available, so the preprocessed export can be written to disk and returned as xml. + * This method is called after `getXmlExportTailDependencies()`. + * At this point the export writer and export directory are available, + * so the preprocessed export can be written to disk and returned as xml. */ public function getXmlRepresentation(string $a_entity, string $a_schema_version, string $a_id): string { @@ -52,8 +53,9 @@ public function getXmlRepresentation(string $a_entity, string $a_schema_version, /** * Collects export tail dependencies for the question pool. * - * The export framework calls this method before `getXmlRepresentation()`. Therefore this method only prepares and - * processes the export in memory using the export state. The export state is created if it does not exist yet. + * The export framework calls this method before `getXmlRepresentation()`. + * Therefore this method only prepares and processes the export in memory using the export state. + * The export state is created if it does not exist yet. */ public function getXmlExportTailDependencies(string $a_entity, string $a_target_release, array $a_ids): array { @@ -62,7 +64,7 @@ public function getXmlExportTailDependencies(string $a_entity, string $a_target_ } // If the default export option was used, the state is not initialized yet. - if ($this->state_holder->exists() === false) { + if (!$this->state_holder->exists()) { $this->initExportState( 'components/ILIAS/TestQuestionPool', $a_target_release, diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php index 1c6a2a528a4d..7c3d3f6b6752 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php @@ -92,19 +92,23 @@ private function finalizeTaxonomyUsage(ilImportMapping $a_mapping): void $qpl_mappings = $a_mapping->getMappingsOfEntity('components/ILIAS/TestQuestionPool', 'qpl'); foreach ($qpl_mappings as $old => $new) { - if ($old !== 'new_id' && (int) $old > 0) { - $new_tax_ids = $a_mapping->getMapping( - 'components/ILIAS/Taxonomy', - 'tax_usage_of_obj', - (string) $old - ); - - if ($new_tax_ids !== null) { - $tax_ids = explode(':', $new_tax_ids); - foreach ($tax_ids as $tid) { - ilObjTaxonomy::saveUsage((int) $tid, (int) $new); - } - } + if ($old === 'new_id' || (int) $old <= 0) { + continue; + } + + $new_tax_ids = $a_mapping->getMapping( + 'components/ILIAS/Taxonomy', + 'tax_usage_of_obj', + (string) $old + ); + + if ($new_tax_ids === null) { + continue; + } + + $tax_ids = explode(':', $new_tax_ids); + foreach ($tax_ids as $tid) { + ilObjTaxonomy::saveUsage((int) $tid, (int) $new); } } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php index 5924475c1875..f4f09b48a77d 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php @@ -62,10 +62,7 @@ public function importXmlRepresentation( $context = $context->with('pool_obj_id', $this->pool_obj->getId()); $this->session->setContext($context); - $qpl_parser = new ilObjQuestionPoolXMLParser( - $this->pool_obj, - $xml_file - ); + $qpl_parser = new ilObjQuestionPoolXMLParser($this->pool_obj, $xml_file); $qpl_parser->startParsing(); // set another question pool name (if possible) @@ -86,11 +83,7 @@ public function importXmlRepresentation( ); $qti_parser->startParsing(); - $page_parser = new ilQuestionPageParser( - $this->pool_obj, - $xml_file, - $import_base_dir - ); + $page_parser = new ilQuestionPageParser($this->pool_obj, $xml_file, $import_base_dir); $page_parser->setQuestionMapping($qti_parser->getImportMapping()); $page_parser->startParsing(); @@ -136,14 +129,18 @@ public function finalProcessing(ilImportMapping $a_mapping): void { $maps = $a_mapping->getMappingsOfEntity('components/ILIAS/TestQuestionPool', 'qpl'); foreach ($maps as $old => $new) { - if ($old !== 'new_id' && (int) $old > 0) { - $new_tax_ids = $a_mapping->getMapping('components/ILIAS/Taxonomy', 'tax_usage_of_obj', (string) $old); - if ($new_tax_ids !== null) { - $tax_ids = explode(':', $new_tax_ids); - foreach ($tax_ids as $tid) { - ilObjTaxonomy::saveUsage((int) $tid, (int) $new); - } - } + if ($old === 'new_id' || (int) $old <= 0) { + continue; + } + + $new_tax_ids = $a_mapping->getMapping('components/ILIAS/Taxonomy', 'tax_usage_of_obj', (string) $old); + if ($new_tax_ids === null) { + continue; + } + + $tax_ids = explode(':', $new_tax_ids); + foreach ($tax_ids as $tid) { + ilObjTaxonomy::saveUsage((int) $tid, (int) $new); } } } @@ -162,11 +159,15 @@ protected function importQuestionSkillAssignments($xmlFile, ilImportMapping $map $importer->import(); - if ($importer->getFailedImportAssignmentList()->assignmentsExist()) { - $fails = new ilAssQuestionSkillAssignmentImportFails($targetParentObjId); - $fails->registerFailedImports($importer->getFailedImportAssignmentList()); - - $this->pool_obj->getObjectProperties()->storePropertyIsOnline($this->pool_obj->getObjectProperties()->getPropertyIsOnline()->withOffline()); + if (!$importer->getFailedImportAssignmentList()->assignmentsExist()) { + return; } + + $fails = new ilAssQuestionSkillAssignmentImportFails($targetParentObjId); + $fails->registerFailedImports($importer->getFailedImportAssignmentList()); + + $this->pool_obj->getObjectProperties()->storePropertyIsOnline( + $this->pool_obj->getObjectProperties()->getPropertyIsOnline()->withOffline() + ); } } diff --git a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionLifecycle.php b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionLifecycle.php index 7b43c435e033..49ba023e22c2 100755 --- a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionLifecycle.php +++ b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionLifecycle.php @@ -174,7 +174,7 @@ public function toNormalized(Transformations $tt): Transformation public function fromNormalized(Transformations $tt): Transformation { return $tt->custom()->transformation( - fn(array $normalized) => self::getInstance($normalized['identifier']) + static fn(array $normalized): ilAssQuestionLifecycle => self::getInstance($normalized['identifier']) ); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/QuestionImage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/QuestionImage.php index a766438425eb..bc3f6621e67b 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/QuestionImage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/QuestionImage.php @@ -24,8 +24,8 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; /** - * Wrapper object for an image file within a question (e.g. an image map question). An image belongs either to the - * question text, answer options or the suggested solution. + * Wrapper object for an image file within a question (e.g. an image map question). + * An image belongs either to the question text, answer options or the suggested solution. */ class QuestionImage implements Envelope { @@ -60,10 +60,9 @@ public function getId(): string return $this->id; } - public function setId(string $id): static + public function setId(string $id): void { $this->id = $id; - return $this; } /** diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/CollectsQuestions.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/CollectsQuestions.php index 0cfb3dde61f8..27102ad8b283 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/CollectsQuestions.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/CollectsQuestions.php @@ -24,14 +24,14 @@ use assFormulaQuestionUnitCategory; use assQuestion; use Generator; -use ilAssQuestionSkillAssignmentList; use ilAssClozeTestFeedback; use ilAssMultiOptionQuestionFeedback; +use ilAssQuestionSkillAssignmentList; use ilAssSpecificFeedbackIdentifierList; use ilDBInterface; use ILIAS\Data\ObjectId; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Envelopes\Feedback; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\Questions\GeneralQuestionProperties; use ilUnitConfigurationRepository; @@ -95,12 +95,14 @@ public function getUnitsAndCategories(int $question_id): array $data['categories'][] = $item; } - if ($item instanceof assFormulaQuestionUnit) { - if ($item->getBaseUnit() === 0 || $item->getBaseUnit() === $item->getId()) { - $data['base_units'][] = $item; - } else { - $data['units'][] = $item; - } + if (!$item instanceof assFormulaQuestionUnit) { + continue; + } + + if ($item->getBaseUnit() === 0 || $item->getBaseUnit() === $item->getId()) { + $data['base_units'][] = $item; + } else { + $data['units'][] = $item; } } @@ -116,14 +118,12 @@ public function getUnitsAndCategories(int $question_id): array */ public function getFeedback(assQuestion $question): Feedback { - $feedback = new Feedback( + return new Feedback( new Id($question->getId(), 'question'), $question->feedbackOBJ->getGenericFeedbackExportPresentation($question->getId(), false), $question->feedbackOBJ->getGenericFeedbackExportPresentation($question->getId(), true), $this->loadSpecificFeedback($question), ); - - return $feedback; } private function loadSpecificFeedback(assQuestion $question): array @@ -136,12 +136,13 @@ private function loadSpecificFeedback(assQuestion $question): array return []; } + $feedback = []; + // Cloze question type specific feedback uses the identifier list to load the answer-specific feedback. if ($question->feedbackOBJ instanceof ilAssClozeTestFeedback) { $feedback_list = new ilAssSpecificFeedbackIdentifierList(); $feedback_list->load($question->getId()); - $feedback = []; foreach ($feedback_list as $identifier) { $feedback[$identifier->getAnswerIndex()] = [ 'answer_index' => $identifier->getAnswerIndex(), @@ -153,6 +154,7 @@ private function loadSpecificFeedback(assQuestion $question): array ), ]; } + return $feedback; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolCollector.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolCollector.php index 28f91dfb8be1..6117764f053d 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolCollector.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolCollector.php @@ -75,10 +75,7 @@ public function getObject(): ilObjQuestionPool */ public function getQuestionProperties(): array { - if ($this->questions === null) { - $this->questions = $this->question_repository->getForParentObjectId($this->pool_id->toInt()); - } - return $this->questions; + return $this->questions ??= $this->question_repository->getForParentObjectId($this->pool_id->toInt()); } private function database(): ilDBInterface diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php index f472d0a43f81..ffff633640d8 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php @@ -25,13 +25,13 @@ use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\ObjectId; use ILIAS\Data\UUID\Factory as UUIDFactory; +use ILIAS\Taxonomy\DomainService as Taxonomy; use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportState; use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportStep; use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Exporter; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Serializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\Taxonomy\DomainService as Taxonomy; use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; @@ -73,10 +73,7 @@ public function prepare(ExportState $state): void ); $state->setCollector($collector); - $question_image_pipe = new CollectQuestionImages( - new UUIDFactory(), - $pool_id - ); + $question_image_pipe = new CollectQuestionImages(new UUIDFactory(), $pool_id); $transformations = $this->builder ->withAdditionalPipes([$question_image_pipe]) @@ -155,7 +152,7 @@ private function extractObjectId(ExportState $state): ?ObjectId { $target_ids = $state->target()->getObjectIds(); - if (count($target_ids) === 0) { + if ($target_ids === []) { $state->logger()->warning('No target object IDs found for question pool export'); return null; } @@ -197,14 +194,13 @@ private function exportQuestions( foreach ($collector->getQuestionObjects() as $question) { $normalized = [ ... $transformations->normalize($question), - 'feedback' => $transformations->normalize( - $collector->getFeedback($question) - ) + 'feedback' => $transformations->normalize($collector->getFeedback($question)) ]; if ($question instanceof assFormulaQuestion) { - $data = $collector->getUnitsAndCategories($question->getId()); - $normalized['formula_data'] = $transformations->normalize($data); + $normalized['formula_data'] = $transformations->normalize( + $collector->getUnitsAndCategories($question->getId()) + ); } $serializer->append('question', $normalized); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php index 5b005ca56e1d..0721f0956c4e 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php @@ -36,7 +36,7 @@ */ class ExportState { - private ExportStep $step; + private ExportStep $step = ExportStep::INIT; private ?Logger $logger = null; private ?ExportPath $path_info = null; private ?Transformations $transformations = null; @@ -54,7 +54,6 @@ public function __construct( private ExportConfig $config, private string $option = '' ) { - $this->step = ExportStep::INIT; } public function target(): ExportTarget diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/StateHolder.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/StateHolder.php index fb5dd3dcac1c..9b3726207d60 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/StateHolder.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/StateHolder.php @@ -38,8 +38,7 @@ public function create( ExportConfig $config, string $option = '' ): ExportState { - $this->export_state = new ExportState($target, $config, $option); - return $this->export_state; + return $this->export_state ??= new ExportState($target, $config, $option); } public function exists(): bool diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php index 2ce4297e0741..512df474c02b 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php @@ -55,14 +55,14 @@ private function processExport(): ExportState $this->logger->debug("Preparing export for component {$state->target()->getComponent()}..."); $state->setLogger($this->logger); $this->exporter->prepare($state); - $this->logger->debug("...Finished preparing export"); + $this->logger->debug('...Finished preparing export'); } if ($state->getStep()->value < ExportStep::PROCESS->value) { $this->logger->debug("Processing export for component {$state->target()->getComponent()}..."); $state->setSerializer(new SimpleXMLSerializer()->open('memory')); $this->exporter->process($state); - $this->logger->debug("...Finished processing export"); + $this->logger->debug('...Finished processing export'); } $this->state_holder->set($state); @@ -83,7 +83,7 @@ private function finalizeExport(): ExportState $this->logger->debug("Writing export for component {$state->target()->getComponent()}..."); $this->exporter->write($state); - $this->logger->debug("...Finished writing export"); + $this->logger->debug('...Finished writing export'); } $this->state_holder->set($state); @@ -111,7 +111,7 @@ private function initExportState( ); $this->logger->debug(sprintf( - "Export state created for component %s with release %s, type %s, class %s, object ids %s, option %s", + 'Export state created for component %s with release %s, type %s, class %s, object ids %s, option %s', $target->getComponent(), $target->getTargetRelease(), $target->getType(), diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Builder.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Builder.php index 07cf40ee86a4..724f5cd01b88 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Builder.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Builder.php @@ -100,10 +100,7 @@ public function withAdditionalPipes(array $prepend = [], array $append = []): se public function create(): TransformationsContract { $pipeline = new Pipeline(); - $object = new Transformations( - $this->dic->refinery(), - $pipeline - ); + $object = new Transformations($this->dic->refinery(), $pipeline); foreach ($this->prepend_pipes as $pipe) { $pipeline->pipe($pipe); @@ -180,8 +177,10 @@ private function resolveVersionKey(string $requested_version, array $available_v $best_concrete = null; foreach (array_keys($available_versions) as $version) { - if ($version === NormalizerArtifactObjective::DEFAULT_KEY - || $this->isWildcardVersion($version)) { + if ( + $version === NormalizerArtifactObjective::DEFAULT_KEY + || $this->isWildcardVersion($version) + ) { continue; } @@ -190,8 +189,10 @@ private function resolveVersionKey(string $requested_version, array $available_v continue; } - if (version_compare($version, $requested_version, '<=') - && ($best_concrete === null || version_compare($version, $best_concrete, '>'))) { + if ( + version_compare($version, $requested_version, '<=') + && ($best_concrete === null || version_compare($version, $best_concrete, '>')) + ) { $best_concrete = $version; } } @@ -272,7 +273,10 @@ private function resolveConstructorArgument( if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) { $type_name = $type->getName(); - if ($type_name === TransformationsContract::class || in_array(TransformationsContract::class, class_implements($type_name))) { + if ( + $type_name === TransformationsContract::class + || in_array(TransformationsContract::class, class_implements($type_name)) + ) { return $transformations; } @@ -283,7 +287,6 @@ private function resolveConstructorArgument( } } - $name = $parameter->getName(); - throw new RuntimeException("Unable to resolve constructor parameter \${$name} for class {$class_name}."); + throw new RuntimeException("Unable to resolve constructor parameter \${$parameter->getName()} for class {$class_name}."); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportSessionRepository.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportSessionRepository.php index f7eda4318390..ed18fdc70859 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportSessionRepository.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportSessionRepository.php @@ -20,10 +20,8 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Importing; -use ilSession; - /** - * Persists the import stage index and context across HTTP requests using `ilSession`. Each instance is scoped by a + * Persists the import stage index and context across HTTP requests using `\ilSession`. Each instance is scoped by a * namespace string so that multiple concurrent import workflows do not interfere with each other. */ class ImportSessionRepository @@ -43,23 +41,19 @@ public function __construct(string $namespace) public function getCurrentStageIndex(): int { - if (ilSession::has($this->key_index)) { - return (int) ilSession::get($this->key_index); - } - - return 0; + return \ilSession::has($this->key_index) ? (int) \ilSession::get($this->key_index) : 0; } public function setCurrentStageIndex(int $index): void { - ilSession::set($this->key_index, $index); + \ilSession::set($this->key_index, $index); } public function getContext(): ImportContext { - if (ilSession::has($this->key_context)) { + if (\ilSession::has($this->key_context)) { return unserialize( - ilSession::get($this->key_context), + \ilSession::get($this->key_context), ['allowed_classes' => [ImportContext::class]] ); } @@ -69,16 +63,12 @@ public function getContext(): ImportContext public function setContext(ImportContext $context): void { - ilSession::set($this->key_context, serialize($context)); + \ilSession::set($this->key_context, serialize($context)); } public function clear(): void { - if (ilSession::has($this->key_index)) { - ilSession::clear($this->key_index); - } - if (ilSession::has($this->key_context)) { - ilSession::clear($this->key_context); - } + \ilSession::clear($this->key_index); + \ilSession::clear($this->key_context); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php index 6aa50d897985..d17f3d0e5c11 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportStageRunner.php @@ -53,8 +53,7 @@ public function run(): StageResult return StageResult::complete($context); } - $stage = $this->stages[$index]; - $result = $stage->process($context); + $result = $this->stages[$index]->process($context); switch ($result->type) { case StageResultType::ADVANCE: @@ -104,21 +103,17 @@ public function buildWorkflow(UIFactory $ui, string $title): Linear ); if ($i < $active_index) { - $step = $step->withStatus(Step::SUCCESSFULLY) - ->withAvailability(Step::NOT_ANYMORE); + $step = $step->withStatus(Step::SUCCESSFULLY)->withAvailability(Step::NOT_ANYMORE); } elseif ($i === $active_index) { - $step = $step->withStatus(Step::IN_PROGRESS) - ->withAvailability(Step::AVAILABLE); + $step = $step->withStatus(Step::IN_PROGRESS)->withAvailability(Step::AVAILABLE); } else { - $step = $step->withStatus(Step::NOT_STARTED) - ->withAvailability(Step::NOT_AVAILABLE); + $step = $step->withStatus(Step::NOT_STARTED)->withAvailability(Step::NOT_AVAILABLE); } $steps[] = $step; } - return $ui->listing()->workflow()->linear($title, $steps) - ->withActive($active_index); + return $ui->listing()->workflow()->linear($title, $steps)->withActive($active_index); } /** diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/Normalizes.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/Normalizes.php index a3177b64e683..e53a0e3358ea 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/Normalizes.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/Normalizes.php @@ -30,15 +30,13 @@ #[Attribute(Attribute::TARGET_CLASS)] final readonly class Normalizes { + /** @var list */ + public array $types; + /** * @param class-string ...$types */ - public function __construct( - string ...$types - ) { + public function __construct(string ...$types) { $this->types = $types; } - - /** @var list */ - public array $types; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Envelopes/Id.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Envelopes/Id.php index 1fb2a36186f0..190a54a83da2 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Envelopes/Id.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Envelopes/Id.php @@ -65,13 +65,13 @@ public function toArray(Transformations $tt): array 'type' => get_class($this->id), 'object' => $this->object, ]; - } else { - return [ - 'id' => (string) $this->id, - 'type' => gettype($this->id), - 'object' => $this->object, - ]; } + + return [ + 'id' => (string) $this->id, + 'type' => gettype($this->id), + 'object' => $this->object, + ]; } /** @@ -84,16 +84,17 @@ public static function fromArray(array $value, Transformations $tt): static if (class_exists($type)) { return new Id($tt->denormalize($raw_id, $type), $value['object']); - } else { - $id = match($type) { - 'integer' => (int) $raw_id, - 'string' => (string) $raw_id, - 'float' => (float) $raw_id, - 'bool' => (bool) $raw_id, - 'null' => null, - default => throw new NormalizingException("Invalid type for id: {$type}") - }; - return new Id($id, $value['object']); } + + $id = match ($type) { + 'integer' => (int) $raw_id, + 'string' => (string) $raw_id, + 'float' => (float) $raw_id, + 'bool' => (bool) $raw_id, + 'null' => null, + default => throw new NormalizingException("Invalid type for id: {$type}") + }; + + return new Id($id, $value['object']); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/DateTimeNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/DateTimeNormalizer.php index e2fb07c8d584..ffec74f76c48 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/DateTimeNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/DateTimeNormalizer.php @@ -49,7 +49,7 @@ public function normalize($value): array|float|bool|int|string|null */ public function denormalize(array|float|bool|int|string|null $value, string $type): DateTime|DateTimeImmutable { - return match($type) { + return match ($type) { DateTimeImmutable::class => DateTimeImmutable::createFromFormat(DATE_ATOM, $value), DateTime::class => DateTime::createFromFormat(DATE_ATOM, $value), default => throw new NormalizingException("Invalid type for datetime: {$type}") diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php index 727963aa1378..497b3d022ceb 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php @@ -94,10 +94,16 @@ private function normalizeProperty(Property $property): array|bool|int|string|nu 'long_description' => $property->getLongDescription(), ]; } + if ($property instanceof Online) { return $property->getIsOnline(); } - if ($property instanceof TitleAndIconVisibility || $property instanceof HeaderActionVisibility || $property instanceof InfoTabVisibility) { + + if ( + $property instanceof TitleAndIconVisibility + || $property instanceof HeaderActionVisibility + || $property instanceof InfoTabVisibility + ) { return $property->getVisibility(); } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Legacy11UUIDNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Legacy11UUIDNormalizer.php index 01c61026d6ac..2d6374b7328a 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Legacy11UUIDNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Legacy11UUIDNormalizer.php @@ -50,8 +50,6 @@ public function normalize($value): array|float|bool|int|string|null */ public function denormalize(array|float|bool|int|string|null $value, string $type): Uuid { - $value = (string) $value; - $value = str_replace('_', '-', $value); - return parent::denormalize($value, $type); + return parent::denormalize(str_replace('_', '-', (string) $value), $type); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Registry.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Registry.php index e42296c44d7c..6987ffd78df4 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Registry.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Registry.php @@ -44,7 +44,7 @@ class Registry */ public function registerNormalizer(string $type, callable $normalizer): void { - if (isset($this->type_map[$type])) { + if ($this->hasNormalizer($type)) { throw new NormalizingException("Type {$type} is already registered"); } $this->type_map[$type] = $normalizer; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php index 2e8452a92dbb..d0a131617c05 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php @@ -21,10 +21,10 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer; use ILIAS\DI\Container; -use ILIAS\ResourceStorage\Resource\ResourceType; -use ILIAS\ResourceStorage\Resource\StorableResource; use ILIAS\ResourceStorage\Identification\ResourceIdentification; use ILIAS\ResourceStorage\Resource\Repository\ResourceRepository; +use ILIAS\ResourceStorage\Resource\ResourceType; +use ILIAS\ResourceStorage\Resource\StorableResource; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; @@ -119,10 +119,10 @@ private function denormalizeResource(array $value): StorableResource throw new NormalizingException('Invalid storable resource', $value); } - $id = new ResourceIdentification($value['id']); - $type = ResourceType::from($value['resource_type']); - - return $this->resource_repository->blank($id, $type); + return $this->resource_repository->blank( + new ResourceIdentification($value['id']), + ResourceType::from($value['resource_type']) + ); } /** @@ -130,9 +130,7 @@ private function denormalizeResource(array $value): StorableResource */ public static function isResourceIdentification(mixed $value): bool { - return is_array($value) - && isset($value[self::KEY_TYPE]) - && $value[self::KEY_TYPE] === self::TYPE_RID; + return ($value[self::KEY_TYPE] ?? false) === self::TYPE_RID; } /** @@ -140,8 +138,6 @@ public static function isResourceIdentification(mixed $value): bool */ public static function isStorableResource(mixed $value): bool { - return is_array($value) - && isset($value[self::KEY_TYPE]) - && $value[self::KEY_TYPE] === self::TYPE_RESOURCE; + return ($value[self::KEY_TYPE] ?? false) === self::TYPE_RESOURCE; } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/TransformationNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/TransformationNormalizer.php index 1f55670c6238..3d79aaff8653 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/TransformationNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/TransformationNormalizer.php @@ -58,6 +58,6 @@ public function normalize($value): array|float|bool|int|string|null */ public function denormalize(array|float|bool|int|string|null $value, string $type): Transformation { - return $this->refinery->custom()->transformation(fn() => $value); + return $this->refinery->custom()->transformation(static fn() => $value); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php index 87529d81b575..9dba92bb2a28 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php @@ -45,9 +45,10 @@ class CollectResources implements Pipe private array $import_mapping = []; public function __construct( - private readonly IRSS $irss, + private readonly IRSS $irss, private readonly LoggerInterface $log - ) { + ) + { } /** @@ -79,9 +80,7 @@ public function handle(mixed $passable, \Closure $next): mixed } if ($passable instanceof DenormalizeCarry && $passable->expected === ResourceIdentification::class) { - $passable->setResult( - $this->replaceRid($passable->result()) - ); + $passable->setResult($this->replaceRid($passable->result())); } return $next($passable); @@ -89,20 +88,18 @@ public function handle(mixed $passable, \Closure $next): mixed private function handleNormalization(ResourceIdentification $rid): void { - $resource = $this->irss->manage()->getResource($rid); - - $this->resources[$rid->serialize()] = $resource; + $this->resources[$rid->serialize()] = $this->irss->manage()->getResource($rid); } private function replaceRid(ResourceIdentification $rid): ResourceIdentification { $id = $rid->serialize(); - if (isset($this->import_mapping[$id])) { - $this->log->debug("Replaced resource id {$id} with {$this->import_mapping[$id]->serialize()}"); - return $this->import_mapping[$id]; - } else { + if (!isset($this->import_mapping[$id])) { $this->log->warning("Unresolved resource id {$id}"); return $rid; } + + $this->log->debug("Replaced resource id {$id} with {$this->import_mapping[$id]->serialize()}"); + return $this->import_mapping[$id]; } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizeCarry.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizeCarry.php index 42f1f9a1a2fb..b3987aa12cca 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizeCarry.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizeCarry.php @@ -42,6 +42,7 @@ public function result(): mixed $normalized_type = get_debug_type($this->normalized); throw new NormalizingException("Unsupported value, expected: {$expected_type}, got: {$normalized_type}"); } + return $this->result; } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Transformations.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Transformations.php index 0cc194358ad5..3c49d26c6d61 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Transformations.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Transformations.php @@ -47,7 +47,7 @@ public function normalize(mixed $value, array $context = []): array|float|bool|i } return $this->pipeline->send(new NormalizeCarry($this, $value, $context)) - ->then(fn(NormalizeCarry $carry) => $carry->result()); + ->then(static fn(NormalizeCarry $carry): mixed => $carry->result()); } /** @@ -60,7 +60,7 @@ public function denormalize(array|float|bool|int|string|null $normalized, string } return $this->pipeline->send(new DenormalizeCarry($this, $normalized, $expected)) - ->then(fn(DenormalizeCarry $carry) => $carry->result()); + ->then(static fn(DenormalizeCarry $carry): mixed => $carry->result()); } /* diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Pipeline.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Pipeline.php index fe32b10d38eb..ed79290c6917 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Pipeline.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Pipeline.php @@ -29,7 +29,7 @@ * @template TPassable * @implements PipelineContract * - * @phpstan-type PipeParam \Closure(TPassable, PipeParam)|Pipe: TPassable + * @phpstan-type PipeParam Closure(TPassable, PipeParam)|Pipe: TPassable */ class Pipeline implements PipelineContract { @@ -123,7 +123,7 @@ public function then(Closure $destination): mixed */ public function thenReturn(): mixed { - return $this->then(fn($passable) => $passable); + return $this->then(fn(mixed $passable): mixed => $passable); } /** @@ -146,13 +146,13 @@ public function pipes(): array /** * Get the final piece of the Closure onion. */ - protected function prepareDestination(Closure $destination) + protected function prepareDestination(Closure $destination): Closure { - return function ($passable) use ($destination) { + return function (mixed $passable) use ($destination): mixed { try { return $destination($passable); } catch (Throwable $e) { - return $this->handleException($passable, $e); + $this->handleException($passable, $e); } }; } @@ -160,13 +160,13 @@ protected function prepareDestination(Closure $destination) /** * Get a Closure that represents a slice of the application onion. */ - protected function carry() + protected function carry(): Closure { - return fn($stack, $pipe) => function ($passable) use ($stack, $pipe) { + return fn($stack, $pipe) => function ($passable) use ($stack, $pipe): mixed { try { return $this->executePipe($pipe, $passable, $stack); } catch (Throwable $e) { - return $this->handleException($passable, $e); + $this->handleException($passable, $e); } }; } @@ -195,6 +195,8 @@ protected function executePipe(Closure|Pipe $pipe, mixed $passable, Closure $nex */ protected function handleException(mixed $passable, Throwable $e): void { + // Maybe we want to handle specific exceptions differently in the future. + // For now, we'll just re-throw the exception.' throw $e; } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemoryDeserializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemoryDeserializer.php index 3ea8e3904b3b..9313ab12a262 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemoryDeserializer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemoryDeserializer.php @@ -59,7 +59,7 @@ public function process(): void foreach ($this->decoded as $key => $value) { if (is_array($value)) { $head = $value[array_key_first($value)]; - $value = (!array_is_list($head)) ? [$head] : $head; + $value = array_is_list($head) ? $head : [$head]; } if (isset($this->handler[$key])) { diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemorySerializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemorySerializer.php index dcf935c8dfd5..92911b306b1f 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemorySerializer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemorySerializer.php @@ -38,9 +38,7 @@ class JSONMemorySerializer implements Serializer public function open(string $path): static { $clone = clone $this; - $clone->stack = [ - ['name' => '', 'data' => []], - ]; + $clone->stack = [['name' => '', 'data' => []]]; return $clone; } @@ -58,13 +56,14 @@ public function startGroup(string $name): void public function endGroup(string $name): void { $frame = array_pop($this->stack); + if($frame === null) { + throw new \LogicException("Called endGroup() without matching startGroup()"); + } + if ($frame['name'] !== $name) { - throw new \LogicException( - "Group name mismatch: expected end of '{$frame['name']}', got '{$name}'" - ); + throw new \LogicException("Group name mismatch: expected end of '{$frame['name']}', got '{$name}'"); } - $top = &$this->stack[array_key_last($this->stack)]; - $top['data'][$name] = $frame['data']; + $this->stack[array_key_last($this->stack)]['data'][$name] = $frame['data']; } /** @@ -83,16 +82,23 @@ public function group(string $name, callable $callback): void public function append(string $name, array $data): void { $top = &$this->stack[array_key_last($this->stack)]; - if (array_key_exists($name, $top['data'])) { - $existing = $top['data'][$name]; - if (is_array($existing) && array_is_list($existing)) { - $top['data'][$name][] = $data; - } else { - $top['data'][$name] = [$existing, $data]; - } - } else { + + // First entry for this name: store it directly. + if (!array_key_exists($name, $top['data'])) { $top['data'][$name] = $data; + return; } + + $existing = $top['data'][$name]; + + // Multiple entries already exist: append to the list. + if (is_array($existing) && array_is_list($existing)) { + $top['data'][$name][] = $data; + return; + } + + // Second entry: convert the existing single entry into a list. + $top['data'][$name] = [$existing, $data]; } /** @@ -105,9 +111,7 @@ public function write(): string } $root = $this->stack[0]['data']; $json = json_encode($root, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - $this->stack = [ - ['name' => '', 'data' => []], - ]; + $this->stack = [['name' => '', 'data' => []]]; return $json; } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php index 8df8f8e8b15c..e331a7232ac9 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php @@ -82,9 +82,7 @@ public function startGroup(string $name): void public function endGroup(string $name): void { if ($this->current_group !== $name) { - throw new \LogicException( - "Group name mismatch: expected end of '{$this->current_group}', got '{$name}'" - ); + throw new \LogicException("Group name mismatch: expected end of '{$this->current_group}', got '{$name}'"); } $this->current_group = ''; @@ -107,7 +105,7 @@ public function group(string $name, callable $callback): void public function append(string $name, array $data): void { $this->writer->startElement($this->formatName($name)); - if (count($data) === 0) { + if ($data === []) { $this->writer->writeAttribute('type', 'empty-array'); } @@ -157,7 +155,7 @@ private function appendRecursive(array $data): void $this->writer->writeRaw($value); } else { - if (count($value) === 0) { + if ($value === []) { $this->writer->writeAttribute('type', 'empty-array'); } $this->appendRecursive($value); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLDeserializerTrait.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLDeserializerTrait.php index e4ab81b7bff1..58c814520177 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLDeserializerTrait.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLDeserializerTrait.php @@ -42,8 +42,7 @@ private function processReader(\XMLReader $reader): void continue; } - $group_data = $this->readGroup($reader); - $this->handler[$node_name]($group_data); + $this->handler[$node_name]($this->readGroup($reader)); } $reader->close(); @@ -89,10 +88,7 @@ private function readElementValue(\XMLReader $reader): mixed $is_marked_empty_array = $this->isMarkedEmptyArray($reader); if ($reader->isEmptyElement) { - if ($is_marked_empty_array) { - return []; - } - return ''; + return $is_marked_empty_array ? [] : ''; } $element_depth = $reader->depth; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLFileDeserializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLFileDeserializer.php index 90e7ad6e52e2..c91cb66c19f0 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLFileDeserializer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLFileDeserializer.php @@ -38,9 +38,7 @@ class XMLFileDeserializer implements Deserializer public function open(string $path): static { if (!is_file($path) || !is_readable($path)) { - throw new \InvalidArgumentException( - "The file '{$path}' does not exist or is not readable." - ); + throw new \InvalidArgumentException("The file '{$path}' does not exist or is not readable."); } $clone = clone $this; @@ -54,17 +52,13 @@ public function open(string $path): static public function process(): void { if ($this->file_path === '') { - throw new \RuntimeException( - 'No file has been opened. Call open() before process().' - ); + throw new \RuntimeException('No file has been opened. Call open() before process().'); } $reader = new \XMLReader(); if (!$reader->open($this->file_path, null, LIBXML_NONET)) { - throw new \RuntimeException( - "Unable to open XML file '{$this->file_path}'." - ); + throw new \RuntimeException("Unable to open XML file '{$this->file_path}'."); } $this->processReader($reader); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLMemoryDeserializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLMemoryDeserializer.php index 395d0d2a5b87..ab40dea5474e 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLMemoryDeserializer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLMemoryDeserializer.php @@ -58,7 +58,8 @@ public function process(): void private function prepareXmlInput(string $xml): string { - $xml = preg_replace('/^\s*<\?xml[^>]*\?>\s*/i', '', trim($xml)) ?? trim($xml); + $xml = trim($xml); + $xml = preg_replace('/^\s*<\?xml[^>]*\?>\s*/i', '', $xml) ?? $xml; return "{$xml}"; } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Setup/NormalizerArtifactObjective.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Setup/NormalizerArtifactObjective.php index 22c220a0ca28..8260d1cebbaa 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Setup/NormalizerArtifactObjective.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Setup/NormalizerArtifactObjective.php @@ -27,6 +27,7 @@ use ILIAS\Setup\ImplementationOfInterfaceFinder; use ILIAS\Setup\Artifact\ArrayArtifact; use ILIAS\Setup\Artifact; +use Generator; use ReflectionClass; /** @@ -44,7 +45,7 @@ class NormalizerArtifactObjective extends BuildArtifactObjective */ public function getArtifactName(): string { - return "questions_normalizers"; + return 'questions_normalizers'; } /** @@ -56,27 +57,34 @@ public function build(): Artifact $type_map = []; foreach ($finder->getMatchingClassNames(Normalizer::class) as $class_name) { - $ref = new ReflectionClass($class_name); - - $attrs = $ref->getAttributes(Normalizes::class); - foreach ($attrs as $attr) { - $instance = $attr->newInstance(); - foreach ($instance->types as $type) { - $type_map[$type][self::DEFAULT_KEY] = $class_name; - } - } - - $attrs = $ref->getAttributes(NormalizesLegacy::class); - foreach ($attrs as $attr) { - $instance = $attr->newInstance(); - foreach ($instance->types as $type) { - foreach ($instance->versions as $version) { - $type_map[$type][$version] = $class_name; - } + foreach ($this->getDeclarations(new ReflectionClass($class_name)) as [$types, $versions]) { + $entry = array_fill_keys($versions, $class_name); + foreach ($types as $type) { + // union instead of array_merge: numeric version keys would be reindexed + $type_map[$type] = $entry + ($type_map[$type] ?? []); } } } return new ArrayArtifact($type_map); } + + /** + * Both attributes declare the same thing: every combination of type and version is served by the given + * class. Normalize is the special case where the only version is the default one. + * + * @param ReflectionClass $ref + * @return Generator, list}> + */ + private function getDeclarations(ReflectionClass $ref): Generator + { + if (($attribute = $ref->getAttributes(Normalizes::class)[0] ?? null) !== null) { + yield [$attribute->newInstance()->types, [self::DEFAULT_KEY]]; + } + + if (($attribute = $ref->getAttributes(NormalizesLegacy::class)[0] ?? null) !== null) { + $legacy = $attribute->newInstance(); + yield [$legacy->types, $legacy->versions]; + } + } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php index f13f3548247c..0f9869c21868 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php @@ -77,8 +77,7 @@ public function process(ImportContext $context): StageResult private function removeDirectory(string $path): void { - $files = array_diff(scandir($path), ['.', '..']); - foreach ($files as $file) { + foreach (array_diff(scandir($path), ['.', '..']) as $file) { if (is_dir("$path/$file")) { $this->removeDirectory("$path/$file"); } else { diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php index cb331f69b8d6..ff163bf3c26a 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php @@ -58,7 +58,7 @@ public function process(ImportContext $context): StageResult $import_base_dir = $context->get(UploadValidationStage::IMPORT_BASE_DIR); $import_name = basename($import_base_dir); - $xml_file = $import_base_dir . DIRECTORY_SEPARATOR . $import_name . '.xml'; + $xml_file = $import_base_dir . DIRECTORY_SEPARATOR . "{$import_name}.xml"; $qti_file = $import_base_dir . DIRECTORY_SEPARATOR . str_replace(['_qpl_', '_tst_'], '_qti_', $import_name) . '.xml'; if (!file_exists($qti_file) || !file_exists($xml_file)) { @@ -68,8 +68,7 @@ public function process(ImportContext $context): StageResult $this->log->info("Detected legacy import files for {$import_name}"); return StageResult::advance( - $context->with(self::LEGACY_QTI_FILE, $qti_file) - ->with(self::LEGACY_XML_FILE, $xml_file) + $context->with(self::LEGACY_QTI_FILE, $qti_file)->with(self::LEGACY_XML_FILE, $xml_file) ); } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php index 440589ed058a..e03ec66fe422 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php @@ -20,19 +20,14 @@ namespace ILIAS\TestQuestionPool\ExportImport\Import; -use ilCtrl; -use ilDBInterface; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\ReferenceId; use ILIAS\Data\UUID\Factory; -use ILIAS\Language\Language; use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Deserializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\IdMappingPipe; -use ILIAS\TestQuestionPool\ExportImport\Import\QuestionSelectionStage; -use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; use ilImportMapping; use ilObjQuestionPool; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php index 7bffa9f3a7b1..fbad7b3a7ca8 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php @@ -98,7 +98,7 @@ public function process(ImportContext $context): StageResult } if (!$context->has(UploadValidationStage::COMPONENT_IMPORT_FILE)) { - $this->log->error("No component import file found in context"); + $this->log->error('No component import file found in context'); return StageResult::error($context, $this->lng->txt('qpl_import_file_not_found')); } @@ -107,7 +107,7 @@ public function process(ImportContext $context): StageResult : $this->readQuestions($context); if ($options === []) { - $this->log->error("No questions found in import file"); + $this->log->error('No questions found in import file'); return StageResult::error($context, $this->lng->txt('qpl_import_no_items')); } @@ -141,17 +141,20 @@ private function readQuestions(ImportContext $context): array $context->get(UploadValidationStage::COMPONENT_IMPORT_FILE) ); - $deserializer->addHandler('questions', function (array $questions) use (&$options): void { - foreach ($questions as $question) { - if (!isset($question['title']) || !isset($question['type'])) { - continue; + $deserializer->addHandler( + 'questions', + function (array $questions) use (&$options): void { + foreach ($questions as $question) { + if (!isset($question['title']) || !isset($question['type'])) { + continue; + } + + $raw_id = $question['id']; + $id = is_array($raw_id) ? (string) ($raw_id['id'] ?? '') : (string) $raw_id; + $options[$id] = "{$question['title']} ({$this->getLabelForQuestionType($question['type'])})"; } - - $raw_id = $question['id']; - $id = is_array($raw_id) ? (string) ($raw_id['id'] ?? '') : (string) $raw_id; - $options[$id] = "{$question['title']} ({$this->getLabelForQuestionType($question['type'])})"; } - }); + ); $deserializer->process(); $count = count($options); @@ -191,12 +194,10 @@ private function buildSelectQuestionsForm(array $options): Form $options )->withValue(array_keys($options)); - $form = $this->ui_factory->input()->container()->form()->standard( + return $this->ui_factory->input()->container()->form()->standard( $this->form_action, ['selected_questions' => $input] )->withSubmitLabel($this->lng->txt('import')); - - return $form; } private function getLabelForQuestionType(string $type): string @@ -218,9 +219,9 @@ private function getLabelForQuestionType(string $type): string private function getLabelForPluginQuestionTypes(string $type): string { - foreach ($this->component_factory->getActivePluginsInSlot('qst') as $pl) { - if ($pl->getQuestionType() === $type) { - return $pl->getQuestionTypeTranslation(); + foreach ($this->component_factory->getActivePluginsInSlot('qst') as $plugin) { + if ($plugin->getQuestionType() === $type) { + return $plugin->getQuestionTypeTranslation(); } } return $type; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php index 238daf17c287..bc942f5a3edd 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php @@ -131,7 +131,7 @@ public function importQuestionImages( $this->log->debug("Imported question image: {$source_path} -> {$image_path}"); $thumbnail = $this->generateThumbnail($input_stream); - if (!$thumbnail) { + if (!$thumbnail instanceof FileStream) { continue; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php index 47ccd6d6831e..6aed319e2474 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php @@ -60,7 +60,7 @@ public function import( $result = ['failed' => [], 'success' => []]; foreach ($normalized_assignments as $item) { - // ParentObjID and QuestionID will be replaced by the mapping pipe + // The mapping pipe replaces ParentObjID and QuestionID $assignment = $transformations->denormalize($item, ilAssQuestionSkillAssignment::class); $skill_data = $this->getSkillIdMapping( diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php index 4a26199c8a5c..585263caf527 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php @@ -89,7 +89,7 @@ public function process(ImportContext $context): StageResult $manifest = new ilManifestParser($import_base_dir . DIRECTORY_SEPARATOR . 'manifest.xml'); $export_file = array_find( $manifest->getExportFiles(), - fn($file): bool => $file['component'] === $this->component + fn(array $file): bool => $file['component'] === $this->component ); if ($export_file === null) { @@ -102,9 +102,10 @@ public function process(ImportContext $context): StageResult $this->log->info("Found valid export file from installation: {$manifest->getInstallId()}"); return StageResult::advance( - $context->with(self::COMPONENT_IMPORT_FILE, $component_import_file) - ->with(self::IMPORT_BASE_DIR, $import_base_dir) - ->with(self::INSTALL_ID, $manifest->getInstallId()) + $context + ->with(self::COMPONENT_IMPORT_FILE, $component_import_file) + ->with(self::IMPORT_BASE_DIR, $import_base_dir) + ->with(self::INSTALL_ID, $manifest->getInstallId()) ); } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/LoggingProvider.php b/components/ILIAS/TestQuestionPool/src/ExportImport/LoggingProvider.php index e0b39c6bd0dc..524a8e0f023d 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/LoggingProvider.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/LoggingProvider.php @@ -20,7 +20,7 @@ namespace ILIAS\TestQuestionPool\ExportImport; -use ilLoggerFactory; +use ILIAS\Logging\Logger\LegacyInitiator; use Psr\Log\LoggerInterface; /** @@ -32,8 +32,9 @@ class LoggingProvider public function getLogger(?string $component_id = null): LoggerInterface { - $component_id = $component_id ?? self::COMPONENT_ID; - return ilLoggerFactory::getLogger($component_id)->getLogger(); + return LegacyInitiator::getInstance() + ->loggerFactory() + ->getLazy($component_id ?? self::COMPONENT_ID); } public function __invoke() diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php index b32beb3e1d88..41777693b1c0 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php @@ -78,14 +78,14 @@ private function handleNormalization(QuestionImage $envelope): void ? $this->question_files->buildImagePath($envelope->getQuestionId(), $pool_id) : $this->question_files->buildSolutionPath($envelope->getQuestionId(), $pool_id); - $source_path = $base_dir . $envelope->getFilename(); + $source_path = "{$base_dir}{$envelope->getFilename()}"; // Generate a unique ID for the image and set it on the envelope and the relative target path $id = $this->uuid_factory->uuid4(); $envelope->setId($id->toString()); $extension = pathinfo($envelope->getFilename(), PATHINFO_EXTENSION); - $target_path = $id->toString() . '.' . $extension; + $target_path = "{$id->toString()}.{$extension}"; $this->files[] = ['from' => $source_path, 'to' => $target_path]; } @@ -97,8 +97,7 @@ private function handleDenormalization(DenormalizeCarry $passable): void throw new NormalizingException('Expected question image envelope, got ' . get_debug_type($envelope)); } - $extension = pathinfo($envelope->getFilename(), PATHINFO_EXTENSION); - $path = $envelope->getId() . '.' . $extension; + $path = "{$envelope->getId()}." . pathinfo($envelope->getFilename(), PATHINFO_EXTENSION); $this->envelopes[$path] = $envelope; } diff --git a/lang/ilias_de.lang b/lang/ilias_de.lang index 3a95e2343cab..f2373f3bef04 100644 --- a/lang/ilias_de.lang +++ b/lang/ilias_de.lang @@ -1067,7 +1067,7 @@ assessment#:#qpl_imagemap_preview_missing#:#ILIAS konnte keine Vorschaudatei mit assessment#:#qpl_import_create_new_qpl#:#In einen neuen Fragenpool importieren assessment#:#qpl_import_no_items#:#Fehler: die Importdatei enthält keine Fragen! assessment#:#qpl_import_non_ilias_files#:#Fehler: Die Importdatei enthält QTI-Dateien, die nicht von einem ILIAS-System erstellt wurden. Bitte kontaktieren Sie das ILIAS-Team, um einen Importfilter für Ihr QTI-Dateiformat zu bekommen. -assessment#:#qpl_import_step_persist#:#Fragenpool in ILIAS importieren +assessment#:#qpl_import_step_persist#:#Fragenpool importieren assessment#:#qpl_import_step_select#:#Fragen auswählen assessment#:#qpl_import_verify_found_questions#:#ILIAS hat die folgenden Fragen in der Importdatei gefunden. Bitte wählen Sie die Fragen aus, die Sie importieren wollen. assessment#:#qpl_lac_desc_brackets#:#Klammerung diff --git a/lang/ilias_en.lang b/lang/ilias_en.lang index 4af32130fd4b..c38826f49082 100644 --- a/lang/ilias_en.lang +++ b/lang/ilias_en.lang @@ -1067,7 +1067,7 @@ assessment#:#qpl_imagemap_preview_missing#:#ILIAS could not create the temporary assessment#:#qpl_import_create_new_qpl#:#Import the questions in a new question pool assessment#:#qpl_import_no_items#:#Error: The import file contains no questions! assessment#:#qpl_import_non_ilias_files#:#Error: The import file contains QTI files which are not created by an ILIAS system. Please contact the ILIAS team to get in import filter for your QTI file format. -assessment#:#qpl_import_step_persist#:#Import question pool into ILIAS +assessment#:#qpl_import_step_persist#:#Import question pool assessment#:#qpl_import_step_select#:#Select questions to import assessment#:#qpl_import_verify_found_questions#:#ILIAS found the following questions in the import file. Please select the questions you want to import. assessment#:#qpl_lac_desc_brackets#:#Brackets From e99c7f427ed4a938a9b843d1eb66e58fa242d706 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Mon, 24 Aug 2026 08:45:43 +0200 Subject: [PATCH 49/66] refactor: improve architecture and small code smells after internal review --- .../Test/src/ExportImport/TestExporter.php | 20 ++--- .../classes/class.ilObjQuestionPoolGUI.php | 2 +- .../Export/QuestionPoolExporter.php | 14 ++-- .../Foundation/Bridge/ExportState.php | 7 +- .../Contracts/ExportDependencies.php | 81 +++++++++++++++++++ .../Foundation/Contracts/Exporter.php | 14 ++-- .../Foundation/Importing/ImportContext.php | 2 +- 7 files changed, 108 insertions(+), 32 deletions(-) create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ExportDependencies.php diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index 0b41ce7575e9..cb3b3497e4c6 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -35,9 +35,9 @@ use ILIAS\Test\Participants\ParticipantRepository; use ILIAS\Test\Questions\Properties\Repository as QuestionsRepository; use ILIAS\Test\Results\Data\Repository as ResultsRepository; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportState; use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportStep; use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ExportDependencies; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Exporter; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Serializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; @@ -69,7 +69,7 @@ public function __construct( /** * @inheritDoc */ - public function prepare(ExportState $state): void + public function prepare(ExportDependencies $state): void { $state->logger()->info('Preparing test export (1/3)...'); $state->assertStep(ExportStep::INIT); @@ -112,7 +112,7 @@ public function prepare(ExportState $state): void $state->logger()->info('...Finished preparing test export (1/3)'); } - private function extractObjectId(ExportState $state): ?ObjectId + private function extractObjectId(ExportDependencies $state): ?ObjectId { $target_ids = $state->target()->getObjectIds(); @@ -133,7 +133,7 @@ private function extractObjectId(ExportState $state): ?ObjectId /** * @inheritDoc */ - public function process(ExportState $state): void + public function process(ExportDependencies $state): void { $state->logger()->info('Processing test export (2/3)...'); $state->assertStep(ExportStep::PREPARE); @@ -208,7 +208,7 @@ public function process(ExportState $state): void $state->logger()->info('...Finished processing test export (2/3)'); } - private function processResults(ExportState $state): void + private function processResults(ExportDependencies $state): void { $state->serializer()->group( 'participants', @@ -231,7 +231,7 @@ private function processResults(ExportState $state): void /** * @inheritDoc */ - public function write(ExportState $state): void + public function write(ExportDependencies $state): void { $state->logger()->info('Writing test export (3/3)...'); $state->assertStep(ExportStep::PROCESS); @@ -280,7 +280,7 @@ private function exportObject( TestCollector $collector, Transformations $transformations, Serializer $serializer, - ExportState $state + ExportDependencies $state ): void { $serializer->append('object', $transformations->normalize($collector->getObject())); @@ -298,7 +298,7 @@ private function exportSettings( TestCollector $collector, Transformations $transformations, Serializer $serializer, - ExportState $state + ExportDependencies $state ): void { $test = $collector->getObject(); $main_settings = $test->getMainSettings(); @@ -319,7 +319,7 @@ private function exportQuestions( TestCollector $collector, Transformations $transformations, Serializer $serializer, - ExportState $state + ExportDependencies $state ): void { $question_properties = $collector->getTestQuestionProperties(); @@ -418,7 +418,7 @@ private function exportAdditionalWorkingTimes( private function writeMappings( TestCollector $collector, Transformations $transformations, - ExportState $state + ExportDependencies $state ): void { $serializer = new SimpleXMLSerializer()->open('memory'); $serializer->createDocument('Test Export Mappings'); diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php index 757ac78ece50..54d9c9a17e7c 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php @@ -1130,7 +1130,7 @@ public function processImportObject(): void } $runner = $this->buildImportStageRunner(); - $result = $runner->run($this->request); + $result = $runner->run(); match ($result->type) { StageResultType::INTERACT => $this->renderImportStage($runner, $result), diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php index ffff633640d8..5aa1af67461c 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php @@ -26,9 +26,9 @@ use ILIAS\Data\ObjectId; use ILIAS\Data\UUID\Factory as UUIDFactory; use ILIAS\Taxonomy\DomainService as Taxonomy; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportState; use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportStep; use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ExportDependencies; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Exporter; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Serializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; @@ -54,7 +54,7 @@ public function __construct( /** * Prepares the export by creating the transformations and the question image pipe. */ - public function prepare(ExportState $state): void + public function prepare(ExportDependencies $state): void { $state->logger()->info('Preparing question pool export (1/3)...'); $state->assertStep(ExportStep::INIT); @@ -87,7 +87,7 @@ public function prepare(ExportState $state): void * Normalizes the question pool object and its questions and writes them to the serializer. It also collects the * dependencies of the export. */ - public function process(ExportState $state): void + public function process(ExportDependencies $state): void { $state->logger()->info('Processing question pool export (2/3)...'); $state->assertStep(ExportStep::PREPARE); @@ -126,7 +126,7 @@ public function process(ExportState $state): void /** * Finalizes the export by copying the question images to the export directory and returning the export context. */ - public function write(ExportState $state): void + public function write(ExportDependencies $state): void { $state->logger()->info('Writing question pool export (3/3)...'); $state->assertStep(ExportStep::PROCESS); @@ -148,7 +148,7 @@ public function write(ExportState $state): void } - private function extractObjectId(ExportState $state): ?ObjectId + private function extractObjectId(ExportDependencies $state): ?ObjectId { $target_ids = $state->target()->getObjectIds(); @@ -170,7 +170,7 @@ private function exportObject( QuestionPoolCollector $collector, Transformations $transformations, Serializer $serializer, - ExportState $state + ExportDependencies $state ): void { $serializer->append('object', $transformations->normalize($collector->getObject())); @@ -189,7 +189,7 @@ private function exportQuestions( QuestionPoolCollector $collector, Transformations $transformations, Serializer $serializer, - ExportState $state + ExportDependencies $state ): void { foreach ($collector->getQuestionObjects() as $question) { $normalized = [ diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php index 0721f0956c4e..2bae15d3b356 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php @@ -25,16 +25,13 @@ use ILIAS\Export\ExportHandler\I\Info\Export\Path\HandlerInterface as ExportPath; use ILIAS\Export\ExportHandler\I\Target\HandlerInterface as ExportTarget; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\DataCollector; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ExportDependencies; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Serializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use Psr\Log\LoggerInterface as Logger; use RuntimeException; -/** - * The ExportState is a data transfer object that contains the state of the export process. It is used to pass the - * context between the core exporter component, the xml exporter and the exporter class steps. - */ -class ExportState +class ExportState implements ExportDependencies { private ExportStep $step = ExportStep::INIT; private ?Logger $logger = null; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ExportDependencies.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ExportDependencies.php new file mode 100644 index 000000000000..6736fca44114 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ExportDependencies.php @@ -0,0 +1,81 @@ +}> + */ + public function getDependencies(): array; + + /** + * @param array $ids + */ + public function addDependency(string $component, string $entity, array $ids): void; + + public function getContent(): string; +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Exporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Exporter.php index 0dcd56624270..5cfd1f10966c 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Exporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Exporter.php @@ -20,8 +20,6 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportState; - /** * An Exporter is responsible for orchestrating the export process. It prepares the dependencies, collects the data to * export, transforms the data and writes the data to the serializer and the file system. @@ -31,30 +29,30 @@ interface Exporter /** * Prepares the export by initializing the export components (collectors, transformations, etc.). * - * `ExportState` dependencies: + * Required dependencies: * - Target * - Config * - Logger */ - public function prepare(ExportState $state): void; + public function prepare(ExportDependencies $state): void; /** * Processes the export by collecting the data to export, transforming the data and writing the data to the * serializer. * - * `ExportState` dependencies: + * Required dependencies: * - Target * - Config * - Logger * - Serializer */ - public function process(ExportState $state): void; + public function process(ExportDependencies $state): void; /** * Finalizes the export by persisting additional data (which cannot be provided by the serializer) to the file * system. * - * `ExportState` dependencies: + * Required dependencies: * - Target * - Config * - Logger @@ -62,5 +60,5 @@ public function process(ExportState $state): void; * - Writer * - Path */ - public function write(ExportState $state): void; + public function write(ExportDependencies $state): void; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportContext.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportContext.php index 01b35fbae0b0..b609f75e2679 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportContext.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportContext.php @@ -26,7 +26,7 @@ */ class ImportContext { - /** @param array $data */ + /** @param array> $data */ public function __construct( private array $data = [], ) { From d6e6b5ea716e1ab58b7d308354261c3a9409255d Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Tue, 25 Aug 2026 09:00:11 +0200 Subject: [PATCH 50/66] refactor: add legacy test export deprecations --- components/ILIAS/Test/classes/class.ilObjTest.php | 3 +-- .../ILIAS/Test/classes/class.ilObjTestXMLParser.php | 5 +---- .../Test/classes/class.ilTestResultsImportParser.php | 3 +++ .../ILIAS/Test/classes/class.ilTestResultsToXML.php | 2 +- .../classes/class.ilTestSkillLevelThresholdExporter.php | 5 +---- .../classes/class.ilTestSkillLevelThresholdImport.php | 5 +---- .../class.ilTestSkillLevelThresholdImportFails.php | 5 +---- .../class.ilTestSkillLevelThresholdImportList.php | 5 +---- .../classes/class.ilTestSkillLevelThresholdImporter.php | 5 +---- .../Test/classes/class.ilTestSkillLevelThresholdList.php | 5 +---- .../classes/class.ilTestSkillLevelThresholdXmlParser.php | 5 +---- .../Test/src/ExportImport/ExportFixedQuestionSet.php | 5 +---- .../Test/src/ExportImport/ExportRandomQuestionSet.php | 5 +---- components/ILIAS/Test/src/ExportImport/Exportable.php | 2 ++ .../TestQuestionPool/classes/QTI/class.ilQTIParser.php | 8 +------- .../ILIAS/TestQuestionPool/classes/class.assQuestion.php | 9 +++++---- .../TestQuestionPool/classes/class.ilObjQuestionPool.php | 2 ++ .../classes/class.ilObjQuestionPoolGUI.php | 6 ++++++ .../classes/export/qti12/class.assClozeTestExport.php | 2 ++ .../classes/export/qti12/class.assErrorTextExport.php | 2 ++ .../classes/export/qti12/class.assFileUploadExport.php | 2 ++ .../export/qti12/class.assFormulaQuestionExport.php | 2 ++ .../export/qti12/class.assImagemapQuestionExport.php | 2 ++ .../classes/export/qti12/class.assKprimChoiceExport.php | 2 ++ .../classes/export/qti12/class.assLongMenuExport.php | 3 +++ .../export/qti12/class.assMatchingQuestionExport.php | 2 ++ .../export/qti12/class.assMultipleChoiceExport.php | 2 ++ .../classes/export/qti12/class.assNumericExport.php | 2 ++ .../export/qti12/class.assOrderingHorizontalExport.php | 2 ++ .../export/qti12/class.assOrderingQuestionExport.php | 2 ++ .../classes/export/qti12/class.assQuestionExport.php | 2 ++ .../classes/export/qti12/class.assSingleChoiceExport.php | 2 ++ .../classes/export/qti12/class.assTextQuestionExport.php | 2 ++ .../classes/export/qti12/class.assTextSubsetExport.php | 2 ++ .../export/qti21/class.assLongMenuExportQti21.php | 3 +++ .../classes/import/qti12/class.assClozeTestImport.php | 2 ++ .../classes/import/qti12/class.assErrorTextImport.php | 2 ++ .../classes/import/qti12/class.assFileUploadImport.php | 2 ++ .../import/qti12/class.assFormulaQuestionImport.php | 2 ++ .../import/qti12/class.assImagemapQuestionImport.php | 2 ++ .../classes/import/qti12/class.assKprimChoiceImport.php | 2 ++ .../classes/import/qti12/class.assLongMenuImport.php | 3 +++ .../import/qti12/class.assMatchingQuestionImport.php | 2 ++ .../import/qti12/class.assMultipleChoiceImport.php | 2 ++ .../classes/import/qti12/class.assNumericImport.php | 2 ++ .../import/qti12/class.assOrderingHorizontalImport.php | 2 ++ .../import/qti12/class.assOrderingQuestionImport.php | 2 ++ .../classes/import/qti12/class.assQuestionImport.php | 2 ++ .../classes/import/qti12/class.assSingleChoiceImport.php | 2 ++ .../classes/import/qti12/class.assTextQuestionImport.php | 2 ++ .../classes/import/qti12/class.assTextSubsetImport.php | 2 ++ .../class.ilAssQuestionSkillAssignmentExporter.php | 5 +---- .../class.ilAssQuestionSkillAssignmentImport.php | 5 +---- .../class.ilAssQuestionSkillAssignmentImportFails.php | 5 +---- .../class.ilAssQuestionSkillAssignmentImportList.php | 6 +----- .../class.ilAssQuestionSkillAssignmentImporter.php | 5 +---- .../class.ilAssQuestionSkillAssignmentRegistry.php | 2 +- .../class.ilAssQuestionSkillAssignmentXmlParser.php | 5 +---- .../interfaces/QTI/interface.ilQTIFlowMatAware.php | 3 +-- .../interfaces/QTI/interface.ilQTIMaterialAware.php | 3 +-- .../QTI/interface.ilQTIPresentationMaterialAware.php | 3 +-- .../src/Import/TestQuestionsImportTrait.php | 3 +++ 62 files changed, 113 insertions(+), 86 deletions(-) diff --git a/components/ILIAS/Test/classes/class.ilObjTest.php b/components/ILIAS/Test/classes/class.ilObjTest.php index 67601ecbabd0..9e2b633664da 100755 --- a/components/ILIAS/Test/classes/class.ilObjTest.php +++ b/components/ILIAS/Test/classes/class.ilObjTest.php @@ -2768,8 +2768,7 @@ public function getAvailableQuestions($arr_filter, $completeonly = 0): array } /** - * Receives parameters from a QTI parser and creates a valid ILIAS test object - * @param ilQTIAssessment $assessment + * @deprecated 12: Use ILIAS\Test\ExportImport\TestImporter instead */ public function fromXML(ilQTIAssessment $assessment, array $mappings): void { diff --git a/components/ILIAS/Test/classes/class.ilObjTestXMLParser.php b/components/ILIAS/Test/classes/class.ilObjTestXMLParser.php index 040d72b5e6af..bf86e7f42f4d 100755 --- a/components/ILIAS/Test/classes/class.ilObjTestXMLParser.php +++ b/components/ILIAS/Test/classes/class.ilObjTestXMLParser.php @@ -23,10 +23,7 @@ use ILIAS\Test\Logging\TestLogger; /** - * @author Björn Heyser - * @version $Id$ - * - * @package components\ILIAS/Test + * @deprecated 12: Use ILIAS\Test\ExportImport\TestImporter instead */ class ilObjTestXMLParser extends ilSaxParser { diff --git a/components/ILIAS/Test/classes/class.ilTestResultsImportParser.php b/components/ILIAS/Test/classes/class.ilTestResultsImportParser.php index 01430e9c6403..4ec1272ba213 100755 --- a/components/ILIAS/Test/classes/class.ilTestResultsImportParser.php +++ b/components/ILIAS/Test/classes/class.ilTestResultsImportParser.php @@ -22,6 +22,9 @@ use ILIAS\ResourceStorage\Services as ResourceStorage; use ILIAS\Test\Logging\TestLogger; +/** + * @deprecated 12: Use ILIAS\Test\ExportImport\TestImporter instead + */ class ilTestResultsImportParser extends ilSaxParser { private $table; diff --git a/components/ILIAS/Test/classes/class.ilTestResultsToXML.php b/components/ILIAS/Test/classes/class.ilTestResultsToXML.php index 55bd7f87c0eb..56492edf0670 100644 --- a/components/ILIAS/Test/classes/class.ilTestResultsToXML.php +++ b/components/ILIAS/Test/classes/class.ilTestResultsToXML.php @@ -300,4 +300,4 @@ public function xmlDumpFile(string $file, bool $format = true): void $this->getXML(); parent::xmlDumpFile($file, $format); } -} \ No newline at end of file +} diff --git a/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdExporter.php b/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdExporter.php index 7c8c3a6b6553..e638617684e5 100755 --- a/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdExporter.php +++ b/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdExporter.php @@ -19,10 +19,7 @@ declare(strict_types=1); /** - * @author Björn Heyser - * @version $Id$ - * - * @package components\ILIAS/Test(QuestionPool) + * @deprecated 12: Use ILIAS\Test\ExportImport\TestExporter instead */ class ilTestSkillLevelThresholdExporter { diff --git a/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdImport.php b/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdImport.php index 39d86e9d0e0c..c96c69ba7023 100755 --- a/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdImport.php +++ b/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdImport.php @@ -19,10 +19,7 @@ declare(strict_types=1); /** - * @author Björn Heyser - * @version $Id$ - * - * @package components\ILIAS/Test(QuestionPool) + * @deprecated 12: Use ILIAS\Test\ExportImport\Import\SkillLevelThresholdImporter instead */ class ilTestSkillLevelThresholdImport { diff --git a/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdImportFails.php b/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdImportFails.php index a81bafdf12d1..ec746488d441 100755 --- a/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdImportFails.php +++ b/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdImportFails.php @@ -19,10 +19,7 @@ declare(strict_types=1); /** - * @author Björn Heyser - * @version $Id$ - * - * @package components\ILIAS/Test(QuestionPool) + * @deprecated 12: Use ILIAS\Test\ExportImport\Import\SkillLevelThresholdImporter instead */ class ilTestSkillLevelThresholdImportFails { diff --git a/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdImportList.php b/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdImportList.php index ee780c7e4c58..c67bac11a301 100755 --- a/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdImportList.php +++ b/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdImportList.php @@ -19,10 +19,7 @@ declare(strict_types=1); /** - * @author Björn Heyser - * @version $Id$ - * - * @package components\ILIAS/Test(QuestionPool) + * @deprecated 12: Use ILIAS\Test\ExportImport\Import\SkillLevelThresholdImporter instead */ class ilTestSkillLevelThresholdImportList implements Iterator { diff --git a/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdImporter.php b/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdImporter.php index a10f4136d347..3620da697425 100755 --- a/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdImporter.php +++ b/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdImporter.php @@ -19,10 +19,7 @@ declare(strict_types=1); /** - * @author Björn Heyser - * @version $Id$ - * - * @package components\ILIAS/Test + * @deprecated 12: Use ILIAS\Test\ExportImport\Import\SkillLevelThresholdImporter instead */ class ilTestSkillLevelThresholdImporter { diff --git a/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdList.php b/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdList.php index 1fde8f370a0a..766478011e71 100755 --- a/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdList.php +++ b/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdList.php @@ -19,10 +19,7 @@ declare(strict_types=1); /** - * @author Björn Heyser - * @version $Id$ - * - * @package components\ILIAS/Test + * @deprecated 12: Use ILIAS\Test\ExportImport\Import\SkillLevelThresholdImporter instead */ class ilTestSkillLevelThresholdList { diff --git a/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdXmlParser.php b/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdXmlParser.php index 2d7f1ab684a8..9ba875ca76cd 100755 --- a/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdXmlParser.php +++ b/components/ILIAS/Test/classes/class.ilTestSkillLevelThresholdXmlParser.php @@ -19,10 +19,7 @@ declare(strict_types=1); /** - * @author Björn Heyser - * @version $Id$ - * - * @package components\ILIAS/Test + * @deprecated 12: Use ILIAS\Test\ExportImport\Import\SkillLevelThresholdImporter instead */ class ilTestSkillLevelThresholdXmlParser extends ilSaxParser { diff --git a/components/ILIAS/Test/src/ExportImport/ExportFixedQuestionSet.php b/components/ILIAS/Test/src/ExportImport/ExportFixedQuestionSet.php index 6602c4040e6a..26336e194c22 100755 --- a/components/ILIAS/Test/src/ExportImport/ExportFixedQuestionSet.php +++ b/components/ILIAS/Test/src/ExportImport/ExportFixedQuestionSet.php @@ -21,10 +21,7 @@ namespace ILIAS\Test\ExportImport; /** - * @author Björn Heyser - * @version $Id$ - * - * @package components\ILIAS/Test + * @deprecated 12: Use ILIAS\Test\ExportImport\TestExporter instead */ class ExportFixedQuestionSet extends Export { diff --git a/components/ILIAS/Test/src/ExportImport/ExportRandomQuestionSet.php b/components/ILIAS/Test/src/ExportImport/ExportRandomQuestionSet.php index 3785bc712fee..89a245670df7 100755 --- a/components/ILIAS/Test/src/ExportImport/ExportRandomQuestionSet.php +++ b/components/ILIAS/Test/src/ExportImport/ExportRandomQuestionSet.php @@ -21,10 +21,7 @@ namespace ILIAS\Test\ExportImport; /** - * @author Björn Heyser - * @version $Id$ - * - * @package components\ILIAS/Test + * @deprecated 12: Use ILIAS\Test\ExportImport\TestExporter instead */ class ExportRandomQuestionSet extends Export { diff --git a/components/ILIAS/Test/src/ExportImport/Exportable.php b/components/ILIAS/Test/src/ExportImport/Exportable.php index 2ba1e1cbdd70..8849c6f9dfa3 100644 --- a/components/ILIAS/Test/src/ExportImport/Exportable.php +++ b/components/ILIAS/Test/src/ExportImport/Exportable.php @@ -35,6 +35,8 @@ * The methods should operate solely on the object's internal state and the provided * data array, without relying on external services, database connections, or any other context. * --- + * + * @deprecated 12: Use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable instead */ interface Exportable { diff --git a/components/ILIAS/TestQuestionPool/classes/QTI/class.ilQTIParser.php b/components/ILIAS/TestQuestionPool/classes/QTI/class.ilQTIParser.php index a76740b964d0..9b2fdbd5cb48 100755 --- a/components/ILIAS/TestQuestionPool/classes/QTI/class.ilQTIParser.php +++ b/components/ILIAS/TestQuestionPool/classes/QTI/class.ilQTIParser.php @@ -21,13 +21,7 @@ use ILIAS\TestQuestionPool\Questions\Files\QuestionFiles; /** - * QTI Parser - * - * @author Helmut Schottmüller - * @version $Id$ - * - * @extends ilSaxParser - * @package assessment + * @deprecated 12: QTI export is no longer supported */ class ilQTIParser extends ilSaxParser { diff --git a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php index 9be93d19bd78..ca0a2bfdd85f 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php @@ -261,6 +261,9 @@ public function getProcessLocker(): ilAssQuestionProcessLocker return $this->processLocker; } + /** + * @deprecated 12: Use ILIAS\TestQuestionPool\ExportImport\Import\QuestionsImporter instead + */ final public function fromXML( string $importdirectory, int $user_id, @@ -287,10 +290,8 @@ final public function fromXML( } /** - * Returns a QTI xml representation of the question - * - * @return string The QTI xml representation of the question - */ + * @deprecated 12: use question normalizing and serialization instead + */ final public function toXML( bool $a_include_header = true, bool $a_include_binary = true, diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPool.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPool.php index 8e0ecc27941a..1fe3c72815f3 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPool.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPool.php @@ -361,6 +361,8 @@ public function checkQuestionParent(int $question_id): bool * @param array $questions An array containing the question ids of the questions * @return string The QTI xml representation of the questions * @access public + * + * @deprecated 12: Use ILIAS\Test\ExportImport\TestExporter instead */ public function questionsToXML($questions): string { diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php index 54d9c9a17e7c..8d9e39b766b5 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php @@ -858,6 +858,9 @@ public function cancelDeleteQuestionsObject(): void $this->ctrl->redirect($this, self::DEFAULT_CMD); } + /** + * @deprecated 12: Use ILIAS\Test\ExportImport\TestExporter instead + */ public function exportQuestions(array $ids): void { if ($ids !== []) { @@ -1093,6 +1096,9 @@ public function moveQuestions(array $ids): void $this->tpl->setOnScreenMessage('info', $this->lng->txt('qpl_move_insert_clipboard'), true); } + /** + * @deprecated 12: excel export is not longer supported + */ public function createExportExcel(): void { $rbacsystem = $this->rbac_system; diff --git a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assClozeTestExport.php b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assClozeTestExport.php index 38df4fc4c97e..f17da91214e2 100755 --- a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assClozeTestExport.php +++ b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assClozeTestExport.php @@ -27,6 +27,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question export instead */ class assClozeTestExport extends assQuestionExport { diff --git a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assErrorTextExport.php b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assErrorTextExport.php index 7e10367f03a8..ebc698d0fb27 100755 --- a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assErrorTextExport.php +++ b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assErrorTextExport.php @@ -22,6 +22,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question export instead */ class assErrorTextExport extends assQuestionExport { diff --git a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assFileUploadExport.php b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assFileUploadExport.php index 729ca485edea..9be379594bc2 100755 --- a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assFileUploadExport.php +++ b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assFileUploadExport.php @@ -22,6 +22,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question export instead */ class assFileUploadExport extends assQuestionExport { diff --git a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assFormulaQuestionExport.php b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assFormulaQuestionExport.php index 75746c4f8761..8d7bb7b138ea 100755 --- a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assFormulaQuestionExport.php +++ b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assFormulaQuestionExport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id: class.assFormulaQuestionExport.php 1185 2010-02-02 08:36:26Z hschottm $ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question export instead */ class assFormulaQuestionExport extends assQuestionExport { diff --git a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assImagemapQuestionExport.php b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assImagemapQuestionExport.php index 0820fc8f17a2..5b491c36704b 100755 --- a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assImagemapQuestionExport.php +++ b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assImagemapQuestionExport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question export instead */ class assImagemapQuestionExport extends assQuestionExport { diff --git a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assKprimChoiceExport.php b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assKprimChoiceExport.php index cc1ba7f8c060..0714429deb63 100755 --- a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assKprimChoiceExport.php +++ b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assKprimChoiceExport.php @@ -21,6 +21,8 @@ * @version $Id$ * * @package components\ILIAS/Test + * + * @deprecated Use normalized question export instead */ class assKprimChoiceExport extends assQuestionExport { diff --git a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assLongMenuExport.php b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assLongMenuExport.php index 74f602d0676f..3ab5b06433e3 100755 --- a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assLongMenuExport.php +++ b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assLongMenuExport.php @@ -16,6 +16,9 @@ * *********************************************************************/ +/** + * @deprecated Use normalized question export instead + */ class assLongMenuExport extends assQuestionExport { /** diff --git a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assMatchingQuestionExport.php b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assMatchingQuestionExport.php index 778bfd88a31a..392765a828c5 100755 --- a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assMatchingQuestionExport.php +++ b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assMatchingQuestionExport.php @@ -26,6 +26,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question export instead */ class assMatchingQuestionExport extends assQuestionExport { diff --git a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assMultipleChoiceExport.php b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assMultipleChoiceExport.php index 472c8b9dd5b7..eda1f43901dc 100755 --- a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assMultipleChoiceExport.php +++ b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assMultipleChoiceExport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question export instead */ class assMultipleChoiceExport extends assQuestionExport { diff --git a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assNumericExport.php b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assNumericExport.php index 9a2c6c24b5d6..aa3f33a2ecd7 100755 --- a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assNumericExport.php +++ b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assNumericExport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question export instead */ class assNumericExport extends assQuestionExport { diff --git a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assOrderingHorizontalExport.php b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assOrderingHorizontalExport.php index aee266f640af..0741b2176f06 100755 --- a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assOrderingHorizontalExport.php +++ b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assOrderingHorizontalExport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question export instead */ class assOrderingHorizontalExport extends assQuestionExport { diff --git a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assOrderingQuestionExport.php b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assOrderingQuestionExport.php index de04e8117f9d..fbe29f69d106 100755 --- a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assOrderingQuestionExport.php +++ b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assOrderingQuestionExport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question export instead */ class assOrderingQuestionExport extends assQuestionExport { diff --git a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assQuestionExport.php b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assQuestionExport.php index a6c0a2755b23..4afa6e755e5a 100755 --- a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assQuestionExport.php +++ b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assQuestionExport.php @@ -26,6 +26,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question export instead */ class assQuestionExport { diff --git a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assSingleChoiceExport.php b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assSingleChoiceExport.php index d9aedc377e7a..299e459abced 100755 --- a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assSingleChoiceExport.php +++ b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assSingleChoiceExport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question export instead */ class assSingleChoiceExport extends assQuestionExport { diff --git a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assTextQuestionExport.php b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assTextQuestionExport.php index 009fb070ac68..7115f427d2af 100755 --- a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assTextQuestionExport.php +++ b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assTextQuestionExport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question export instead */ class assTextQuestionExport extends assQuestionExport { diff --git a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assTextSubsetExport.php b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assTextSubsetExport.php index 8fbbf6a12d04..da12b9fc3a56 100755 --- a/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assTextSubsetExport.php +++ b/components/ILIAS/TestQuestionPool/classes/export/qti12/class.assTextSubsetExport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question export instead */ class assTextSubsetExport extends assQuestionExport { diff --git a/components/ILIAS/TestQuestionPool/classes/export/qti21/class.assLongMenuExportQti21.php b/components/ILIAS/TestQuestionPool/classes/export/qti21/class.assLongMenuExportQti21.php index 2160425f2087..91a664b0f780 100755 --- a/components/ILIAS/TestQuestionPool/classes/export/qti21/class.assLongMenuExportQti21.php +++ b/components/ILIAS/TestQuestionPool/classes/export/qti21/class.assLongMenuExportQti21.php @@ -16,6 +16,9 @@ * *********************************************************************/ +/** + * @deprecated Use normalized question export instead + */ class assLongMenuExportQti21 extends assQuestionExport { /** diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assClozeTestImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assClozeTestImport.php index 1e95b8d5c56d..26c9fbfa93e6 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assClozeTestImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assClozeTestImport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question import instead */ class assClozeTestImport extends assQuestionImport { diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assErrorTextImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assErrorTextImport.php index aaa7db0a95a1..30aaba38460b 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assErrorTextImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assErrorTextImport.php @@ -22,6 +22,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question import instead */ class assErrorTextImport extends assQuestionImport { diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assFileUploadImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assFileUploadImport.php index 3f08c77b5e4c..86a38521584d 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assFileUploadImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assFileUploadImport.php @@ -22,6 +22,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question import instead */ class assFileUploadImport extends assQuestionImport { diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assFormulaQuestionImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assFormulaQuestionImport.php index 5fb30a0a6003..860f3df10f6d 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assFormulaQuestionImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assFormulaQuestionImport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id: class.assFormulaQuestionImport.php 1185 2010-02-02 08:36:26Z hschottm $ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question import instead */ class assFormulaQuestionImport extends assQuestionImport { diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assImagemapQuestionImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assImagemapQuestionImport.php index 3e19507abbdc..b8cab0d96fa8 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assImagemapQuestionImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assImagemapQuestionImport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question import instead */ class assImagemapQuestionImport extends assQuestionImport { diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assKprimChoiceImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assKprimChoiceImport.php index 734b1917d099..e3c68406308f 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assKprimChoiceImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assKprimChoiceImport.php @@ -21,6 +21,8 @@ * @version $Id$ * * @package components\ILIAS/Test + * + * @deprecated Use normalized question import instead */ class assKprimChoiceImport extends assQuestionImport { diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assLongMenuImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assLongMenuImport.php index f98757706f54..e8433eae21f1 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assLongMenuImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assLongMenuImport.php @@ -16,6 +16,9 @@ * *********************************************************************/ +/** + * @deprecated Use normalized question import instead + */ class assLongMenuImport extends assQuestionImport { public $object; diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assMatchingQuestionImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assMatchingQuestionImport.php index 3e7a5946d040..7d992a428b8d 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assMatchingQuestionImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assMatchingQuestionImport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question import instead */ class assMatchingQuestionImport extends assQuestionImport { diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assMultipleChoiceImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assMultipleChoiceImport.php index 7b8cb9841dc8..584bb6fdce36 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assMultipleChoiceImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assMultipleChoiceImport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question import instead */ class assMultipleChoiceImport extends assQuestionImport { diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assNumericImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assNumericImport.php index ce1750add09a..c73529bdc747 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assNumericImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assNumericImport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question import instead */ class assNumericImport extends assQuestionImport { diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assOrderingHorizontalImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assOrderingHorizontalImport.php index 94550a37f495..9969ef698257 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assOrderingHorizontalImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assOrderingHorizontalImport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question import instead */ class assOrderingHorizontalImport extends assQuestionImport { diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assOrderingQuestionImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assOrderingQuestionImport.php index 5c1ef72b620b..74853cda461d 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assOrderingQuestionImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assOrderingQuestionImport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question import instead */ class assOrderingQuestionImport extends assQuestionImport { diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assQuestionImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assQuestionImport.php index 51aabfc9cb85..c9b9f2fe8154 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assQuestionImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assQuestionImport.php @@ -30,6 +30,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question import instead */ class assQuestionImport { diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assSingleChoiceImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assSingleChoiceImport.php index af1fcdd4ce61..3d70e52b39e6 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assSingleChoiceImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assSingleChoiceImport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question import instead */ class assSingleChoiceImport extends assQuestionImport { diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assTextQuestionImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assTextQuestionImport.php index 3ab40e8be8bc..2039f5cea6c6 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assTextQuestionImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assTextQuestionImport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question import instead */ class assTextQuestionImport extends assQuestionImport { diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assTextSubsetImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assTextSubsetImport.php index cd21132fb74f..46241f5df10f 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assTextSubsetImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assTextSubsetImport.php @@ -24,6 +24,8 @@ * @author Helmut Schottmüller * @version $Id$ * @ingroup components\ILIASTestQuestionPool + * + * @deprecated Use normalized question import instead */ class assTextSubsetImport extends assQuestionImport { diff --git a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentExporter.php b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentExporter.php index 3dad3c8d4346..ce2863de95a8 100755 --- a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentExporter.php +++ b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentExporter.php @@ -19,10 +19,7 @@ declare(strict_types=1); /** - * @author Björn Heyser - * @version $Id$ - * - * @package components\ILIAS/TestQuestionPool + * @deprecated 12: Use ILIAS\TestQuestionPool\ExportImport\Export\QuestionPoolExporter instead */ class ilAssQuestionSkillAssignmentExporter { diff --git a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentImport.php b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentImport.php index 4c744ef0bfe2..b19aecac3437 100755 --- a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentImport.php +++ b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentImport.php @@ -17,10 +17,7 @@ *********************************************************************/ /** - * @author Björn Heyser - * @version $Id$ - * - * @package components\ILIAS/TestQuestionPool + * @deprecated 12: Use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter instead */ class ilAssQuestionSkillAssignmentImport { diff --git a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentImportFails.php b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentImportFails.php index 5ebe88a7d3f8..a6d32523c1f2 100755 --- a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentImportFails.php +++ b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentImportFails.php @@ -17,10 +17,7 @@ *********************************************************************/ /** - * @author Björn Heyser - * @version $Id$ - * - * @package components\ILIAS/TestQuestionPool + * @deprecated 12: Use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter instead */ class ilAssQuestionSkillAssignmentImportFails { diff --git a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentImportList.php b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentImportList.php index aefe73068ac1..5fed2d9f0571 100755 --- a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentImportList.php +++ b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentImportList.php @@ -19,11 +19,7 @@ declare(strict_types=1); /** - * @author Björn Heyser - * @version $Id$ - * - * @package components\ILIAS/TestQuestionPool - * @implements Iterator + * @deprecated 12: Use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter instead */ class ilAssQuestionSkillAssignmentImportList implements Iterator { diff --git a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentImporter.php b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentImporter.php index 049f48ebbc48..2942b61082db 100755 --- a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentImporter.php +++ b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentImporter.php @@ -19,10 +19,7 @@ use ILIAS\Skill\Service\SkillUsageService; /** - * @author Björn Heyser - * @version $Id$ - * - * @package components\ILIAS/TestQuestionPool + * @deprecated 12: Use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter instead */ class ilAssQuestionSkillAssignmentImporter { diff --git a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentRegistry.php b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentRegistry.php index 07af315d4ab0..49b409c38c10 100755 --- a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentRegistry.php +++ b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentRegistry.php @@ -17,7 +17,7 @@ *********************************************************************/ /** - * Class ilAssQuestionSkillAssignmentRegistry + * @deprecated 12: Use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter instead */ class ilAssQuestionSkillAssignmentRegistry { diff --git a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentXmlParser.php b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentXmlParser.php index 58c0b7c85db8..9b48877ef0af 100755 --- a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentXmlParser.php +++ b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionSkillAssignmentXmlParser.php @@ -19,10 +19,7 @@ declare(strict_types=1); /** - * @author Björn Heyser - * @version $Id$ - * - * @package components\ILIAS/TestQuestionPool + * @deprecated 12: Use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter instead */ class ilAssQuestionSkillAssignmentXmlParser extends ilSaxParser { diff --git a/components/ILIAS/TestQuestionPool/interfaces/QTI/interface.ilQTIFlowMatAware.php b/components/ILIAS/TestQuestionPool/interfaces/QTI/interface.ilQTIFlowMatAware.php index d6a63ebe6a2e..d123e088c1cc 100755 --- a/components/ILIAS/TestQuestionPool/interfaces/QTI/interface.ilQTIFlowMatAware.php +++ b/components/ILIAS/TestQuestionPool/interfaces/QTI/interface.ilQTIFlowMatAware.php @@ -19,8 +19,7 @@ declare(strict_types=1); /** - * Interface ilQTIMaterialAware - * @author Michael Jansen + * @deprecated 12: QTI export is no longer supported */ interface ilQTIFlowMatAware { diff --git a/components/ILIAS/TestQuestionPool/interfaces/QTI/interface.ilQTIMaterialAware.php b/components/ILIAS/TestQuestionPool/interfaces/QTI/interface.ilQTIMaterialAware.php index 3aa61a17ca2d..0217a864d1db 100755 --- a/components/ILIAS/TestQuestionPool/interfaces/QTI/interface.ilQTIMaterialAware.php +++ b/components/ILIAS/TestQuestionPool/interfaces/QTI/interface.ilQTIMaterialAware.php @@ -19,8 +19,7 @@ declare(strict_types=1); /** - * Interface ilQTIMaterialAware - * @author Michael Jansen + * @deprecated 12: QTI export is no longer supported */ interface ilQTIMaterialAware { diff --git a/components/ILIAS/TestQuestionPool/interfaces/QTI/interface.ilQTIPresentationMaterialAware.php b/components/ILIAS/TestQuestionPool/interfaces/QTI/interface.ilQTIPresentationMaterialAware.php index f0a445a25990..cb7bf714e096 100755 --- a/components/ILIAS/TestQuestionPool/interfaces/QTI/interface.ilQTIPresentationMaterialAware.php +++ b/components/ILIAS/TestQuestionPool/interfaces/QTI/interface.ilQTIPresentationMaterialAware.php @@ -19,8 +19,7 @@ declare(strict_types=1); /** - * Interface ilQTIPresentationMaterialAware - * @author Michael Jansen + * @deprecated 12: QTI export is no longer supported */ interface ilQTIPresentationMaterialAware { diff --git a/components/ILIAS/TestQuestionPool/src/Import/TestQuestionsImportTrait.php b/components/ILIAS/TestQuestionPool/src/Import/TestQuestionsImportTrait.php index 07e896a3a420..152496760d60 100644 --- a/components/ILIAS/TestQuestionPool/src/Import/TestQuestionsImportTrait.php +++ b/components/ILIAS/TestQuestionPool/src/Import/TestQuestionsImportTrait.php @@ -23,6 +23,9 @@ use Psr\Http\Message\ServerRequestInterface; use ILIAS\UI\Component\Input\Container\Form\Standard as StandardForm; +/** + * @deprecated 12: Use ILIAS\Test\ExportImport\TestImporter instead + */ trait TestQuestionsImportTrait { private string $import_temp_directory = CLIENT_DATA_DIR . DIRECTORY_SEPARATOR . 'temp'; From c792cf6189fcace055347d76389e46bdaeeeb552 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Mon, 21 Sep 2026 13:23:00 +0200 Subject: [PATCH 51/66] test: characterize export import foundation seams --- .../ExportImportCharacterizationTest.php | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php diff --git a/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php b/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php new file mode 100644 index 000000000000..63a502f4a1fc --- /dev/null +++ b/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php @@ -0,0 +1,250 @@ +session = $_SESSION ?? []; + $_SESSION = []; + $this->setGlobalVariable( + 'refinery', + new RefineryFactory(new DataFactory(), $GLOBALS['DIC']['lng']) + ); + } + + protected function tearDown(): void + { + $_SESSION = $this->session; + + parent::tearDown(); + } + + public function testSingleChoiceRoundtripPreservesQuestionAndCollectsImages(): void + { + global $DIC; + + $question = new assSingleChoice( + 'Characterization question', + 'Question description', + 'Question author', + 17, + 'Which answer is correct?' + ); + $question->setId(23); + $question->setObjId(42); + $question->setAnswers([ + new ASS_AnswerBinaryStateImage('Correct', 2.5, 0, true, 'answer.png', 5), + new ASS_AnswerBinaryStateImage('Incorrect', 0.0, 1, false, null, 6), + ]); + + $export_images = new CollectQuestionImages( + new UUIDFactory(), + (new DataFactory())->objId(42) + ); + $export_transformations = (new Builder($DIC, QuestionPoolDIC::dic())) + ->withAdditionalPipes([$export_images]) + ->create(); + + $normalized = $export_transformations->normalize($question); + + $this->assertSame('Characterization question', $normalized['title']); + $this->assertSame('Which answer is correct?', $normalized['question_text']); + $this->assertNotSame('', $normalized['answers'][0]['image']['id']); + $this->assertSame( + [[ + 'from' => CLIENT_WEB_DIR . '/assessment/42/23/images/answer.png', + 'to' => $normalized['answers'][0]['image']['id'] . '.png' + ]], + $export_images->getFiles() + ); + + $import_images = new CollectQuestionImages( + new UUIDFactory(), + (new DataFactory())->objId(0) + ); + $import_transformations = (new Builder($DIC, QuestionPoolDIC::dic())) + ->withAdditionalPipes(append: [$import_images]) + ->create(); + + $restored = $import_transformations->denormalize($normalized, new assSingleChoice()); + + $this->assertInstanceOf(assSingleChoice::class, $restored); + $this->assertSame($question->getId(), $restored->getId()); + $this->assertSame($question->getObjId(), $restored->getObjId()); + $this->assertSame($question->getTitle(), $restored->getTitle()); + $this->assertSame($question->getAuthor(), $restored->getAuthor()); + $this->assertSame($question->getQuestion(), $restored->getQuestion()); + $this->assertCount(2, $restored->getAnswers()); + $this->assertSame('Correct', $restored->getAnswers()[0]->getAnswertext()); + $this->assertSame(2.5, $restored->getAnswers()[0]->getPoints()); + $this->assertSame('answer.png', $restored->getAnswers()[0]->getImage()); + $this->assertArrayHasKey( + $normalized['answers'][0]['image']['id'] . '.png', + $import_images->getEnvelopes() + ); + } + + public function testXmlStringAndFileDeserializersProduceTheSameData(): void + { + $serializer = (new SimpleXMLSerializer())->open(''); + $serializer->createDocument('Export/import characterization'); + $serializer->group('sample_group', function () use ($serializer): void { + $serializer->append('sample', [ + 'empty_list' => [], + 'enabled' => true, + 'count' => 3, + 'nothing' => null, + 'label' => 'A < B', + ]); + }); + $xml = $serializer->write(); + + $path = tempnam(sys_get_temp_dir(), 'ilias-export-import-'); + $this->assertNotFalse($path); + file_put_contents($path, $xml); + + try { + $from_string = $this->deserializeGroup( + (new XMLMemoryDeserializer())->open($xml), + 'sample_group' + ); + $from_file = $this->deserializeGroup( + (new XMLFileDeserializer())->open($path), + 'sample_group' + ); + } finally { + unlink($path); + } + + $expected = [[ + 'empty_list' => [], + 'enabled' => '1', + 'count' => '3', + 'nothing' => null, + 'label' => 'A < B', + ]]; + $this->assertSame($expected, $from_string); + $this->assertSame($expected, $from_file); + } + + public function testXmlMemoryDeserializerProcessesDocumentFragments(): void + { + $groups = []; + $deserializer = (new XMLMemoryDeserializer())->open( + '' + . 'first' + . 'second' + ); + $deserializer->addHandler( + 'sample_group', + static function (array $group_data) use (&$groups): void { + $groups[] = $group_data; + } + ); + + $deserializer->process(); + + $this->assertSame( + [ + [['value' => 'first']], + [['value' => 'second']], + ], + $groups + ); + } + + public function testXmlFileDeserializerRejectsMissingFiles(): void + { + $this->expectException(InvalidArgumentException::class); + + (new XMLFileDeserializer())->open(__DIR__ . '/missing.xml'); + } + + public function testXmlSerializerRejectsMismatchedGroups(): void + { + $serializer = (new SimpleXMLSerializer())->open(''); + $serializer->startGroup('expected'); + + $this->expectException(LogicException::class); + + $serializer->endGroup('actual'); + } + + public function testImportSessionRepositoryPreservesCurrentContextBehavior(): void + { + $repository = new ImportSessionRepository('characterization'); + $context = (new ImportContext()) + ->with('file_to_import', '/tmp/import.zip') + ->with('selected_question_ids', ['12', '27']) + ->with('unknown_field', 'preserved'); + + $repository->setCurrentStageIndex(3); + $repository->setContext($context); + + $restored = $repository->getContext(); + $this->assertSame(3, $repository->getCurrentStageIndex()); + $this->assertSame('/tmp/import.zip', $restored->get('file_to_import')); + $this->assertSame(['12', '27'], $restored->get('selected_question_ids')); + $this->assertSame('preserved', $restored->get('unknown_field')); + $this->assertNull($restored->get('missing')); + $this->assertSame('fallback', $restored->get('missing', 'fallback')); + + $repository->clear(); + + $this->assertSame(0, $repository->getCurrentStageIndex()); + $this->assertFalse($repository->getContext()->has('file_to_import')); + } + + private function deserializeGroup(Deserializer $deserializer, string $group): array + { + $data = null; + $deserializer->addHandler($group, static function (array $group_data) use (&$data): void { + $data = $group_data; + }); + $deserializer->process(); + + $this->assertIsArray($data); + return $data; + } +} From 65b4ecf591b9432040b63b2c3b83577a4a39c7b7 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Mon, 21 Sep 2026 14:01:54 +0200 Subject: [PATCH 52/66] refactor: isolate export state lifecycle --- .../Test/classes/class.ilTestExporter.php | 6 +- .../class.ilTestQuestionPoolExporter.php | 6 +- .../Foundation/Bridge/StateHolder.php | 7 ++- .../Foundation/Bridge/XmlExporterBridge.php | 5 +- .../tests/ExportImport/StateHolderTest.php | 57 +++++++++++++++++++ 5 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 components/ILIAS/TestQuestionPool/tests/ExportImport/StateHolderTest.php diff --git a/components/ILIAS/Test/classes/class.ilTestExporter.php b/components/ILIAS/Test/classes/class.ilTestExporter.php index 9dbcb826d5ec..28640ac3e920 100755 --- a/components/ILIAS/Test/classes/class.ilTestExporter.php +++ b/components/ILIAS/Test/classes/class.ilTestExporter.php @@ -21,6 +21,7 @@ use ILIAS\Export\ExportHandler\Factory as ExportHandler; use ILIAS\Test\ExportImport\Types; use ILIAS\Test\TestDIC; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportStep; use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\XmlExporterBridge; class ilTestExporter extends ilXmlExporter @@ -65,7 +66,10 @@ public function getXmlExportTailDependencies(string $a_entity, string $a_target_ } // If the default export option was used, the state is not initialized yet. - if (!$this->state_holder->exists()) { + if ( + !$this->state_holder->exists() + || $this->state_holder->get()->getStep() !== ExportStep::INIT + ) { $this->initExportState( 'components/ILIAS/Test', $a_target_release, diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php index b50fad4eeb33..dbe1bd88aa2c 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php @@ -17,6 +17,7 @@ *********************************************************************/ use ILIAS\Export\ExportHandler\Factory as ExportHandler; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportStep; use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\XmlExporterBridge; use ILIAS\TestQuestionPool\QuestionPoolDIC; @@ -64,7 +65,10 @@ public function getXmlExportTailDependencies(string $a_entity, string $a_target_ } // If the default export option was used, the state is not initialized yet. - if (!$this->state_holder->exists()) { + if ( + !$this->state_holder->exists() + || $this->state_holder->get()->getStep() !== ExportStep::INIT + ) { $this->initExportState( 'components/ILIAS/TestQuestionPool', $a_target_release, diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/StateHolder.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/StateHolder.php index 9b3726207d60..db0b359b1e1a 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/StateHolder.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/StateHolder.php @@ -38,7 +38,7 @@ public function create( ExportConfig $config, string $option = '' ): ExportState { - return $this->export_state ??= new ExportState($target, $config, $option); + return $this->export_state = new ExportState($target, $config, $option); } public function exists(): bool @@ -58,4 +58,9 @@ public function set(ExportState $export_state): void { $this->export_state = $export_state; } + + public function clear(): void + { + $this->export_state = null; + } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php index 512df474c02b..f99ffcc3a06d 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php @@ -60,7 +60,7 @@ private function processExport(): ExportState if ($state->getStep()->value < ExportStep::PROCESS->value) { $this->logger->debug("Processing export for component {$state->target()->getComponent()}..."); - $state->setSerializer(new SimpleXMLSerializer()->open('memory')); + $state->setSerializer((new SimpleXMLSerializer())->open('memory')); $this->exporter->process($state); $this->logger->debug('...Finished processing export'); } @@ -86,7 +86,7 @@ private function finalizeExport(): ExportState $this->logger->debug('...Finished writing export'); } - $this->state_holder->set($state); + $this->state_holder->clear(); return $state; } @@ -97,6 +97,7 @@ private function initExportState( array $object_ids, string $option = '' ): ExportState { + $object_ids = array_slice($object_ids, 0, 1); $target = $this->export_handler->target()->handler() ->withType($type) ->withTargetRelease($target_release) diff --git a/components/ILIAS/TestQuestionPool/tests/ExportImport/StateHolderTest.php b/components/ILIAS/TestQuestionPool/tests/ExportImport/StateHolderTest.php new file mode 100644 index 000000000000..706598c7b030 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/tests/ExportImport/StateHolderTest.php @@ -0,0 +1,57 @@ +createMock(ExportConfig::class); + $first_target = $this->createMock(ExportTarget::class); + $second_target = $this->createMock(ExportTarget::class); + + $first = $holder->create($first_target, $config, 'first'); + $second = $holder->create($second_target, $config, 'second'); + + $this->assertNotSame($first, $second); + $this->assertSame($second, $holder->get()); + $this->assertSame($second_target, $second->target()); + $this->assertSame('second', $second->getOption()); + } + + public function testClearRemovesExistingState(): void + { + $holder = new StateHolder(); + $holder->create( + $this->createMock(ExportTarget::class), + $this->createMock(ExportConfig::class) + ); + + $holder->clear(); + + $this->assertFalse($holder->exists()); + $this->expectException(RuntimeException::class); + $holder->get(); + } +} From 73b854bf60726f213352b69d0212258059bbc406 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Mon, 21 Sep 2026 14:24:50 +0200 Subject: [PATCH 53/66] refactor: unify XML serialization --- .../Test/classes/class.ilTestImporter.php | 4 +- .../src/ExportImport/Import/PersistStage.php | 4 +- .../Test/src/ExportImport/TestExporter.php | 4 +- .../class.ilTestQuestionPoolImporter.php | 4 +- .../Foundation/Bridge/XmlExporterBridge.php | 4 +- .../Foundation/Contracts/Deserializer.php | 17 +- .../Foundation/Contracts/Serializer.php | 42 +--- .../Serializing/JSONMemoryDeserializer.php | 70 ------ .../Serializing/JSONMemorySerializer.php | 117 --------- .../Serializing/XMLDeserializerTrait.php | 197 --------------- .../Serializing/XMLFileDeserializer.php | 66 ----- .../Serializing/XMLMemoryDeserializer.php | 65 ----- .../Serializing/XmlDeserializer.php | 234 ++++++++++++++++++ ...pleXMLSerializer.php => XmlSerializer.php} | 59 ++--- .../Import/QuestionSelectionStage.php | 4 +- .../ExportImportCharacterizationTest.php | 17 +- 16 files changed, 276 insertions(+), 632 deletions(-) delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemoryDeserializer.php delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemorySerializer.php delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLDeserializerTrait.php delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLFileDeserializer.php delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLMemoryDeserializer.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XmlDeserializer.php rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/{SimpleXMLSerializer.php => XmlSerializer.php} (79%) diff --git a/components/ILIAS/Test/classes/class.ilTestImporter.php b/components/ILIAS/Test/classes/class.ilTestImporter.php index e4ffba4da970..33ada2d1d317 100755 --- a/components/ILIAS/Test/classes/class.ilTestImporter.php +++ b/components/ILIAS/Test/classes/class.ilTestImporter.php @@ -22,7 +22,7 @@ use ILIAS\Test\ExportImport\Import\TestImporter; use ILIAS\Test\TestDIC; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XMLMemoryDeserializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlDeserializer; use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; class ilTestImporter extends ilXmlImporter @@ -71,7 +71,7 @@ public function importXmlRepresentation( } $result = $this->importer->import( - new XMLMemoryDeserializer()->open($a_xml), + XmlDeserializer::fromString($a_xml), $a_mapping, new ReferenceId($a_mapping->getTargetId()), $context, diff --git a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php index 79b289c5c32e..177d0d7398b7 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php +++ b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php @@ -25,7 +25,7 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResult; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XMLFileDeserializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlDeserializer; use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; use ilImport; @@ -91,7 +91,7 @@ private function importMappingsFile(ImportContext $context): ?StageResult return StageResult::error($context, $this->lng->txt('obj_import_file_error')); } - $deserializer = new XMLFileDeserializer()->open($mappings_file); + $deserializer = XmlDeserializer::fromFile($mappings_file); $deserializer->addHandler('mappings', function (array $mappings) use (&$context) { $context = $context->with('mappings', $mappings); }); diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index cb3b3497e4c6..5058cf4aca79 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -42,7 +42,7 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Serializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\CollectResources; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\SimpleXMLSerializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlSerializer; use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ilTree; @@ -420,7 +420,7 @@ private function writeMappings( Transformations $transformations, ExportDependencies $state ): void { - $serializer = new SimpleXMLSerializer()->open('memory'); + $serializer = XmlSerializer::inMemory(); $serializer->createDocument('Test Export Mappings'); $serializer->startGroup('mappings'); diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php index 7c3d3f6b6752..3cd0765b9d8b 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php @@ -20,7 +20,7 @@ use ILIAS\Data\ReferenceId; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XMLMemoryDeserializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlDeserializer; use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionPoolImporter; use ILIAS\TestQuestionPool\QuestionPoolDIC; @@ -66,7 +66,7 @@ public function importXmlRepresentation( } $result = $this->importer->import( - new XMLMemoryDeserializer()->open($a_xml), + XmlDeserializer::fromString($a_xml), $a_mapping, new ReferenceId($a_mapping->getTargetId()), $context, diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php index f99ffcc3a06d..05a23b2535b5 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/XmlExporterBridge.php @@ -23,7 +23,7 @@ use ILIAS\Export\ExportHandler\Factory as ExportHandler; use ILIAS\Export\ExportHandler\Info\Export\Path\Handler as ExportPath; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Exporter; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\SimpleXMLSerializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlSerializer; use Psr\Log\LoggerInterface as Logger; /** @@ -60,7 +60,7 @@ private function processExport(): ExportState if ($state->getStep()->value < ExportStep::PROCESS->value) { $this->logger->debug("Processing export for component {$state->target()->getComponent()}..."); - $state->setSerializer((new SimpleXMLSerializer())->open('memory')); + $state->setSerializer(XmlSerializer::inMemory()); $this->exporter->process($state); $this->logger->debug('...Finished processing export'); } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Deserializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Deserializer.php index 93dd7d6fc7e0..72e90e648b70 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Deserializer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Deserializer.php @@ -27,22 +27,9 @@ interface Deserializer { /** - * Open the source (file path or string content) and prepare the deserializer for reading. This method should be - * called before any other method. If the deserializer is used for reading from memory, the path parameter may - * contain the raw content (e.g. JSON string) instead of a file path. + * Registers a handler that is invoked during process() with the data of the matching group. * - * @param string $path The path to the file to read from or the raw content (e.g. when reading from memory) - * - * @return static - */ - public function open(string $path): static; - - /** - * Register a handler for a group of data. When process() is called, the handler will be invoked with the data of - * the matching group. - * - * @param string $group The name of the group - * @param callable(array): void $handler Callable that receives the group data + * @param callable(array): void $handler */ public function addHandler(string $group, callable $handler): void; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Serializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Serializer.php index 71873e44e722..0d3aeb68bd41 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Serializer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Serializer.php @@ -21,58 +21,26 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts; /** - * Interface for serializers that can be used to write data to a file or memory using a specific data format (e.g. XML, - * JSON, etc.). + * Writes data in a specific format. Groups structure the written data into logical units, items are appended to the + * currently open group. */ interface Serializer { - /** - * Open a new file/memory and prepare the serializer for writing. This method should be called before any other - * method. If the serializer is used for writing to memory, the path will be ignored. - * - * @param string $path The path to the file to write to - */ - public function open(string $path): static; - - /** - * Write the data to the file/memory and return the result. If the serializer is used for writing to memory, the - * result will be returned as a string. If the serializer is used for writing to a file, the result will be the path - * to the file. - * - * @return string The path to the file or the stringified data - */ public function write(): string; - /** - * Start a new group of data. Groups are used to structure the data into logical units. - * - * @param string $name The name of the group - */ public function startGroup(string $name): void; - /** - * End the current group of data. This method should be called after the last data has been written to the group. If - * the group name does not match the name of the group that was started, an exception will be thrown. - * - * @param string $name The name of the group - * - * @throws \LogicException If the group name does not match the name of the group that was started - */ public function endGroup(string $name): void; /** - * Start a new group of data, execute the callback and end the group. + * Starts a group, executes the callback and ends the group. * - * @param string $name The name of the group - * @param callable(): void $callback Callback that will be executed when the group is started + * @param callable(): void $callback */ public function group(string $name, callable $callback): void; /** - * Append a data item to the current group. - * - * @param string $name The name of the item - * @param array $data The data to append + * @param array $data */ public function append(string $name, array $data): void; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemoryDeserializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemoryDeserializer.php deleted file mode 100644 index 9313ab12a262..000000000000 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemoryDeserializer.php +++ /dev/null @@ -1,70 +0,0 @@ - $handler */ - private array $handler = []; - - /** - * @inheritDoc - */ - public function open(string $json): static - { - $clone = clone $this; - $clone->decoded = json_decode($json, true); - return $clone; - } - - /** - * @inheritDoc - */ - public function addHandler(string $group, callable $handler): void - { - $this->handler[$group] = $handler; - } - - /** - * @inheritDoc - */ - public function process(): void - { - foreach ($this->decoded as $key => $value) { - if (is_array($value)) { - $head = $value[array_key_first($value)]; - $value = array_is_list($head) ? $head : [$head]; - } - - if (isset($this->handler[$key])) { - $this->handler[$key]($value); - } - } - } -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemorySerializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemorySerializer.php deleted file mode 100644 index 92911b306b1f..000000000000 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/JSONMemorySerializer.php +++ /dev/null @@ -1,117 +0,0 @@ - Stack of for nested structure - */ - private array $stack = []; - - /** - * @inheritDoc - */ - public function open(string $path): static - { - $clone = clone $this; - $clone->stack = [['name' => '', 'data' => []]]; - return $clone; - } - - /** - * @inheritDoc - */ - public function startGroup(string $name): void - { - $this->stack[] = ['name' => $name, 'data' => []]; - } - - /** - * @inheritDoc - */ - public function endGroup(string $name): void - { - $frame = array_pop($this->stack); - if($frame === null) { - throw new \LogicException("Called endGroup() without matching startGroup()"); - } - - if ($frame['name'] !== $name) { - throw new \LogicException("Group name mismatch: expected end of '{$frame['name']}', got '{$name}'"); - } - $this->stack[array_key_last($this->stack)]['data'][$name] = $frame['data']; - } - - /** - * @inheritDoc - */ - public function group(string $name, callable $callback): void - { - $this->startGroup($name); - $callback(); - $this->endGroup($name); - } - - /** - * @inheritDoc - */ - public function append(string $name, array $data): void - { - $top = &$this->stack[array_key_last($this->stack)]; - - // First entry for this name: store it directly. - if (!array_key_exists($name, $top['data'])) { - $top['data'][$name] = $data; - return; - } - - $existing = $top['data'][$name]; - - // Multiple entries already exist: append to the list. - if (is_array($existing) && array_is_list($existing)) { - $top['data'][$name][] = $data; - return; - } - - // Second entry: convert the existing single entry into a list. - $top['data'][$name] = [$existing, $data]; - } - - /** - * @inheritDoc - */ - public function write(): string - { - if ($this->stack === []) { - return '{}'; - } - $root = $this->stack[0]['data']; - $json = json_encode($root, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - $this->stack = [['name' => '', 'data' => []]]; - return $json; - } -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLDeserializerTrait.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLDeserializerTrait.php deleted file mode 100644 index 58c814520177..000000000000 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLDeserializerTrait.php +++ /dev/null @@ -1,197 +0,0 @@ - */ - private array $handler = []; - - public function addHandler(string $group, callable $handler): void - { - $this->handler[$group] = $handler; - } - - private function processReader(\XMLReader $reader): void - { - while ($reader->read()) { - $node_name = $this->kebabToSnake($reader->name); - - if ($reader->nodeType !== \XMLReader::ELEMENT || !isset($this->handler[$node_name])) { - continue; - } - - $this->handler[$node_name]($this->readGroup($reader)); - } - - $reader->close(); - } - - /** - * @return list - */ - private function readGroup(\XMLReader $reader): array - { - if ($reader->isEmptyElement) { - return []; - } - - $group_depth = $reader->depth; - $group_name = $reader->name; - $group_data = []; - - while ($reader->read()) { - if ( - $reader->nodeType === \XMLReader::END_ELEMENT - && $reader->depth === $group_depth - && $reader->name === $group_name - ) { - break; - } - - if ( - $reader->nodeType !== \XMLReader::ELEMENT - || $reader->depth !== $group_depth + 1 - ) { - continue; - } - - $group_data[] = $this->readElementValue($reader); - } - - return $group_data; - } - - private function readElementValue(\XMLReader $reader): mixed - { - $is_marked_empty_array = $this->isMarkedEmptyArray($reader); - - if ($reader->isEmptyElement) { - return $is_marked_empty_array ? [] : ''; - } - - $element_depth = $reader->depth; - $element_name = $reader->name; - $children = []; - $text_content = ''; - - while ($reader->read()) { - if ( - $reader->nodeType === \XMLReader::END_ELEMENT - && $reader->depth === $element_depth - && $reader->name === $element_name - ) { - break; - } - - if ( - $reader->nodeType === \XMLReader::ELEMENT - && $reader->depth === $element_depth + 1 - ) { - $child_key = $this->resolveElementKey($reader); - $child_value = $this->readElementValue($reader); - - if ($child_key === null) { - $children[] = $child_value; - continue; - } - - $this->appendValue($children, $child_key, $child_value); - continue; - } - - if ( - $reader->depth === $element_depth + 1 - && in_array( - $reader->nodeType, - [ - \XMLReader::TEXT, - \XMLReader::CDATA, - \XMLReader::SIGNIFICANT_WHITESPACE - ], - true - ) - ) { - $text_content .= $reader->value; - } - } - - if ($children !== []) { - return $children; - } - - if ($is_marked_empty_array && trim($text_content) === '') { - return []; - } - - return $this->decodeScalarValue($text_content); - } - - private function isMarkedEmptyArray(\XMLReader $reader): bool - { - return $reader->getAttribute('type') === 'empty-array'; - } - - private function resolveElementKey(\XMLReader $reader): ?string - { - if ($reader->name !== 'item') { - return $this->kebabToSnake($reader->name); - } - - $raw_key = $reader->getAttribute('key'); - if ($raw_key === null || $raw_key === '') { - return null; - } - - return $this->kebabToSnake($raw_key); - } - - /** - * @param array $target - */ - private function appendValue(array &$target, string $key, mixed $value): void - { - if (!array_key_exists($key, $target)) { - $target[$key] = $value; - return; - } - - if (!is_array($target[$key]) || !array_is_list($target[$key])) { - $target[$key] = [$target[$key]]; - } - - $target[$key][] = $value; - } - - private function decodeScalarValue(string $value): mixed - { - $decoded = htmlspecialchars_decode($value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5); - return $decoded === 'NULL' ? null : $decoded; - } - - private function kebabToSnake(string $name): string - { - return str_replace('-', '_', $name); - } -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLFileDeserializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLFileDeserializer.php deleted file mode 100644 index c91cb66c19f0..000000000000 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLFileDeserializer.php +++ /dev/null @@ -1,66 +0,0 @@ -file_path = $path; - return $clone; - } - - /** - * @inheritDoc - */ - public function process(): void - { - if ($this->file_path === '') { - throw new \RuntimeException('No file has been opened. Call open() before process().'); - } - - $reader = new \XMLReader(); - - if (!$reader->open($this->file_path, null, LIBXML_NONET)) { - throw new \RuntimeException("Unable to open XML file '{$this->file_path}'."); - } - - $this->processReader($reader); - } -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLMemoryDeserializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLMemoryDeserializer.php deleted file mode 100644 index ab40dea5474e..000000000000 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XMLMemoryDeserializer.php +++ /dev/null @@ -1,65 +0,0 @@ -xml = $path; - return $clone; - } - - /** - * @inheritDoc - */ - public function process(): void - { - $reader = new \XMLReader(); - $xml = $this->prepareXmlInput($this->xml); - - if (!$reader->XML($xml, null, LIBXML_NONET)) { - throw new \RuntimeException('Unable to read XML input.'); - } - - $this->processReader($reader); - } - - private function prepareXmlInput(string $xml): string - { - $xml = trim($xml); - $xml = preg_replace('/^\s*<\?xml[^>]*\?>\s*/i', '', $xml) ?? $xml; - return "{$xml}"; - } -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XmlDeserializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XmlDeserializer.php new file mode 100644 index 000000000000..35f167504667 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XmlDeserializer.php @@ -0,0 +1,234 @@ + */ + private array $handlers = []; + + private function __construct(private readonly \XMLReader $reader) + { + } + + public static function fromFile(string $path): self + { + if (!is_file($path) || !is_readable($path)) { + throw new \InvalidArgumentException("The file '{$path}' does not exist or is not readable."); + } + + $reader = new \XMLReader(); + if (!$reader->open($path, null, LIBXML_NONET)) { + throw new \RuntimeException("Unable to open XML file '{$path}'."); + } + + return new self($reader); + } + + public static function fromString(string $xml): self + { + $xml = trim($xml); + $xml = preg_replace('/^\s*<\?xml[^>]*\?>\s*/i', '', $xml) ?? $xml; + + $reader = new \XMLReader(); + if (!$reader->XML("{$xml}", null, LIBXML_NONET)) { + throw new \RuntimeException('Unable to open XML input.'); + } + + return new self($reader); + } + + #[\Override] + public function addHandler(string $group, callable $handler): void + { + $this->handlers[$group] = $handler; + } + + #[\Override] + public function process(): void + { + try { + while ($this->reader->read()) { + $node_name = $this->kebabToSnake($this->reader->name); + + if ( + $this->reader->nodeType !== \XMLReader::ELEMENT + || !isset($this->handlers[$node_name]) + ) { + continue; + } + + $this->handlers[$node_name]($this->readGroup()); + } + } finally { + $this->reader->close(); + } + } + + /** + * @return list + */ + private function readGroup(): array + { + if ($this->reader->isEmptyElement) { + return []; + } + + $group_depth = $this->reader->depth; + $group_name = $this->reader->name; + $group_data = []; + + while ($this->reader->read()) { + if ( + $this->reader->nodeType === \XMLReader::END_ELEMENT + && $this->reader->depth === $group_depth + && $this->reader->name === $group_name + ) { + break; + } + + if ( + $this->reader->nodeType !== \XMLReader::ELEMENT + || $this->reader->depth !== $group_depth + 1 + ) { + continue; + } + + $group_data[] = $this->readElementValue(); + } + + return $group_data; + } + + private function readElementValue(): mixed + { + $is_marked_empty_array = $this->isMarkedEmptyArray(); + + if ($this->reader->isEmptyElement) { + return $is_marked_empty_array ? [] : ''; + } + + $element_depth = $this->reader->depth; + $element_name = $this->reader->name; + $children = []; + $text_content = ''; + + while ($this->reader->read()) { + if ( + $this->reader->nodeType === \XMLReader::END_ELEMENT + && $this->reader->depth === $element_depth + && $this->reader->name === $element_name + ) { + break; + } + + if ( + $this->reader->nodeType === \XMLReader::ELEMENT + && $this->reader->depth === $element_depth + 1 + ) { + $child_key = $this->resolveElementKey(); + $child_value = $this->readElementValue(); + + if ($child_key === null) { + $children[] = $child_value; + continue; + } + + $this->appendValue($children, $child_key, $child_value); + continue; + } + + if ( + $this->reader->depth === $element_depth + 1 + && in_array( + $this->reader->nodeType, + [ + \XMLReader::TEXT, + \XMLReader::CDATA, + \XMLReader::SIGNIFICANT_WHITESPACE + ], + true + ) + ) { + $text_content .= $this->reader->value; + } + } + + if ($children !== []) { + return $children; + } + + if ($is_marked_empty_array && trim($text_content) === '') { + return []; + } + + return $this->decodeScalarValue($text_content); + } + + private function isMarkedEmptyArray(): bool + { + return $this->reader->getAttribute('type') === 'empty-array'; + } + + private function resolveElementKey(): ?string + { + if ($this->reader->name !== 'item') { + return $this->kebabToSnake($this->reader->name); + } + + $raw_key = $this->reader->getAttribute('key'); + if ($raw_key === null || $raw_key === '') { + return null; + } + + return $this->kebabToSnake($raw_key); + } + + /** + * @param array $target + */ + private function appendValue(array &$target, string $key, mixed $value): void + { + if (!array_key_exists($key, $target)) { + $target[$key] = $value; + return; + } + + if (!is_array($target[$key]) || !array_is_list($target[$key])) { + $target[$key] = [$target[$key]]; + } + + $target[$key][] = $value; + } + + private function decodeScalarValue(string $value): mixed + { + $decoded = htmlspecialchars_decode($value, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5); + return $decoded === 'NULL' ? null : $decoded; + } + + private function kebabToSnake(string $name): string + { + return str_replace('-', '_', $name); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XmlSerializer.php similarity index 79% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XmlSerializer.php index e331a7232ac9..6d58b79a1580 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/SimpleXMLSerializer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Serializing/XmlSerializer.php @@ -22,39 +22,24 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Serializer; -/** - * Simple serializer that creates an XML string in memory. Due to its simplicity, it is not suitable for large datasets. - */ -class SimpleXMLSerializer implements Serializer +final class XmlSerializer implements Serializer { - private readonly \XMLWriter $writer; - private bool $has_document = false; - private string $current_group = ''; - public function __construct() + private function __construct(private readonly \XMLWriter $writer) { - $this->writer = new \XMLWriter(); } - /** - * @inheritDoc - */ - public function open(string $path): static + public static function inMemory(): self { - $clone = clone $this; - $clone->writer->openMemory(); - $clone->writer->setIndent(true); - return $clone; + $writer = new \XMLWriter(); + $writer->openMemory(); + $writer->setIndent(true); + + return new self($writer); } - /** - * Start a new xml document in the current writer. if a document has already been started, an exception will be - * thrown. - * - * @throws \LogicException if a document has already been started - */ public function createDocument(string $comment): void { if ($this->has_document) { @@ -66,19 +51,14 @@ public function createDocument(string $comment): void $this->has_document = true; } - - /** - * @inheritDoc - */ + #[\Override] public function startGroup(string $name): void { $this->current_group = $name; $this->writer->startElement($this->formatName($name)); } - /** - * @inheritDoc - */ + #[\Override] public function endGroup(string $name): void { if ($this->current_group !== $name) { @@ -89,9 +69,7 @@ public function endGroup(string $name): void $this->writer->endElement(); } - /** - * @inheritDoc - */ + #[\Override] public function group(string $name, callable $callback): void { $this->startGroup($name); @@ -99,9 +77,7 @@ public function group(string $name, callable $callback): void $this->endGroup($name); } - /** - * @inheritDoc - */ + #[\Override] public function append(string $name, array $data): void { $this->writer->startElement($this->formatName($name)); @@ -113,9 +89,7 @@ public function append(string $name, array $data): void $this->writer->endElement(); } - /** - * @inheritDoc - */ + #[\Override] public function write(): string { if ($this->has_document) { @@ -145,15 +119,13 @@ private function appendRecursive(array $data): void } if (!$is_nested) { - $value = match (gettype($value)) { + $this->writer->writeRaw(match (gettype($value)) { 'NULL' => 'NULL', 'integer' => (string) $value, 'float' => (string) $value, 'boolean' => $value ? '1' : '0', default => htmlspecialchars((string) $value), - }; - - $this->writer->writeRaw($value); + }); } else { if ($value === []) { $this->writer->writeAttribute('type', 'empty-array'); @@ -181,7 +153,6 @@ private function isValidXmlElementName(string $name): bool private function formatName(int|string $name): string { - // Transform key to kebab-case $output = strtolower(preg_replace('/(?open( + $deserializer = XmlDeserializer::fromFile( $context->get(UploadValidationStage::COMPONENT_IMPORT_FILE) ); diff --git a/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php b/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php index 63a502f4a1fc..b5f59b97cebd 100644 --- a/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php +++ b/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php @@ -25,9 +25,8 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\SimpleXMLSerializer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XMLFileDeserializer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XMLMemoryDeserializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlDeserializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlSerializer; use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; use ILIAS\TestQuestionPool\QuestionPoolDIC; @@ -127,7 +126,7 @@ public function testSingleChoiceRoundtripPreservesQuestionAndCollectsImages(): v public function testXmlStringAndFileDeserializersProduceTheSameData(): void { - $serializer = (new SimpleXMLSerializer())->open(''); + $serializer = XmlSerializer::inMemory(); $serializer->createDocument('Export/import characterization'); $serializer->group('sample_group', function () use ($serializer): void { $serializer->append('sample', [ @@ -146,11 +145,11 @@ public function testXmlStringAndFileDeserializersProduceTheSameData(): void try { $from_string = $this->deserializeGroup( - (new XMLMemoryDeserializer())->open($xml), + XmlDeserializer::fromString($xml), 'sample_group' ); $from_file = $this->deserializeGroup( - (new XMLFileDeserializer())->open($path), + XmlDeserializer::fromFile($path), 'sample_group' ); } finally { @@ -171,7 +170,7 @@ public function testXmlStringAndFileDeserializersProduceTheSameData(): void public function testXmlMemoryDeserializerProcessesDocumentFragments(): void { $groups = []; - $deserializer = (new XMLMemoryDeserializer())->open( + $deserializer = XmlDeserializer::fromString( '' . 'first' . 'second' @@ -198,12 +197,12 @@ public function testXmlFileDeserializerRejectsMissingFiles(): void { $this->expectException(InvalidArgumentException::class); - (new XMLFileDeserializer())->open(__DIR__ . '/missing.xml'); + XmlDeserializer::fromFile(__DIR__ . '/missing.xml'); } public function testXmlSerializerRejectsMismatchedGroups(): void { - $serializer = (new SimpleXMLSerializer())->open(''); + $serializer = XmlSerializer::inMemory(); $serializer->startGroup('expected'); $this->expectException(LogicException::class); From 77cf5df492f4e530e2baad47a34630712c294cb7 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Mon, 21 Sep 2026 15:25:44 +0200 Subject: [PATCH 54/66] refactor: persist import context as json Replace the string-key session bag and PHP serialize so wizard state is a typed optional value object and unknown or leftover payloads start empty. --- .../ILIAS/Test/classes/class.ilObjTestGUI.php | 6 +- .../Test/classes/class.ilTestImporter.php | 5 +- .../classes/class.ilTestLegacyImporter.php | 24 +- .../src/ExportImport/Import/PersistStage.php | 22 +- .../src/ExportImport/Import/TestImporter.php | 32 +- .../classes/class.ilObjQuestionPoolGUI.php | 4 +- .../class.ilTestQuestionPoolImporter.php | 5 +- ...class.ilTestQuestionPoolLegacyImporter.php | 22 +- .../Foundation/Importing/ImportContext.php | 352 +++++++++++++++++- .../Importing/ImportSessionRepository.php | 16 +- .../src/ExportImport/Import/CleanupStage.php | 20 +- .../Import/DetectLegacyImportStage.php | 17 +- .../src/ExportImport/Import/PersistStage.php | 4 +- .../Import/QuestionPoolImporter.php | 16 +- .../Import/QuestionSelectionStage.php | 41 +- .../ExportImport/Import/QuestionsImporter.php | 2 +- .../Import/UploadValidationStage.php | 32 +- .../ExportImportCharacterizationTest.php | 130 ++++++- 18 files changed, 571 insertions(+), 179 deletions(-) diff --git a/components/ILIAS/Test/classes/class.ilObjTestGUI.php b/components/ILIAS/Test/classes/class.ilObjTestGUI.php index 1c9fd1a9fe13..ed8ee9160c6b 100755 --- a/components/ILIAS/Test/classes/class.ilObjTestGUI.php +++ b/components/ILIAS/Test/classes/class.ilObjTestGUI.php @@ -1394,7 +1394,7 @@ protected function importFile(string $file_to_import, string $path_to_uploaded_f { $this->import_session_repository->clear(); - $context = new ImportContext([UploadValidationStage::FILE_TO_IMPORT => $file_to_import]); + $context = (new ImportContext())->withFileToImport($file_to_import); $this->import_session_repository->setContext($context); $this->import_session_repository->setCurrentStageIndex(0); @@ -1439,8 +1439,8 @@ public function processImportObject(): void private function afterImportCompleted(ImportContext $context): void { $new_obj = new ilObjTest(0, false); - $new_obj->setId($context->get('test_obj_id')); - $new_obj->setRefId($context->get('test_ref_id')); + $new_obj->setId($context->testObjId() ?? 0); + $new_obj->setRefId($context->testRefId() ?? 0); if ($new_obj->getTestLogger()->isLoggingEnabled()) { $new_obj->getTestLogger()->logTestAdministrationInteraction( diff --git a/components/ILIAS/Test/classes/class.ilTestImporter.php b/components/ILIAS/Test/classes/class.ilTestImporter.php index 33ada2d1d317..4f8d1bffa700 100755 --- a/components/ILIAS/Test/classes/class.ilTestImporter.php +++ b/components/ILIAS/Test/classes/class.ilTestImporter.php @@ -23,7 +23,6 @@ use ILIAS\Test\TestDIC; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlDeserializer; -use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; class ilTestImporter extends ilXmlImporter { @@ -61,7 +60,7 @@ public function importXmlRepresentation( ): void { // Check if forward to legacy importer is needed $context = $this->session->getContext(); - if (DetectLegacyImportStage::isLegacyImport($context)) { + if ($context->isLegacyImport()) { $this->legacy_importer->setInstallId($this->getInstallId()); $this->legacy_importer->setInstallUrl($this->getInstallUrl()); $this->legacy_importer->setSchemaVersion($this->getSchemaVersion()); @@ -83,7 +82,7 @@ public function finalProcessing(ilImportMapping $a_mapping): void { // Check if forward to legacy importer is needed $context = $this->session->getContext(); - if (DetectLegacyImportStage::isLegacyImport($context)) { + if ($context->isLegacyImport()) { $this->legacy_importer->finalProcessing($a_mapping); return; } diff --git a/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php b/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php index 5a7f54645879..a7809d6513f3 100755 --- a/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php +++ b/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php @@ -21,8 +21,6 @@ use ILIAS\ResourceStorage\Services as ResourceStorage; use ILIAS\Test\RequestDataCollector; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; -use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; -use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; use ILIAS\TestQuestionPool\Import\TestQuestionsImportTrait; use ILIAS\Test\TestDIC; use ILIAS\Test\Logging\TestLogger; @@ -75,16 +73,14 @@ public function importXmlRepresentation( $a_mapping->addMapping('components/ILIAS/Test', 'tst', 'new_id', (string) $new_obj->getId()); $context = $this->session->getContext(); - $import_base_dir = $context->get(UploadValidationStage::IMPORT_BASE_DIR); - $xml_file = $context->get(DetectLegacyImportStage::LEGACY_XML_FILE); // start parsing of QTI files $qti_parser = new ilQTIParser( - $import_base_dir, - $context->get(DetectLegacyImportStage::LEGACY_QTI_FILE), + $context->importBaseDir(), + $context->legacyQtiFile(), ilQTIParser::IL_MO_PARSE_QTI, $new_obj->getId(), - $context->get('selected_questions'), + $context->selectedQuestionIds(), $a_mapping->getAllMappings() ); $qti_parser->setTestObject($new_obj); @@ -94,8 +90,8 @@ public function importXmlRepresentation( // import page data $question_page_parser = new ilQuestionPageParser( $new_obj, - $xml_file, - $import_base_dir + $context->legacyXmlFile(), + $context->importBaseDir() ); $question_page_parser->setQuestionMapping($qti_parser->getImportMapping()); $question_page_parser->startParsing(); @@ -103,10 +99,10 @@ public function importXmlRepresentation( $a_mapping = $this->addTaxonomyAndQuestionsMapping($qti_parser->getQuestionIdMapping(), $new_obj->getId(), $a_mapping); if ($new_obj->isRandomTest()) { - $this->importRandomQuestionSetConfig($new_obj, $xml_file, $a_mapping); + $this->importRandomQuestionSetConfig($new_obj, $context->legacyXmlFile(), $a_mapping); } - $results_file_path = str_replace('__tst', '__results', $xml_file); + $results_file_path = str_replace('__tst', '__results', $context->legacyXmlFile()); if (file_exists($results_file_path)) { $results = new ilTestResultsImportParser($results_file_path, $new_obj, $this->db, $this->logger, $this->irss); $results->setQuestionIdMapping($a_mapping->getMappingsOfEntity('components/ILIAS/Test', 'quest')); @@ -119,9 +115,9 @@ public function importXmlRepresentation( $this->importSkillLevelThresholds( $a_mapping, - $this->importQuestionSkillAssignments($a_mapping, $new_obj, $xml_file), + $this->importQuestionSkillAssignments($a_mapping, $new_obj, $context->legacyXmlFile()), $new_obj, - $xml_file + $context->legacyXmlFile() ); $a_mapping->addMapping( @@ -131,7 +127,7 @@ public function importXmlRepresentation( "{$new_obj->getId()}:0:tst" ); - $context = $context->with('test_obj_id', $new_obj->getId())->with('test_ref_id', $new_obj->getRefId()); + $context = $context->withTestObjId($new_obj->getId())->withTestRefId($new_obj->getRefId()); $this->session->setContext($context); } diff --git a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php index 177d0d7398b7..9c54fe3f65e3 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php +++ b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php @@ -26,8 +26,6 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResult; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlDeserializer; -use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; -use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; use ilImport; use Psr\Log\LoggerInterface; @@ -63,7 +61,7 @@ public function getDescription(): ?string public function process(ImportContext $context): StageResult { - if (!DetectLegacyImportStage::isLegacyImport($context)) { + if (!$context->isLegacyImport()) { if ($result = $this->importMappingsFile($context)) { return $result; } @@ -71,8 +69,8 @@ public function process(ImportContext $context): StageResult (new ilImport($this->requested_ref_id))->importObject( null, - $context->get(UploadValidationStage::FILE_TO_IMPORT), - basename($context->get(UploadValidationStage::FILE_TO_IMPORT)), + $context->fileToImport(), + basename($context->fileToImport()), 'tst', 'components/ILIAS/Test', true, @@ -84,7 +82,7 @@ public function process(ImportContext $context): StageResult private function importMappingsFile(ImportContext $context): ?StageResult { - $component_import_dir = dirname($context->get(UploadValidationStage::COMPONENT_IMPORT_FILE)); + $component_import_dir = dirname($context->componentImportFile()); $mappings_file = "{$component_import_dir}/mappings.xml"; if (!is_file($mappings_file)) { $this->log->error("Mappings file not found: {$mappings_file}"); @@ -92,8 +90,16 @@ private function importMappingsFile(ImportContext $context): ?StageResult } $deserializer = XmlDeserializer::fromFile($mappings_file); - $deserializer->addHandler('mappings', function (array $mappings) use (&$context) { - $context = $context->with('mappings', $mappings); + $deserializer->addHandler('mappings', function (array $mappings) use (&$context): void { + $user_mappings = $mappings[0] ?? null; + $resource_mappings = $mappings[1] ?? []; + if (!is_array($user_mappings) || !is_array($resource_mappings)) { + return; + } + + $context = $context + ->withUserMappings($user_mappings) + ->withResourceMappings($resource_mappings); }); $deserializer->process(); diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php index 9bc51891b6d5..cc5fef4b03af 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php @@ -42,16 +42,13 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\CollectResources; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\IdMappingPipe; -use ILIAS\TestQuestionPool\ExportImport\Import\QuestionSelectionStage; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionsImporter; use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter; -use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; use ilImportMapping; use ilObjTest; use ilTestPage; use Psr\Log\LoggerInterface; -use RuntimeException; /** * Orchestrates the import of a test. It uses the Builder to create a pipeline of transformations that are used to normalize @@ -150,11 +147,11 @@ function (array $normalized) use ($tt, $mapping, $context, &$test_object): void function (array $assignments) use ($tt, $mapping, &$context): void { $result = $this->skill_importer->import( $assignments, - UploadValidationStage::getInstallId($context), + $context->installId(), $tt, $mapping, ); - $context = $context->with('skill_assignments', $result); + $context = $context->withSkillAssignments($result); } ); @@ -163,11 +160,11 @@ function (array $assignments) use ($tt, $mapping, &$context): void { function (array $thresholds) use ($tt, $mapping, &$context): void { $result = $this->skill_thresholds_importer->import( $thresholds, - UploadValidationStage::getInstallId($context), + $context->installId(), $tt, $mapping, ); - $context = $context->with('skill_thresholds', $result); + $context = $context->withSkillThresholds($result); } ); @@ -220,7 +217,7 @@ function (array $times) use ($tt): void { $this->log->info('...Finished importing question images'); $this->log->info("Finished importing test {$test_object->getTestId()} (Test ID), {$test_object->getId()} (Object ID)"); - return $context->with('test_obj_id', $test_object->getId())->with('test_ref_id', $test_object->getRefId()); + return $context->withTestObjId($test_object->getId())->withTestRefId($test_object->getRefId()); } /** @@ -242,11 +239,7 @@ private function importMappings( CollectResources $resource_pipe, ImportContext $context ): void { - $mappings = $context->get('mappings'); - if (count($mappings) < 2) { - throw new RuntimeException('Invalid mappings: Expected at least 2 mappings, got ' . count($mappings)); - } - [$user_mapping, $resource_mapping] = $mappings; + $user_mapping = $context->userMappings(); $this->log->info('Importing user mappings...'); $user_resolver = new UserImportResolver($this->database, $this->log); @@ -258,8 +251,8 @@ private function importMappings( $this->log->info('...Finished importing user mappings'); $this->log->info('Importing resources and storing mappings...'); - $import_dir = dirname($context->get(UploadValidationStage::COMPONENT_IMPORT_FILE)) . '/expDir_1'; - foreach ($resource_mapping as $resource) { + $import_dir = dirname($context->componentImportFile()) . '/expDir_1'; + foreach ($context->resourceMappings() as $resource) { $clean_id = str_replace(['-', '_'], '', $resource['id']); $resource_path = "$import_dir/resources/{$clean_id}.{$resource['suffix']}"; if (!file_exists($resource_path)) { @@ -363,10 +356,13 @@ private function importQuestions( ImportContext $context, ilObjTest $test_object ): void { - $selected_questions = QuestionSelectionStage::getSelectedQuestions($context); - foreach ($list as $normalized) { - $question = $this->questions_importer->importQuestion($normalized, $tt, $mapping, $selected_questions); + $question = $this->questions_importer->importQuestion( + $normalized, + $tt, + $mapping, + $context->selectedQuestionIds() + ); if (!$question instanceof \assQuestion || $normalized['sequence'] === null) { continue; } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php index 8d9e39b766b5..cb143f917dd5 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php @@ -1120,7 +1120,7 @@ protected function importFile(string $file_to_import, string $path_to_uploaded_f { $this->import_session_repository->clear(); - $context = new ImportContext([UploadValidationStage::FILE_TO_IMPORT => $file_to_import]); + $context = (new ImportContext())->withFileToImport($file_to_import); $this->import_session_repository->setContext($context); $this->import_session_repository->setCurrentStageIndex(0); @@ -1198,7 +1198,7 @@ private function renderImportError(StageResult $result): void private function renderImportSuccess(StageResult $result): void { - $pool_obj_id = $this->data_factory->objId($result->context->get('pool_obj_id')); + $pool_obj_id = $this->data_factory->objId($result->context->poolObjId()); $pool_ref_id = $pool_obj_id->toReferenceIds()[0]->toInt(); $this->tpl->setOnScreenMessage( diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php index 3cd0765b9d8b..e5a843f18a86 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php @@ -21,7 +21,6 @@ use ILIAS\Data\ReferenceId; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlDeserializer; -use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionPoolImporter; use ILIAS\TestQuestionPool\QuestionPoolDIC; @@ -56,7 +55,7 @@ public function importXmlRepresentation( ): void { // Check if forward to legacy importer is needed $context = $this->session->getContext(); - if (DetectLegacyImportStage::isLegacyImport($context)) { + if ($context->isLegacyImport()) { $this->legacy_importer->setInstallId($this->getInstallId()); $this->legacy_importer->setInstallUrl($this->getInstallUrl()); $this->legacy_importer->setSchemaVersion($this->getSchemaVersion()); @@ -78,7 +77,7 @@ public function finalProcessing(ilImportMapping $a_mapping): void { // Check if forward to legacy importer is needed $context = $this->session->getContext(); - if (DetectLegacyImportStage::isLegacyImport($context)) { + if ($context->isLegacyImport()) { $this->legacy_importer->finalProcessing($a_mapping); return; } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php index f4f09b48a77d..5cfcc3e35662 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php @@ -19,8 +19,6 @@ declare(strict_types=1); use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; -use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; -use ILIAS\TestQuestionPool\ExportImport\Import\UploadValidationStage; use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\TestQuestionPool\RequestDataCollector; @@ -57,12 +55,10 @@ public function importXmlRepresentation( $a_mapping->addMapping('components/ILIAS/TestQuestionPool', 'qpl', $a_id, (string) $this->pool_obj->getId()); $context = $this->session->getContext(); - $import_base_dir = $context->get(UploadValidationStage::IMPORT_BASE_DIR); - $xml_file = $context->get(DetectLegacyImportStage::LEGACY_XML_FILE); - $context = $context->with('pool_obj_id', $this->pool_obj->getId()); + $context = $context->withPoolObjId($this->pool_obj->getId()); $this->session->setContext($context); - $qpl_parser = new ilObjQuestionPoolXMLParser($this->pool_obj, $xml_file); + $qpl_parser = new ilObjQuestionPoolXMLParser($this->pool_obj, $context->legacyXmlFile()); $qpl_parser->startParsing(); // set another question pool name (if possible) @@ -75,15 +71,19 @@ public function importXmlRepresentation( $this->pool_obj->saveToDb(); $qti_parser = new ilQTIParser( - $import_base_dir, - $context->get(DetectLegacyImportStage::LEGACY_QTI_FILE), + $context->importBaseDir(), + $context->legacyQtiFile(), ilQTIParser::IL_MO_PARSE_QTI, $this->pool_obj->getId(), - $context->get('selected_questions') + $context->selectedQuestionIds() ); $qti_parser->startParsing(); - $page_parser = new ilQuestionPageParser($this->pool_obj, $xml_file, $import_base_dir); + $page_parser = new ilQuestionPageParser( + $this->pool_obj, + $context->legacyXmlFile(), + $context->importBaseDir() + ); $page_parser->setQuestionMapping($qti_parser->getImportMapping()); $page_parser->startParsing(); @@ -113,7 +113,7 @@ public function importXmlRepresentation( ); } - $this->importQuestionSkillAssignments($xml_file, $a_mapping, $this->pool_obj->getId()); + $this->importQuestionSkillAssignments($context->legacyXmlFile(), $a_mapping, $this->pool_obj->getId()); $a_mapping->addMapping( 'components/ILIAS/MetaData', diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportContext.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportContext.php index b609f75e2679..3c5570f5ea4b 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportContext.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportContext.php @@ -21,31 +21,357 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Importing; /** - * Immutable value object that accumulates data across import stages. Each stage receives the current context and - * returns an updated copy via the with() method. + * Immutable JSON-capable import context shared by question pool and test wizards. + * + * @phpstan-type SkillAssignmentResult array{failed: list, success: list} + * @phpstan-type SkillThresholdResult array{failed: list, success: list} */ -class ImportContext +final class ImportContext { - /** @param array> $data */ - public function __construct( - private array $data = [], - ) { + private ?string $file_to_import = null; + private ?string $component_import_file = null; + private ?string $import_base_dir = null; + private ?int $install_id = null; + private ?string $legacy_qti_file = null; + private ?string $legacy_xml_file = null; + /** @var list */ + private array $selectable_question_ids = []; + /** @var list */ + private array $selected_question_ids = []; + private ?int $pool_obj_id = null; + private ?int $test_obj_id = null; + private ?int $test_ref_id = null; + /** @var array|null */ + private ?array $user_mappings = null; + /** @var list> */ + private array $resource_mappings = []; + /** @var SkillAssignmentResult|null */ + private ?array $skill_assignments = null; + /** @var SkillThresholdResult|null */ + private ?array $skill_thresholds = null; + + public function withFileToImport(string $path): self + { + $clone = clone $this; + $clone->file_to_import = $path; + return $clone; + } + + public function hasFileToImport(): bool + { + return $this->file_to_import !== null; + } + + public function fileToImport(): string + { + if($this->file_to_import === null) { + throw new \LogicException('File to import is not set'); + } + + return $this->file_to_import; + } + + public function withComponentImportFile(string $path): self + { + $clone = clone $this; + $clone->component_import_file = $path; + return $clone; + } + + public function componentImportFile(): string + { + if($this->component_import_file === null) { + throw new \LogicException('Component import file is not set'); + } + + return $this->component_import_file; + } + + public function withImportBaseDir(string $path): self + { + $clone = clone $this; + $clone->import_base_dir = $path; + return $clone; + } + + public function hasImportBaseDir(): bool + { + return $this->import_base_dir !== null; + } + + public function importBaseDir(): string + { + if($this->import_base_dir === null) { + throw new \LogicException('Import base dir is not set'); + } + + return $this->import_base_dir; + } + + public function withInstallId(int $install_id): self + { + $clone = clone $this; + $clone->install_id = $install_id; + return $clone; + } + + public function installId(): int + { + if($this->install_id === null) { + throw new \LogicException('Install id is not set'); + } + + return $this->install_id; + } + + public function withLegacyQtiFile(string $path): self + { + $clone = clone $this; + $clone->legacy_qti_file = $path; + return $clone; + } + + public function legacyQtiFile(): string + { + if($this->legacy_qti_file === null) { + throw new \LogicException('Legacy QTI file is not set'); + } + + return $this->legacy_qti_file; + } + + public function withLegacyXmlFile(string $path): self + { + $clone = clone $this; + $clone->legacy_xml_file = $path; + return $clone; + } + + public function legacyXmlFile(): string + { + if($this->legacy_xml_file === null) { + throw new \LogicException('Legacy XML file is not set'); + } + + return $this->legacy_xml_file; + } + + /** + * @param list $ids + */ + public function withSelectableQuestionIds(array $ids): self + { + $clone = clone $this; + $clone->selectable_question_ids = $ids; + return $clone; + } + + /** + * @return list + */ + public function selectableQuestionIds(): array + { + return $this->selectable_question_ids; + } + + /** + * @param list $ids + */ + public function withSelectedQuestionIds(array $ids): self + { + $clone = clone $this; + $clone->selected_question_ids = $ids; + return $clone; + } + + /** + * @return list + */ + public function selectedQuestionIds(): array + { + return $this->selected_question_ids; + } + + public function withPoolObjId(int $pool_obj_id): self + { + $clone = clone $this; + $clone->pool_obj_id = $pool_obj_id; + return $clone; } - public function with(string $key, mixed $value): self + public function poolObjId(): int + { + if($this->pool_obj_id === null) { + throw new \LogicException('Pool object id is not set'); + } + + return $this->pool_obj_id; + } + + public function withTestObjId(int $test_obj_id): self { $clone = clone $this; - $clone->data[$key] = $value; + $clone->test_obj_id = $test_obj_id; return $clone; } - public function get(string $key, mixed $default = null): mixed + public function testObjId(): ?int { - return $this->data[$key] ?? $default; + if($this->test_obj_id === null) { + throw new \LogicException('Test object id is not set'); + } + + return $this->test_obj_id; + } + + public function withTestRefId(int $test_ref_id): self + { + $clone = clone $this; + $clone->test_ref_id = $test_ref_id; + return $clone; + } + + public function testRefId(): int + { + if($this->test_ref_id === null) { + throw new \LogicException('Test reference id is not set'); + } + + return $this->test_ref_id; + } + + /** + * @param array $mappings + */ + public function withUserMappings(array $mappings): self + { + $clone = clone $this; + $clone->user_mappings = $mappings; + return $clone; + } + + /** + * @return array + */ + public function userMappings(): array + { + if($this->user_mappings === null) { + throw new \LogicException('User mappings are not set'); + } + + return $this->user_mappings; } - public function has(string $key): bool + /** + * @param list>|array $mappings + */ + public function withResourceMappings(array $mappings): self { - return array_key_exists($key, $this->data); + $clone = clone $this; + $clone->resource_mappings = $mappings; + return $clone; + } + + /** + * @return list> + */ + public function resourceMappings(): array + { + return $this->resource_mappings; + } + + /** + * @param SkillAssignmentResult $result + */ + public function withSkillAssignments(array $result): self + { + $clone = clone $this; + $clone->skill_assignments = $result; + return $clone; + } + + /** + * @return SkillAssignmentResult + */ + public function skillAssignments(): array + { + if($this->skill_assignments === null) { + throw new \LogicException('Skill assignments are not set'); + } + + return $this->skill_assignments; + } + + /** + * @param SkillThresholdResult $result + */ + public function withSkillThresholds(array $result): self + { + $clone = clone $this; + $clone->skill_thresholds = $result; + return $clone; + } + + /** + * @return SkillThresholdResult + */ + public function skillThresholds(): array + { + if($this->skill_thresholds === null) { + throw new \LogicException('Skill thresholds are not set'); + } + + return $this->skill_thresholds; + } + + public function isLegacyImport(): bool + { + return $this->legacy_qti_file !== null && $this->legacy_xml_file !== null; + } + + public static function fromJson(string $json): self + { + $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); + + $context = new self(); + $context->file_to_import = $data['file_to_import'] ?? null; + $context->component_import_file = $data['component_import_file'] ?? null; + $context->import_base_dir = $data['import_base_dir'] ?? null; + $context->install_id = $data['install_id'] ?? null; + $context->legacy_qti_file = $data['legacy_qti_file'] ?? null; + $context->legacy_xml_file = $data['legacy_xml_file'] ?? null; + $context->selectable_question_ids = $data['selectable_question_ids'] ?? []; + $context->selected_question_ids = $data['selected_question_ids'] ?? []; + $context->pool_obj_id = $data['pool_obj_id'] ?? null; + $context->test_obj_id = $data['test_obj_id'] ?? null; + $context->test_ref_id = $data['test_ref_id'] ?? null; + $context->user_mappings = $data['user_mappings'] ?? null; + $context->resource_mappings = $data['resource_mappings'] ?? []; + $context->skill_assignments = $data['skill_assignments'] ?? null; + $context->skill_thresholds = $data['skill_thresholds'] ?? null; + return $context; + } + + public function toJson(): string + { + $payload = [ + 'file_to_import' => $this->file_to_import, + 'component_import_file' => $this->component_import_file, + 'import_base_dir' => $this->import_base_dir, + 'install_id' => $this->install_id, + 'legacy_qti_file' => $this->legacy_qti_file, + 'legacy_xml_file' => $this->legacy_xml_file, + 'selectable_question_ids' => $this->selectable_question_ids, + 'selected_question_ids' => $this->selected_question_ids, + 'pool_obj_id' => $this->pool_obj_id, + 'test_obj_id' => $this->test_obj_id, + 'test_ref_id' => $this->test_ref_id, + 'user_mappings' => $this->user_mappings, + 'resource_mappings' => $this->resource_mappings, + 'skill_assignments' => $this->skill_assignments, + 'skill_thresholds' => $this->skill_thresholds, + ]; + + return json_encode($payload, JSON_THROW_ON_ERROR); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportSessionRepository.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportSessionRepository.php index ed18fdc70859..d34eb053844b 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportSessionRepository.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Importing/ImportSessionRepository.php @@ -51,19 +51,21 @@ public function setCurrentStageIndex(int $index): void public function getContext(): ImportContext { - if (\ilSession::has($this->key_context)) { - return unserialize( - \ilSession::get($this->key_context), - ['allowed_classes' => [ImportContext::class]] - ); + if (!\ilSession::has($this->key_context)) { + return new ImportContext(); } - return new ImportContext([]); + $payload = \ilSession::get($this->key_context); + if (!is_string($payload)) { + return new ImportContext(); + } + + return ImportContext::fromJson($payload); } public function setContext(ImportContext $context): void { - \ilSession::set($this->key_context, serialize($context)); + \ilSession::set($this->key_context, $context->toJson()); } public function clear(): void diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php index 0f9869c21868..cb9bcadbfe5b 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php @@ -31,9 +31,7 @@ */ class CleanupStage implements ImportStage { - public function __construct( - private readonly LoggerInterface $log, - ) { + public function __construct(private readonly LoggerInterface $log) { } public function getIdentifier(): string @@ -53,10 +51,9 @@ public function getDescription(): ?string public function process(ImportContext $context): StageResult { - $file_to_import = $context->get(UploadValidationStage::FILE_TO_IMPORT); - if ($file_to_import !== null) { - $temp_dir = dirname($file_to_import); - if ($temp_dir && file_exists($temp_dir) && is_dir($temp_dir)) { + if ($context->hasFileToImport()) { + $temp_dir = dirname($context->fileToImport()); + if ($temp_dir !== '' && file_exists($temp_dir) && is_dir($temp_dir)) { $this->removeDirectory($temp_dir); $this->log->info("Removed temporary import directory: {$temp_dir}"); } else { @@ -64,12 +61,11 @@ public function process(ImportContext $context): StageResult } } - $import_base_dir = $context->get(UploadValidationStage::IMPORT_BASE_DIR); - if ($import_base_dir && file_exists($import_base_dir) && is_dir($import_base_dir)) { - $this->removeDirectory($import_base_dir); - $this->log->info("Removed import target base directory: {$import_base_dir}"); + if ($context->hasImportBaseDir() && is_dir($context->importBaseDir())) { + $this->removeDirectory($context->importBaseDir()); + $this->log->info("Removed import target base directory: {$context->importBaseDir()}"); } else { - $this->log->warning("Import target base directory does not exist: {$import_base_dir}"); + $this->log->warning("Import target base directory does not exist: {$context->importBaseDir()}"); } return StageResult::complete($context); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php index ff163bf3c26a..f7f398919dc6 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php @@ -30,9 +30,6 @@ */ class DetectLegacyImportStage implements ImportStage { - public const string LEGACY_QTI_FILE = 'legacy_qti_file'; - public const string LEGACY_XML_FILE = 'legacy_xml_file'; - public function __construct( private readonly LoggerInterface $log, ) { @@ -55,11 +52,10 @@ public function getDescription(): ?string public function process(ImportContext $context): StageResult { - $import_base_dir = $context->get(UploadValidationStage::IMPORT_BASE_DIR); - $import_name = basename($import_base_dir); + $import_name = basename($context->importBaseDir()); - $xml_file = $import_base_dir . DIRECTORY_SEPARATOR . "{$import_name}.xml"; - $qti_file = $import_base_dir . DIRECTORY_SEPARATOR . str_replace(['_qpl_', '_tst_'], '_qti_', $import_name) . '.xml'; + $xml_file = $context->importBaseDir() . DIRECTORY_SEPARATOR . "{$import_name}.xml"; + $qti_file = $context->importBaseDir() . DIRECTORY_SEPARATOR . str_replace(['_qpl_', '_tst_'], '_qti_', $import_name) . '.xml'; if (!file_exists($qti_file) || !file_exists($xml_file)) { $this->log->debug("No legacy import files found for {$import_name}"); @@ -68,12 +64,7 @@ public function process(ImportContext $context): StageResult $this->log->info("Detected legacy import files for {$import_name}"); return StageResult::advance( - $context->with(self::LEGACY_QTI_FILE, $qti_file)->with(self::LEGACY_XML_FILE, $xml_file) + $context->withLegacyQtiFile($qti_file)->withLegacyXmlFile($xml_file) ); } - - public static function isLegacyImport(ImportContext $context): bool - { - return $context->has(self::LEGACY_QTI_FILE) && $context->has(self::LEGACY_XML_FILE); - } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php index f3173b323d0f..de131c9794e3 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php @@ -61,8 +61,8 @@ public function process(ImportContext $context): StageResult $importer = new ilImport($this->request_data_collector->getRefId()); $importer->importObject( null, - $context->get(UploadValidationStage::FILE_TO_IMPORT), - basename($context->get(UploadValidationStage::FILE_TO_IMPORT)), + $context->fileToImport(), + basename($context->fileToImport()), 'qpl', 'components/ILIAS/TestQuestionPool', true, diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php index e03ec66fe422..0ae80565e394 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php @@ -63,8 +63,6 @@ public function import( $images_pipe = new CollectQuestionImages(new Factory(), $this->data_factory->objId(0)); $tt = $this->builder->withAdditionalPipes(append: [$id_mapping_pipe, $images_pipe])->create(); - $selected_questions = QuestionSelectionStage::getSelectedQuestions($context); - $deserializer->addHandler( 'general', function (array $objects) use ($tt, $mapping, $parent_id, &$context): void { @@ -74,19 +72,19 @@ function (array $objects) use ($tt, $mapping, $parent_id, &$context): void { $mapping, $parent_id ); - $context = $context->with('pool_obj_id', $new_pool_id); + $context = $context->withPoolObjId($new_pool_id); } ); $deserializer->addHandler( 'questions', - function (array $questions) use ($tt, $mapping, $selected_questions): void { + function (array $questions) use ($tt, $mapping, $context): void { foreach ($questions as $question) { $this->questions_importer->importQuestion( $question, $tt, $mapping, - $selected_questions + $context->selectedQuestionIds() ); } } @@ -97,11 +95,11 @@ function (array $questions) use ($tt, $mapping, $selected_questions): void { function (array $assignments) use ($tt, $mapping, &$context): void { $result = $this->skill_importer->import( $assignments, - UploadValidationStage::getInstallId($context), + $context->installId(), $tt, $mapping, ); - $context = $context->with('skill_assignments', $result); + $context = $context->withSkillAssignments($result); } ); @@ -111,14 +109,14 @@ function (array $assignments) use ($tt, $mapping, &$context): void { $this->log->info('Importing question images...'); $this->questions_importer->importQuestionImages( - $context->get('pool_obj_id'), + $context->poolObjId(), $mapping, $context, $images_pipe ); $this->log->info('...Finished importing question images'); - $this->log->info("Finished importing question pool {$context->get('pool_obj_id')} (Object ID)"); + $this->log->info("Finished importing question pool {$context->poolObjId()} (Object ID)"); return $context; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php index c438949298c2..c3addd5f3773 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php @@ -37,9 +37,6 @@ */ class QuestionSelectionStage implements ImportStage { - public const string SELECTED_QUESTIONS = 'selected_questions'; - private const string SELECTABLE_QUESTIONS = 'selectable_questions'; - private array $old_export_question_types = [ 'ORDERING QUESTION' => \ilQTIItem::QT_ORDERING, 'KPRIM CHOICE QUESTION' => \ilQTIItem::QT_KPRIM_CHOICE, @@ -82,27 +79,25 @@ public function getDescription(): ?string public function process(ImportContext $context): StageResult { - if ($context->has('selectable_questions')) { + if ($context->selectableQuestionIds() !== []) { $options = []; - foreach ($context->get('selectable_questions') as $question) { - $options[$question] = $question; + foreach ($context->selectableQuestionIds() as $question) { + $options[$question] = (string) $question; } $data = $this->buildSelectQuestionsForm($options) ->withRequest($this->request) ->getData(); - if (isset($data['selected_questions'])) { - return StageResult::advance($context->with(self::SELECTED_QUESTIONS, $data['selected_questions'])); + if (is_array($data) && is_array($data['selected_questions'])) { + $selected_questions = array_map('intval', $data['selected_questions']); + return StageResult::advance( + $context->withSelectedQuestionIds($selected_questions) + ); } } - if (!$context->has(UploadValidationStage::COMPONENT_IMPORT_FILE)) { - $this->log->error('No component import file found in context'); - return StageResult::error($context, $this->lng->txt('qpl_import_file_not_found')); - } - - $options = DetectLegacyImportStage::isLegacyImport($context) + $options = $context->isLegacyImport() ? $this->readQuestionsFromQTI($context) : $this->readQuestions($context); @@ -120,26 +115,16 @@ public function process(ImportContext $context): StageResult ); return StageResult::interact( - $context->with(self::SELECTABLE_QUESTIONS, array_keys($options)), + $context->withSelectableQuestionIds(array_keys($options)), [$panel] ); } - /** - * @return list - */ - public static function getSelectedQuestions(ImportContext $context): array - { - return array_map('intval', $context->get(self::SELECTED_QUESTIONS, [])); - } - private function readQuestions(ImportContext $context): array { $options = []; - $deserializer = XmlDeserializer::fromFile( - $context->get(UploadValidationStage::COMPONENT_IMPORT_FILE) - ); + $deserializer = XmlDeserializer::fromFile($context->componentImportFile()); $deserializer->addHandler( 'questions', @@ -169,8 +154,8 @@ function (array $questions) use (&$options): void { private function readQuestionsFromQTI(ImportContext $context): array { $parser = new \ilQTIParser( - $context->get(UploadValidationStage::IMPORT_BASE_DIR), - $context->get(DetectLegacyImportStage::LEGACY_QTI_FILE), + $context->importBaseDir(), + $context->legacyQtiFile(), \ilQTIParser::IL_MO_VERIFY_QTI, 0 ); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php index bc942f5a3edd..29d5a5040ee7 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php @@ -110,7 +110,7 @@ public function importQuestionImages( ImportContext $context, CollectQuestionImages $pipe, ): void { - $import_dir = dirname($context->get(UploadValidationStage::COMPONENT_IMPORT_FILE)) . '/expDir_1'; + $import_dir = dirname($context->componentImportFile()) . '/expDir_1'; foreach ($pipe->getEnvelopes() as $filename => $envelope) { $source_path = $import_dir . DIRECTORY_SEPARATOR . $filename; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php index 585263caf527..7a518e7493e2 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php @@ -36,11 +36,6 @@ */ class UploadValidationStage implements ImportStage { - public const string FILE_TO_IMPORT = 'file_to_import'; - public const string COMPONENT_IMPORT_FILE = 'component_import_file'; - public const string IMPORT_BASE_DIR = 'import_base_dir'; - public const string INSTALL_ID = 'install_id'; - private const string IMPORT_TEMP_DIR = CLIENT_DATA_DIR . DIRECTORY_SEPARATOR . 'temp'; public function __construct( @@ -68,23 +63,23 @@ public function getDescription(): ?string public function process(ImportContext $context): StageResult { - $file_to_import = $context->get(self::FILE_TO_IMPORT); if ( - $file_to_import === null - || !is_file($file_to_import) - || !str_ends_with(strtolower($file_to_import), '.zip') + !$context->hasFileToImport() + || !is_file($context->fileToImport()) + || !str_ends_with(strtolower($context->fileToImport()), '.zip') ) { - $this->log->error("Invalid import file: {$file_to_import}"); + $this->log->error("Invalid import file: {$context->fileToImport()}"); return StageResult::error($context, $this->lng->txt('obj_import_file_error')); } - $subdir = basename($file_to_import, '.zip'); + $subdir = basename($context->fileToImport(), '.zip'); $import_base_dir = self::IMPORT_TEMP_DIR . DIRECTORY_SEPARATOR . $subdir; $options = (new UnzipOptions())->withZipOutputPath(self::IMPORT_TEMP_DIR); - $unzip = $this->archives->unzip(Streams::ofResource(fopen($file_to_import, 'r')), $options); + $handle = fopen($context->fileToImport(), 'r'); + $unzip = $this->archives->unzip(Streams::ofResource($handle), $options); $unzip->extract(); - $this->log->info("Extracted import file: {$file_to_import} -> {$import_base_dir}"); + $this->log->info("Extracted import file: {$context->fileToImport()} -> {$import_base_dir}"); $manifest = new ilManifestParser($import_base_dir . DIRECTORY_SEPARATOR . 'manifest.xml'); $export_file = array_find( @@ -103,14 +98,9 @@ public function process(ImportContext $context): StageResult return StageResult::advance( $context - ->with(self::COMPONENT_IMPORT_FILE, $component_import_file) - ->with(self::IMPORT_BASE_DIR, $import_base_dir) - ->with(self::INSTALL_ID, $manifest->getInstallId()) + ->withComponentImportFile($component_import_file) + ->withImportBaseDir($import_base_dir) + ->withInstallId((int) $manifest->getInstallId()) ); } - - public static function getInstallId(ImportContext $context): int - { - return intval($context->get(self::INSTALL_ID)); - } } diff --git a/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php b/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php index b5f59b97cebd..ec548a462f89 100644 --- a/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php +++ b/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php @@ -210,29 +210,137 @@ public function testXmlSerializerRejectsMismatchedGroups(): void $serializer->endGroup('actual'); } - public function testImportSessionRepositoryPreservesCurrentContextBehavior(): void + public function testImportSessionRepositoryPersistsNamedContextAsJson(): void { $repository = new ImportSessionRepository('characterization'); - $context = (new ImportContext()) - ->with('file_to_import', '/tmp/import.zip') - ->with('selected_question_ids', ['12', '27']) - ->with('unknown_field', 'preserved'); + $original = new ImportContext(); + $context = $original + ->withFileToImport('/tmp/import.zip') + ->withComponentImportFile('/tmp/import/qpl_data.xml') + ->withImportBaseDir('/tmp/import') + ->withInstallId(123) + ->withLegacyQtiFile('/tmp/import/qti.xml') + ->withLegacyXmlFile('/tmp/import/qpl.xml') + ->withSelectableQuestionIds([11, 12]) + ->withSelectedQuestionIds([12]) + ->withPoolObjId(45) + ->withTestObjId(88) + ->withTestRefId(99) + ->withUserMappings(['identifier' => 'login', 'mapping' => ['alice' => 'alice']]) + ->withResourceMappings([['id' => 'rid', 'suffix' => 'png', 'title' => 'img']]) + ->withSkillAssignments([ + 'failed' => [], + 'success' => [['skill_id' => 1, 'tref_id' => 0, 'title' => 'Skill', 'path' => '/']], + ]) + ->withSkillThresholds([ + 'failed' => [], + 'success' => [[ + 'skill_base_id' => 1, + 'skill_tref_id' => 0, + 'skill_level_id' => 4, + 'threshold' => 50, + ]], + ]); $repository->setCurrentStageIndex(3); $repository->setContext($context); + $this->assertFalse($original->hasFileToImport()); + $this->assertSame('/tmp/import.zip', $context->fileToImport()); + + $payload = ilSession::get('import_stage_characterization_context'); + $this->assertIsString($payload); + $decoded = json_decode($payload, true); + $this->assertIsArray($decoded); + $this->assertSame('/tmp/import.zip', $decoded['file_to_import']); + $this->assertSame(123, $decoded['install_id']); + $restored = $repository->getContext(); $this->assertSame(3, $repository->getCurrentStageIndex()); - $this->assertSame('/tmp/import.zip', $restored->get('file_to_import')); - $this->assertSame(['12', '27'], $restored->get('selected_question_ids')); - $this->assertSame('preserved', $restored->get('unknown_field')); - $this->assertNull($restored->get('missing')); - $this->assertSame('fallback', $restored->get('missing', 'fallback')); + $this->assertSame('/tmp/import.zip', $restored->fileToImport()); + $this->assertSame('/tmp/import/qpl_data.xml', $restored->componentImportFile()); + $this->assertSame('/tmp/import', $restored->importBaseDir()); + $this->assertSame(123, $restored->installId()); + $this->assertSame('/tmp/import/qti.xml', $restored->legacyQtiFile()); + $this->assertSame('/tmp/import/qpl.xml', $restored->legacyXmlFile()); + $this->assertTrue($restored->isLegacyImport()); + $this->assertSame([11, 12], $restored->selectableQuestionIds()); + $this->assertSame([12], $restored->selectedQuestionIds()); + $this->assertSame(45, $restored->poolObjId()); + $this->assertSame(88, $restored->testObjId()); + $this->assertSame(99, $restored->testRefId()); + $this->assertSame( + ['identifier' => 'login', 'mapping' => ['alice' => 'alice']], + $restored->userMappings() + ); + $this->assertSame( + [['id' => 'rid', 'suffix' => 'png', 'title' => 'img']], + $restored->resourceMappings() + ); + $this->assertSame( + [ + 'failed' => [], + 'success' => [['skill_id' => 1, 'tref_id' => 0, 'title' => 'Skill', 'path' => '/']], + ], + $restored->skillAssignments() + ); + $this->assertSame( + [ + 'failed' => [], + 'success' => [[ + 'skill_base_id' => 1, + 'skill_tref_id' => 0, + 'skill_level_id' => 4, + 'threshold' => 50, + ]], + ], + $restored->skillThresholds() + ); $repository->clear(); $this->assertSame(0, $repository->getCurrentStageIndex()); - $this->assertFalse($repository->getContext()->has('file_to_import')); + $empty = $repository->getContext(); + $this->assertFalse($empty->hasFileToImport()); + $this->assertSame([], $empty->selectedQuestionIds()); + $this->assertFalse($empty->isLegacyImport()); + } + + public function testImportSessionRepositoryIgnoresUnknownJsonKeys(): void + { + $repository = new ImportSessionRepository('characterization'); + ilSession::set( + 'import_stage_characterization_context', + json_encode([ + 'file_to_import' => '/tmp/import.zip', + 'bridge_tmp' => 'ignored', + ], JSON_THROW_ON_ERROR) + ); + + $restored = $repository->getContext(); + $decoded = json_decode($restored->toJson(), true); + + $this->assertSame('/tmp/import.zip', $restored->fileToImport()); + $this->assertNull($decoded['component_import_file']); + $this->assertArrayNotHasKey('bridge_tmp', $decoded); + } + + public function testImportContextIsLegacyImportOnlyWhenBothLegacyPathsAreSet(): void + { + $repository = new ImportSessionRepository('characterization'); + $repository->setContext( + (new ImportContext())->withLegacyQtiFile('/tmp/qti.xml') + ); + + $this->assertFalse($repository->getContext()->isLegacyImport()); + + $repository->setContext( + (new ImportContext()) + ->withLegacyQtiFile('/tmp/qti.xml') + ->withLegacyXmlFile('/tmp/qpl.xml') + ); + + $this->assertTrue($repository->getContext()->isLegacyImport()); } private function deserializeGroup(Deserializer $deserializer, string $group): array From 5420cb49360accef734b0a2905c627de8a99d684 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Mon, 21 Sep 2026 17:03:15 +0200 Subject: [PATCH 55/66] refactor: improve test export components structure --- .../classes/class.ilTestQuestionSetConfig.php | 5 + ...tRandomQuestionSetSourcePoolDefinition.php | 152 ++++----- .../Envelopes/AdditionalWorkingTime.php | 2 +- .../ExportImport/Envelopes/ManualFeedback.php | 2 +- .../ExportImport/Envelopes/QuestionResult.php | 2 +- .../Envelopes/RandomTestQuestion.php | 2 +- .../src/ExportImport/Envelopes/Solution.php | 2 +- .../ExportImport/Envelopes/WorkingTime.php | 2 +- .../Test/src/ExportImport/Exportable.php | 2 +- .../Import/SkillLevelThresholdsImporter.php | 4 +- .../src/ExportImport/Import/TestImporter.php | 117 +++---- .../Import/TestResultsImporter.php | 22 +- .../Normalizer/AttemptResultNormalizer.php | 30 +- .../Normalizer/ExportableNormalizer.php | 8 - .../Normalizer/ParticipantNormalizer.php | 4 +- .../ParticipantResultNormalizer.php | 4 +- .../QuestionSetConfigNormalizer.php | 55 ++-- .../Normalizer/ilObjTestNormalizer.php | 2 - .../Normalizer/ilTestSequenceNormalizer.php | 15 +- .../ilTestSkillLevelThresholdNormalizer.php | 10 +- .../src/ExportImport/Pipes/CollectUserIds.php | 18 +- .../Test/src/ExportImport/TestCollector.php | 3 +- .../Test/src/ExportImport/TestExporter.php | 108 ++++--- .../TestNormalizerRegistration.php | 83 +++++ .../ExportImport/TransformationsBuilder.php | 87 ++++++ components/ILIAS/Test/src/TestDIC.php | 13 +- .../class.ilTestQuestionPoolSetupAgent.php | 4 +- .../classes/class.assAnswerBinaryState.php | 27 +- .../class.assAnswerBinaryStateImage.php | 29 +- .../classes/class.assAnswerCloze.php | 31 +- .../classes/class.assAnswerErrorText.php | 34 +- .../classes/class.assAnswerImagemap.php | 31 +- .../classes/class.assAnswerMatching.php | 36 ++- .../classes/class.assAnswerMatchingPair.php | 34 +- .../classes/class.assAnswerMatchingTerm.php | 32 +- .../class.assAnswerMultipleResponse.php | 27 +- .../class.assAnswerMultipleResponseImage.php | 29 +- .../classes/class.assAnswerSimple.php | 36 ++- .../classes/class.assAnswerTrueFalse.php | 27 +- .../classes/class.assClozeGap.php | 43 +-- .../classes/class.assClozeTest.php | 58 ++-- .../classes/class.assErrorText.php | 46 +-- .../classes/class.assFileUpload.php | 36 ++- .../classes/class.assFormulaQuestion.php | 50 +-- .../class.assFormulaQuestionResult.php | 74 ++--- .../classes/class.assFormulaQuestionUnit.php | 52 ++-- .../class.assFormulaQuestionUnitCategory.php | 40 +-- .../class.assFormulaQuestionVariable.php | 50 +-- .../classes/class.assImagemapQuestion.php | 48 +-- .../classes/class.assKprimChoice.php | 56 ++-- .../classes/class.assLongMenu.php | 46 +-- .../classes/class.assMatchingQuestion.php | 60 ++-- .../classes/class.assMultipleChoice.php | 46 +-- .../classes/class.assNumeric.php | 38 ++- .../classes/class.assOrderingHorizontal.php | 42 +-- .../classes/class.assOrderingQuestion.php | 46 +-- .../classes/class.assQuestion.php | 87 +++--- .../classes/class.assSingleChoice.php | 50 +-- .../classes/class.assTextQuestion.php | 58 ++-- .../classes/class.assTextSubset.php | 52 ++-- .../classes/class.ilAssKprimChoiceAnswer.php | 38 ++- .../questions/class.ilAssOrderingElement.php | 56 ++-- .../class.ilAssQuestionLifecycle.php | 26 +- .../src/ExportImport/Envelopes/Feedback.php | 2 +- .../ExportImport/Envelopes/QuestionImage.php | 2 +- .../Export/QuestionPoolCollector.php | 3 +- .../Export/QuestionPoolExporter.php | 56 ++-- .../Foundation/Bridge/ExportState.php | 16 +- .../src/ExportImport/Foundation/Builder.php | 292 ------------------ .../Foundation/Contracts/Envelope.php | 2 +- .../Contracts/ExportDependencies.php | 81 ----- .../Foundation/Contracts/Exporter.php | 8 +- .../{Pipe.php => FromNormalized.php} | 25 +- .../Foundation/Contracts/Normalizable.php | 62 ---- .../Foundation/Contracts/Pipeline.php | 98 ------ .../Foundation/Contracts/ToNormalized.php | 39 +++ .../Foundation/Contracts/Transformations.php | 105 ------- .../Normalizing/Attributes/Normalizes.php | 42 --- .../Attributes/NormalizesLegacy.php | 48 --- .../Foundation/Normalizing/Envelopes/Id.php | 2 +- .../FoundationNormalizerRegistration.php | 68 ++++ .../Normalizer/DateTimeNormalizer.php | 4 +- .../Normalizer/EnvelopeNormalizer.php | 4 +- .../Normalizer/IlObjectNormalizer.php | 4 +- .../Normalizer/Legacy11UUIDNormalizer.php | 2 - .../Normalizing/Normalizer/Registry.php | 154 ++++----- .../Normalizer/ResourceNormalizer.php | 19 +- .../Normalizer/TransformationNormalizer.php | 12 +- .../Normalizing/Normalizer/UUIDNormalizer.php | 2 - .../Normalizing/Pipes/CollectResources.php | 23 +- .../Normalizing/Pipes/DenormalizeCarry.php | 58 ++-- .../Normalizing/Pipes/DenormalizingPipe.php | 62 ---- .../Pipes/DenormalizingProcessor.php | 72 +++++ .../Normalizing/Pipes/FinalizeNormalizing.php | 16 +- .../Normalizing/Pipes/IdMappingPipe.php | 87 ------ .../Normalizing/Pipes/IdMappingProcessor.php | 88 ++++++ .../Normalizing/Pipes/NormalizeCarry.php | 53 +++- .../Normalizing/Pipes/NormalizingPipe.php | 66 ---- .../Pipes/NormalizingProcessor.php | 58 ++++ .../Normalizing/Transformations.php | 66 ++-- .../src/ExportImport/Foundation/Pipeline.php | 202 ------------ .../DataCollector.php => Queue/Processor.php} | 15 +- .../ExportImport/Foundation/Queue/Queue.php | 65 ++++ .../Setup/NormalizerArtifactObjective.php | 90 ------ .../Import/QuestionPoolImporter.php | 35 ++- .../ExportImport/Import/QuestionsImporter.php | 8 +- .../Import/SkillAssignmentsImporter.php | 4 +- .../SuggestedSolutionNormalizer.php | 5 +- ...ilAssQuestionSkillAssignmentNormalizer.php | 11 +- .../ilObjQuestionPoolNormalizer.php | 3 - .../Pipes/CollectQuestionImages.php | 26 +- .../QuestionPoolNormalizerRegistration.php | 51 +++ .../ExportImport/TransformationsBuilder.php | 80 +++++ .../TestQuestionPool/src/QuestionPoolDIC.php | 13 +- .../ExportImportCharacterizationTest.php | 87 +++++- 115 files changed, 2244 insertions(+), 2581 deletions(-) create mode 100644 components/ILIAS/Test/src/ExportImport/TestNormalizerRegistration.php create mode 100644 components/ILIAS/Test/src/ExportImport/TransformationsBuilder.php delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Builder.php delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ExportDependencies.php rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/{Pipe.php => FromNormalized.php} (56%) delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Normalizable.php delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Pipeline.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ToNormalized.php delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Transformations.php delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/Normalizes.php delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/NormalizesLegacy.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/FoundationNormalizerRegistration.php delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizingPipe.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizingProcessor.php delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/IdMappingPipe.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/IdMappingProcessor.php delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizingPipe.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizingProcessor.php delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Pipeline.php rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Contracts/DataCollector.php => Queue/Processor.php} (50%) create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Queue/Queue.php delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Setup/NormalizerArtifactObjective.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolNormalizerRegistration.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/TransformationsBuilder.php diff --git a/components/ILIAS/Test/classes/class.ilTestQuestionSetConfig.php b/components/ILIAS/Test/classes/class.ilTestQuestionSetConfig.php index 320f896ee042..d714a3082561 100755 --- a/components/ILIAS/Test/classes/class.ilTestQuestionSetConfig.php +++ b/components/ILIAS/Test/classes/class.ilTestQuestionSetConfig.php @@ -49,6 +49,11 @@ abstract public function doesQuestionSetRelatedDataExist(): bool; abstract public function removeQuestionSetRelatedData(): void; abstract public function cloneQuestionSetRelatedData(ilObjTest $clone_test_obj): void; + public function getTestObject(): ilObjTest + { + return $this->test_obj; + } + public function getQuestionPoolPathString(int $pool_id): string { $ref_id = current(ilObject::_getAllReferences($pool_id)); diff --git a/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php b/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php index eef6e4391bbb..b8a48ae6a683 100755 --- a/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php +++ b/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\Refinery\Transformation; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; /** @@ -29,7 +29,7 @@ * * @package Modules/Test */ -class ilTestRandomQuestionSetSourcePoolDefinition implements Normalizable +class ilTestRandomQuestionSetSourcePoolDefinition implements ToNormalized, FromNormalized { private ?int $id = null; private ?int $pool_id = null; @@ -478,85 +478,87 @@ public function getPoolInfoLabel(ilLanguage $lng): string /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function () use ($tt): array { - $normalized = [ - 'id' => $tt->normalize(new Id($this->getId(), 'rnd_src_pool_def')), - 'pool_id' => $tt->normalize(new Id($this->getPoolId(), 'qpl')), - 'pool_title' => $this->getPoolTitle(), - 'pool_path' => $this->getPoolPath(), - 'quest_amount' => $this->getQuestionAmount(), - 'pool_quest_count' => $this->getPoolQuestionCount(), - 'position' => $this->getSequencePosition(), - 'type_filter' => $this->getTypeFilterAsTypeTags(), - 'lifecycle_filter' => $this->getLifecycleFilter(), - 'taxonomy_filter' => [], - 'mapped_taxonomy_filter' => [], + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null + { + $normalized = [ + 'id' => $transformations->normalize(new Id($this->getId(), 'rnd_src_pool_def')), + 'pool_id' => $transformations->normalize(new Id($this->getPoolId(), 'qpl')), + 'pool_title' => $this->getPoolTitle(), + 'pool_path' => $this->getPoolPath(), + 'quest_amount' => $this->getQuestionAmount(), + 'pool_quest_count' => $this->getPoolQuestionCount(), + 'position' => $this->getSequencePosition(), + 'type_filter' => $this->getTypeFilterAsTypeTags(), + 'lifecycle_filter' => $this->getLifecycleFilter(), + 'taxonomy_filter' => [], + 'mapped_taxonomy_filter' => [], + ]; + + foreach ($this->getOriginalTaxonomyFilter() as $tax_id => $node_ids) { + $normalized['taxonomy_filter'][] = [ + 'tax_id' => $transformations->normalize(new Id($tax_id, 'tax')), + 'node_ids' => array_map( + static fn($node_id): mixed => $transformations->normalize(new Id($node_id, 'tax_node')), + $node_ids + ), ]; + } - foreach ($this->getOriginalTaxonomyFilter() as $tax_id => $node_ids) { - $normalized['taxonomy_filter'][] = [ - 'tax_id' => $tt->normalize(new Id($tax_id, 'tax')), - 'node_ids' => array_map( - static fn($node_id): mixed => $tt->normalize(new Id($node_id, 'tax_node')), - $node_ids - ), - ]; - } - - foreach ($this->getMappedTaxonomyFilter() as $tax_id => $node_ids) { - $normalized['mapped_taxonomy_filter'][] = [ - 'tax_id' => $tt->normalize(new Id($tax_id, 'mapped_tax')), - 'node_ids' => array_map( - static fn($node_id): mixed => $tt->normalize(new Id($node_id, 'mapped_tax_node')), - $node_ids - ), - ]; - } + foreach ($this->getMappedTaxonomyFilter() as $tax_id => $node_ids) { + $normalized['mapped_taxonomy_filter'][] = [ + 'tax_id' => $transformations->normalize(new Id($tax_id, 'mapped_tax')), + 'node_ids' => array_map( + static fn($node_id): mixed => $transformations->normalize(new Id($node_id, 'mapped_tax_node')), + $node_ids + ), + ]; + } - return $normalized; - }); + return $normalized; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = clone $this; - $clone->setId($tt->denormalize($normalized['id'], Id::class)->getId()); - $clone->setPoolId($tt->denormalize($normalized['pool_id'], Id::class)->getId()); - $clone->setPoolTitle($tt->string($normalized['pool_title'])); - $clone->setPoolPath($tt->string($normalized['pool_path'])); - $clone->setQuestionAmount($tt->nullableInt($normalized['quest_amount'])); - $clone->setPoolQuestionCount($tt->nullableInt($normalized['pool_quest_count'])); - $clone->setSequencePosition($tt->int($normalized['position'])); - $clone->setTypeFilterFromTypeTags($normalized['type_filter']); - $clone->setLifecycleFilter($normalized['lifecycle_filter']); - - $taxonomy_filter = []; - foreach ($normalized['taxonomy_filter'] as $item) { - $tax_id = $tt->denormalize($item['tax_id'], Id::class)->getId(); - $taxonomy_filter[$tax_id] = array_map( - static fn($node_id): mixed => $tt->denormalize($node_id, Id::class)->getId(), - $item['node_ids'] - ); - } - $clone->setOriginalTaxonomyFilter($taxonomy_filter); - - $mapped_taxonomy_filter = []; - foreach (($normalized['mapped_taxonomy_filter'] ?? []) as $item) { - $tax_id = $tt->denormalize($item['tax_id'], Id::class)->getId(); - $mapped_taxonomy_filter[$tax_id] = array_map( - static fn($node_id): mixed => $tt->denormalize($node_id, Id::class)->getId(), - $item['node_ids'] - ); - } - $clone->setMappedTaxonomyFilter($mapped_taxonomy_filter); + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static + { + $clone = clone $this; + $clone->setId($transformations->denormalize($normalized['id'], Id::class)->getId()); + $clone->setPoolId($transformations->denormalize($normalized['pool_id'], Id::class)->getId()); + $clone->setPoolTitle($transformations->string($normalized['pool_title'])); + $clone->setPoolPath($transformations->string($normalized['pool_path'])); + $clone->setQuestionAmount($transformations->nullableInt($normalized['quest_amount'])); + $clone->setPoolQuestionCount($transformations->nullableInt($normalized['pool_quest_count'])); + $clone->setSequencePosition($transformations->int($normalized['position'])); + $clone->setTypeFilterFromTypeTags($normalized['type_filter']); + $clone->setLifecycleFilter($normalized['lifecycle_filter']); + + $taxonomy_filter = []; + foreach ($normalized['taxonomy_filter'] as $item) { + $tax_id = $transformations->denormalize($item['tax_id'], Id::class)->getId(); + $taxonomy_filter[$tax_id] = array_map( + static fn($node_id): mixed => $transformations->denormalize($node_id, Id::class)->getId(), + $item['node_ids'] + ); + } + $clone->setOriginalTaxonomyFilter($taxonomy_filter); + + $mapped_taxonomy_filter = []; + foreach (($normalized['mapped_taxonomy_filter'] ?? []) as $item) { + $tax_id = $transformations->denormalize($item['tax_id'], Id::class)->getId(); + $mapped_taxonomy_filter[$tax_id] = array_map( + static fn($node_id): mixed => $transformations->denormalize($node_id, Id::class)->getId(), + $item['node_ids'] + ); + } + $clone->setMappedTaxonomyFilter($mapped_taxonomy_filter); - return $clone; - }); + return $clone; } } diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/AdditionalWorkingTime.php b/components/ILIAS/Test/src/ExportImport/Envelopes/AdditionalWorkingTime.php index bb55f0f092fe..380869203893 100644 --- a/components/ILIAS/Test/src/ExportImport/Envelopes/AdditionalWorkingTime.php +++ b/components/ILIAS/Test/src/ExportImport/Envelopes/AdditionalWorkingTime.php @@ -21,7 +21,7 @@ namespace ILIAS\Test\ExportImport\Envelopes; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Envelope; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; class AdditionalWorkingTime implements Envelope diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/ManualFeedback.php b/components/ILIAS/Test/src/ExportImport/Envelopes/ManualFeedback.php index 123c743a9032..01ecba4e6ec6 100644 --- a/components/ILIAS/Test/src/ExportImport/Envelopes/ManualFeedback.php +++ b/components/ILIAS/Test/src/ExportImport/Envelopes/ManualFeedback.php @@ -21,7 +21,7 @@ namespace ILIAS\Test\ExportImport\Envelopes; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Envelope; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; class ManualFeedback implements Envelope diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/QuestionResult.php b/components/ILIAS/Test/src/ExportImport/Envelopes/QuestionResult.php index dfe9877771ca..5bc7dd307310 100644 --- a/components/ILIAS/Test/src/ExportImport/Envelopes/QuestionResult.php +++ b/components/ILIAS/Test/src/ExportImport/Envelopes/QuestionResult.php @@ -21,7 +21,7 @@ namespace ILIAS\Test\ExportImport\Envelopes; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Envelope; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; class QuestionResult implements Envelope diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/RandomTestQuestion.php b/components/ILIAS/Test/src/ExportImport/Envelopes/RandomTestQuestion.php index e759fa26f9ed..c9718e7dbb52 100644 --- a/components/ILIAS/Test/src/ExportImport/Envelopes/RandomTestQuestion.php +++ b/components/ILIAS/Test/src/ExportImport/Envelopes/RandomTestQuestion.php @@ -21,7 +21,7 @@ namespace ILIAS\Test\ExportImport\Envelopes; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Envelope; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; class RandomTestQuestion implements Envelope diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/Solution.php b/components/ILIAS/Test/src/ExportImport/Envelopes/Solution.php index 5ef1747f5ceb..413f1eb08a72 100644 --- a/components/ILIAS/Test/src/ExportImport/Envelopes/Solution.php +++ b/components/ILIAS/Test/src/ExportImport/Envelopes/Solution.php @@ -22,7 +22,7 @@ use ILIAS\ResourceStorage\Identification\ResourceIdentification; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Envelope; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer\ResourceNormalizer; diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/WorkingTime.php b/components/ILIAS/Test/src/ExportImport/Envelopes/WorkingTime.php index 97aeeeb4dc9b..743d69191698 100644 --- a/components/ILIAS/Test/src/ExportImport/Envelopes/WorkingTime.php +++ b/components/ILIAS/Test/src/ExportImport/Envelopes/WorkingTime.php @@ -21,7 +21,7 @@ namespace ILIAS\Test\ExportImport\Envelopes; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Envelope; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; class WorkingTime implements Envelope diff --git a/components/ILIAS/Test/src/ExportImport/Exportable.php b/components/ILIAS/Test/src/ExportImport/Exportable.php index 8849c6f9dfa3..5218a045715a 100644 --- a/components/ILIAS/Test/src/ExportImport/Exportable.php +++ b/components/ILIAS/Test/src/ExportImport/Exportable.php @@ -36,7 +36,7 @@ * data array, without relying on external services, database connections, or any other context. * --- * - * @deprecated 12: Use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable instead + * @deprecated 12: Use the direction-specific normalization contracts instead. */ interface Exportable { diff --git a/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php b/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php index b3009a6d9711..3a529c108996 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php @@ -25,7 +25,7 @@ use ilSkillTreeRepository; use ilTestSkillLevelThreshold; use ilTestSkillLevelThresholdList; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use Psr\Log\LoggerInterface; /** @@ -65,7 +65,7 @@ public function import( $threshold_list = new ilTestSkillLevelThresholdList($this->db); foreach ($normalized_thresholds as $item) { - // The mapping pipe replaces TestID and Skill BaseID/TRefID + // The mapping processor replaces TestID and Skill BaseID/TRefID $threshold = $transformations->denormalize($item, ilTestSkillLevelThreshold::class); $local_level_id = $this->getLevelIdMapping($import_install_id, $threshold->getSkillLevelId()); diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php index cc5fef4b03af..b3ddc299a2dc 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php @@ -35,13 +35,13 @@ use ILIAS\Test\Settings\GlobalSettings\UserIdentifiers; use ILIAS\Test\Settings\MainSettings\MainSettings; use ILIAS\Test\Settings\ScoreReporting\ScoreSettings; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; +use ILIAS\Test\ExportImport\TransformationsBuilder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Deserializer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\CollectResources; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\IdMappingPipe; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\IdMappingProcessor; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionsImporter; use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter; use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; @@ -51,15 +51,12 @@ use Psr\Log\LoggerInterface; /** - * Orchestrates the import of a test. It uses the Builder to create a pipeline of transformations that are used to normalize - * the data provided by the deserializer. It imports the test object, related resources (file uploads, question images, - * etc.), resolves the user mappings from the import mapping and imports the test content (settings, questions, - * participants, results, etc.) into the database. + * Orchestrates denormalization and persistence of a test import and its related resources. */ class TestImporter { public function __construct( - private readonly Builder $builder, + private readonly TransformationsBuilder $builder, private readonly ilDBInterface $database, private readonly LoggerInterface $log, private readonly IRSS $irss, @@ -84,21 +81,25 @@ public function import( ReferenceId $parent_id, ImportContext $context ): ImportContext { - $resource_pipe = new CollectResources($this->irss, $this->log); - $id_mapping_pipe = new IdMappingPipe($mapping, 'components/ILIAS/Test', $this->log); - $question_images_pipe = new CollectQuestionImages(new UUIDFactory(), $this->data_factory->objId(0)); - - $tt = $this->builder->withAdditionalPipes(append: [$id_mapping_pipe, $question_images_pipe, $resource_pipe])->create(); + $resource_collector = new CollectResources($this->irss, $this->log); + $question_images_collector = new CollectQuestionImages(new UUIDFactory(), $this->data_factory->objId(0)); + + $transformations = $this->builder->forImport( + null, + new IdMappingProcessor($mapping, 'components/ILIAS/Test', $this->log), + $question_images_collector, + $resource_collector + ); /** @var ilObjTest|null $test_object */ $test_object = null; $deserializer->addHandler( 'general', - function (array $objects) use ($tt, $mapping, $parent_id, &$test_object): void { + function (array $objects) use ($transformations, $mapping, $parent_id, &$test_object): void { $test_object = $this->importTest( array_pop($objects), - $tt, + $transformations, $mapping, $parent_id ); @@ -107,10 +108,10 @@ function (array $objects) use ($tt, $mapping, $parent_id, &$test_object): void { $deserializer->addHandler( 'settings', - function (array $settings) use ($tt, $mapping, &$test_object): void { + function (array $settings) use ($transformations, $mapping, &$test_object): void { $this->importSettings( $settings, - $tt, + $transformations, $mapping, $test_object ); @@ -119,10 +120,10 @@ function (array $settings) use ($tt, $mapping, &$test_object): void { $deserializer->addHandler( 'questions', - function (array $normalized) use ($tt, $mapping, $context, &$test_object): void { + function (array $normalized) use ($transformations, $mapping, $context, &$test_object): void { $this->importQuestions( $normalized, - $tt, + $transformations, $mapping, $context, $test_object @@ -132,10 +133,10 @@ function (array $normalized) use ($tt, $mapping, $context, &$test_object): void $deserializer->addHandler( 'question_set_config', - function (array $normalized) use ($tt, $mapping, $context, &$test_object): void { + function (array $normalized) use ($transformations, $mapping, $context, &$test_object): void { $this->importQuestionSetConfig( reset($normalized), - $tt, + $transformations, $mapping, $test_object ); @@ -144,11 +145,11 @@ function (array $normalized) use ($tt, $mapping, $context, &$test_object): void $deserializer->addHandler( 'skill_assignments', - function (array $assignments) use ($tt, $mapping, &$context): void { + function (array $assignments) use ($transformations, $mapping, &$context): void { $result = $this->skill_importer->import( $assignments, $context->installId(), - $tt, + $transformations, $mapping, ); $context = $context->withSkillAssignments($result); @@ -157,11 +158,11 @@ function (array $assignments) use ($tt, $mapping, &$context): void { $deserializer->addHandler( 'skill_thresholds', - function (array $thresholds) use ($tt, $mapping, &$context): void { + function (array $thresholds) use ($transformations, $mapping, &$context): void { $result = $this->skill_thresholds_importer->import( $thresholds, $context->installId(), - $tt, + $transformations, $mapping, ); $context = $context->withSkillThresholds($result); @@ -170,10 +171,10 @@ function (array $thresholds) use ($tt, $mapping, &$context): void { $deserializer->addHandler( 'participants', - function (array $participants) use ($tt, $mapping): void { + function (array $participants) use ($transformations, $mapping): void { $this->importParticipants( $participants, - $tt, + $transformations, $mapping, ); } @@ -181,26 +182,26 @@ function (array $participants) use ($tt, $mapping): void { $deserializer->addHandler( 'results', - function (array $results) use ($tt): void { + function (array $results) use ($transformations): void { $this->test_results_importer->import( $results, - $tt, + $transformations, ); } ); $deserializer->addHandler( 'additional_working_times', - function (array $times) use ($tt): void { + function (array $times) use ($transformations): void { $this->test_results_importer->importAdditionalWorkingTimes( $times, - $tt, + $transformations, ); } ); $this->log->info('Importing users and resources mappings...'); - $this->importMappings($mapping, $resource_pipe, $context); + $this->importMappings($mapping, $resource_collector, $context); $this->log->info('...Finished importing users and resources mappings'); $this->log->info('Importing test export file...'); @@ -212,7 +213,7 @@ function (array $times) use ($tt): void { $test_object->getId(), $mapping, $context, - $question_images_pipe + $question_images_collector ); $this->log->info('...Finished importing question images'); @@ -236,7 +237,7 @@ public function finalize(ilImportMapping $mapping): void private function importMappings( ilImportMapping $mapping, - CollectResources $resource_pipe, + CollectResources $resource_collector, ImportContext $context ): void { $user_mapping = $context->userMappings(); @@ -265,7 +266,7 @@ private function importMappings( new assFileUploadStakeholder(), $resource['title'] ); - $resource_pipe->storeMapping($resource['id'], $new_id); + $resource_collector->storeMapping($resource['id'], $new_id); $this->log->debug("Imported resource: {$resource_path} -> {$new_id->serialize()}"); } $this->log->info('...Finished importing resources and storing mappings'); @@ -273,11 +274,11 @@ private function importMappings( private function importTest( array $normalized, - Transformations $tt, + Transformations $transformations, ilImportMapping $mapping, ReferenceId $parent_id ): ilObjTest { - $test_object = $tt->denormalize($normalized, ilObjTest::class); + $test_object = $transformations->denormalize($normalized, ilObjTest::class); $old_obj_id = $test_object->getId(); $old_test_id = $test_object->getTestId(); @@ -301,15 +302,15 @@ private function importTest( private function importSettings( array $list, - Transformations $tt, + Transformations $transformations, ilImportMapping $mapping, ilObjTest $test_object ): void { $settings_id = $test_object->getMainSettings()->getId(); - $main_settings = $tt->denormalize($list[0], MainSettings::class)->withId($settings_id); - $scoring_settings = $tt->denormalize($list[1], ScoreSettings::class)->withId($settings_id); - $mark_schema = $tt->denormalize($list[2], MarkSchema::class)->withTestId($test_object->getTestId()); + $main_settings = $transformations->denormalize($list[0], MainSettings::class)->withId($settings_id); + $scoring_settings = $transformations->denormalize($list[1], ScoreSettings::class)->withId($settings_id); + $mark_schema = $transformations->denormalize($list[2], MarkSchema::class)->withTestId($test_object->getTestId()); if ($intro_page_id = $main_settings->getIntroductionSettings()->getIntroductionPageId()) { $new_page_id = $this->createPage($intro_page_id, $test_object->getId(), $mapping); @@ -351,7 +352,7 @@ private function createPage(int $imported_page_id, int $parent_id, ilImportMappi private function importQuestions( array $list, - Transformations $tt, + Transformations $transformations, ilImportMapping $mapping, ImportContext $context, ilObjTest $test_object @@ -359,7 +360,7 @@ private function importQuestions( foreach ($list as $normalized) { $question = $this->questions_importer->importQuestion( $normalized, - $tt, + $transformations, $mapping, $context->selectedQuestionIds() ); @@ -367,7 +368,7 @@ private function importQuestions( continue; } - $sequence = $tt->int($normalized['sequence']); + $sequence = $transformations->int($normalized['sequence']); $test_object->questions[$sequence] = $question->getId(); $this->log->debug("Stored question {$question->getId()} at sequence {$sequence} in test"); } @@ -378,32 +379,32 @@ private function importQuestions( private function importQuestionSetConfig( array $normalized, - Transformations $tt, + Transformations $transformations, ilImportMapping $mapping, ilObjTest $test_object ): void { - $config = $tt->denormalize($normalized, QuestionSetConfig::class); + $config = $transformations->denormalize($normalized, QuestionSetConfig::class); if ($config->isRandom()) { $this->random_test_config_importer->import($config, $mapping, $test_object); } } - private function importParticipants(array $list, Transformations $tt, ilImportMapping $mapping): void + private function importParticipants(array $list, Transformations $transformations, ilImportMapping $mapping): void { foreach ($list as $normalized) { if ($normalized['active_id'] === null) { - $this->importInvitedParticipant($normalized, $tt); + $this->importInvitedParticipant($normalized, $transformations); continue; } - $old_active_id = $tt->denormalize($normalized['active_id'], Id::class)->getId(); + $old_active_id = $transformations->denormalize($normalized['active_id'], Id::class)->getId(); $new_active_id = $this->database->nextId('tst_active'); $mapping->addMapping('components/ILIAS/Test', 'participant', (string) $old_active_id, (string) $new_active_id); $this->log->debug("Stored participant/test session mapping: {$old_active_id} -> {$new_active_id}"); - // The mapping pipe replaces TestID, UserID and ActiveID - $participant = $tt->denormalize($normalized, Participant::class); + // The mapping processor replaces TestID, UserID and ActiveID + $participant = $transformations->denormalize($normalized, Participant::class); $this->database->insert( 'tst_active', @@ -418,20 +419,20 @@ private function importParticipants(array $list, Transformations $tt, ilImportMa 'last_started_pass' => [ilDBConstants::T_INTEGER, $participant->getLastStartedAttempt()], 'importname' => [ilDBConstants::T_TEXT, "{$participant->getFirstname()} {$participant->getLastname()}"], 'tstamp' => [ilDBConstants::T_INTEGER, time()], - 'submittimestamp' => [ilDBConstants::T_TIMESTAMP, $tt->nullableString($normalized['submittimestamp'])], - 'lastindex' => [ilDBConstants::T_INTEGER, $tt->nullableInt($normalized['lastindex'])], - 'objective_container' => [ilDBConstants::T_INTEGER, $tt->nullableInt($normalized['objective_container'])], - 'start_lock' => [ilDBConstants::T_TEXT, $tt->nullableString($normalized['start_lock'])], + 'submittimestamp' => [ilDBConstants::T_TIMESTAMP, $transformations->nullableString($normalized['submittimestamp'])], + 'lastindex' => [ilDBConstants::T_INTEGER, $transformations->nullableInt($normalized['lastindex'])], + 'objective_container' => [ilDBConstants::T_INTEGER, $transformations->nullableInt($normalized['objective_container'])], + 'start_lock' => [ilDBConstants::T_TEXT, $transformations->nullableString($normalized['start_lock'])], ] ); $this->log->debug("Stored test session in database: {$new_active_id} (Active ID)"); } } - private function importInvitedParticipant(array $normalized, Transformations $tt): void + private function importInvitedParticipant(array $normalized, Transformations $transformations): void { - // The mapping pipe replaces TestID and UserID - $participant = $tt->denormalize($normalized, Participant::class); + // The mapping processor replaces TestID and UserID + $participant = $transformations->denormalize($normalized, Participant::class); $this->database->insert( 'tst_invited_user', diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php index b8011e106ba3..63d9e855f658 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php @@ -31,7 +31,7 @@ use ILIAS\Test\Results\Data\AttemptResult; use ILIAS\Test\Results\Data\ParticipantResult; use ILIAS\Test\TestManScoringDoneHelper; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ilTestSequence; use Psr\Log\LoggerInterface; @@ -74,7 +74,7 @@ public function import(array $list, Transformations $tt): void public function importTestSequences(array $list, Transformations $tt): void { foreach ($list as $normalized) { - // The mapping pipe replaces ActiveID and QuestionIDs + // The mapping processor replaces ActiveID and QuestionIDs $sequence = $tt->denormalize($normalized, ilTestSequence::class); $sequence->saveToDb(); $this->log->debug("Stored test sequence in database: {$sequence->getActiveId()} (Active ID), {$sequence->getPass()} (Pass)"); @@ -84,7 +84,7 @@ public function importTestSequences(array $list, Transformations $tt): void public function importSolutions(array $list, Transformations $tt): void { foreach ($list as $normalized) { - // The mapping pipe replaces ActiveID and QuestionID + // The mapping processor replaces ActiveID and QuestionID $solution = $tt->denormalize($normalized, Solution::class); $next_id = $this->database->nextId('tst_solutions'); @@ -110,7 +110,7 @@ public function importSolutions(array $list, Transformations $tt): void public function importQuestionResults(array $list, Transformations $tt): void { foreach ($list as $normalized) { - // The mapping pipe replaces ActiveID and QuestionID + // The mapping processor replaces ActiveID and QuestionID $result = $tt->denormalize($normalized, QuestionResult::class); $next_id = $this->database->nextId('tst_test_result'); @@ -134,7 +134,7 @@ public function importQuestionResults(array $list, Transformations $tt): void public function importAttemptResults(array $list, Transformations $tt): void { foreach ($list as $normalized) { - // The mapping pipe replaces ActiveID + // The mapping processor replaces ActiveID $attempt = $tt->denormalize($normalized, AttemptResult::class); $this->database->insert( @@ -163,7 +163,7 @@ public function importTestResult(?array $normalized, Transformations $tt): void return; } - // The mapping pipe replaces ActiveID + // The mapping processor replaces ActiveID $result = $tt->denormalize($normalized, ParticipantResult::class); $this->database->insert( @@ -186,7 +186,7 @@ public function importTestResult(?array $normalized, Transformations $tt): void public function importWorkingTimes(array $list, Transformations $tt): void { foreach ($list as $normalized) { - // The mapping pipe replaces ActiveID + // The mapping processor replaces ActiveID $working_time = $tt->denormalize($normalized, WorkingTime::class); $next_id = $this->database->nextId('tst_times'); @@ -208,7 +208,7 @@ public function importWorkingTimes(array $list, Transformations $tt): void public function importManualFeedback(array $list, Transformations $tt): void { foreach ($list as $normalized) { - // The mapping pipe replaces ActiveID, QuestionID and UserID + // The mapping processor replaces ActiveID, QuestionID and UserID $manual_feedback = $tt->denormalize($normalized, ManualFeedback::class); $next_id = $this->database->nextId('tst_manual_fb'); @@ -232,7 +232,7 @@ public function importManualFeedback(array $list, Transformations $tt): void public function importManualScoring(array $normalized, Transformations $tt): void { - // The mapping pipe replaces ActiveID, QuestionID and UserID + // The mapping processor replaces ActiveID, QuestionID and UserID $active_id = $tt->denormalize($normalized['active_id'], Id::class)->getId(); (new TestManScoringDoneHelper())->setDone($active_id, $tt->bool($normalized['done'])); @@ -242,7 +242,7 @@ public function importManualScoring(array $normalized, Transformations $tt): voi public function importAdditionalWorkingTimes(array $list, Transformations $tt): void { foreach ($list as $normalized) { - // The mapping pipe replaces UserID and TestID + // The mapping processor replaces UserID and TestID $time = $tt->denormalize($normalized, AdditionalWorkingTime::class); $this->database->insert( @@ -261,7 +261,7 @@ public function importAdditionalWorkingTimes(array $list, Transformations $tt): public function importRandomTestQuestions(array $list, Transformations $tt): void { foreach ($list as $normalized) { - // The mapping pipe replaces ActiveID, QuestionID and SourcePoolDefinitionID + // The mapping processor replaces ActiveID, QuestionID and SourcePoolDefinitionID $question = $tt->denormalize($normalized, RandomTestQuestion::class); $next_id = $this->database->nextId('tst_test_rnd_qst'); diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/AttemptResultNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/AttemptResultNormalizer.php index 15a1a4349b90..e8d3641e4036 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/AttemptResultNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/AttemptResultNormalizer.php @@ -22,20 +22,16 @@ use ILIAS\Test\Results\Data\AttemptResult; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; /** * @implements Normalizer */ -#[Normalizes(AttemptResult::class)] class AttemptResultNormalizer implements Normalizer { - public function __construct( - private readonly Transformations $tt, - ) { + public function __construct(private readonly Transformations $transformations) { } /** @@ -48,7 +44,7 @@ public function normalize($value): array|float|bool|int|string|null } return [ - 'active_id' => $this->tt->normalize(new Id($value->getActiveId(), 'participant')), + 'active_id' => $this->transformations->normalize(new Id($value->getActiveId(), 'participant')), 'attempt' => $value->getAttempt(), 'max_points' => $value->getMaxPoints(), 'reached_points' => $value->getReachedPoints(), @@ -71,16 +67,16 @@ public function denormalize(array|float|bool|int|string|null $value, string $typ } return new AttemptResult( - $this->tt->denormalize($value['active_id'], Id::class)->getId(), - $this->tt->int($value['attempt']), - $this->tt->float($value['max_points']), - $this->tt->float($value['reached_points']), - $this->tt->int($value['question_count']), - $this->tt->int($value['answered_questions']), - $this->tt->int($value['working_time']), - $this->tt->int($value['timestamp']), - $this->tt->string($value['exam_id']), - $this->tt->string($value['finalized_by']), + $this->transformations->denormalize($value['active_id'], Id::class)->getId(), + $this->transformations->int($value['attempt']), + $this->transformations->float($value['max_points']), + $this->transformations->float($value['reached_points']), + $this->transformations->int($value['question_count']), + $this->transformations->int($value['answered_questions']), + $this->transformations->int($value['working_time']), + $this->transformations->int($value['timestamp']), + $this->transformations->string($value['exam_id']), + $this->transformations->string($value['finalized_by']), ); } } diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ExportableNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/ExportableNormalizer.php index 1490132728dd..549f13f7cb93 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/ExportableNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/ExportableNormalizer.php @@ -22,21 +22,13 @@ use ILIAS\Test\ExportImport\Exportable; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; /** * @implements Normalizer */ -#[Normalizes(Exportable::class)] class ExportableNormalizer implements Normalizer { - public function __construct( - private readonly Transformations $tt, - ) { - } - /** * @inheritDoc */ diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php index c3e11a9f982b..35c450307daa 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php @@ -23,15 +23,13 @@ use DateTimeImmutable; use ILIAS\Test\Participants\Participant; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; /** * @implements Normalizer */ -#[Normalizes(Participant::class)] class ParticipantNormalizer implements Normalizer { public function __construct( diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantResultNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantResultNormalizer.php index b31df4ca9e0a..28335e5c769e 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantResultNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantResultNormalizer.php @@ -23,15 +23,13 @@ use ILIAS\Test\Results\Data\ParticipantResult; use ILIAS\Test\Scoring\Marks\Mark; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; /** * @implements Normalizer */ -#[Normalizes(ParticipantResult::class)] class ParticipantResultNormalizer implements Normalizer { public function __construct( diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php index 59e54fcafafa..1522aada28cb 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php @@ -20,32 +20,37 @@ namespace ILIAS\Test\ExportImport\Normalizer; -use ILIAS\DI\Container; +use ilComponentRepository; +use ilDBInterface; use ILIAS\Test\ExportImport\Envelopes\QuestionSetConfig; -use ILIAS\Test\TestDIC; +use ILIAS\Test\Logging\TestLogger; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; +use ilLanguage; use ilObjTest; use ilTestException; use ilTestFixedQuestionSetConfig; use ilTestRandomQuestionSetConfig; use ilTestQuestionSetConfig; use ilTestRandomQuestionSetSourcePoolDefinition; -use ReflectionClass; +use ilTree; /** * @implements Normalizer */ -#[Normalizes(QuestionSetConfig::class)] class QuestionSetConfigNormalizer implements Normalizer { public function __construct( private readonly Transformations $tt, - private readonly Container $dic, - private readonly TestDIC $test_dic, + private readonly ilDBInterface $db, + private readonly ilLanguage $lng, + private readonly ilTree $tree, + private readonly ilComponentRepository $component_repository, + private readonly TestLogger $logger, + private readonly GeneralQuestionPropertiesRepository $repository, ) { } @@ -111,13 +116,7 @@ private function normalizeStagingPools(array $staging_pools): array private function normalizeTestObj(ilTestQuestionSetConfig $config): array { - $reflection = new ReflectionClass($config); - $property = $reflection->getProperty('test_obj'); - $test_obj = $property->getValue($config); - - if (!$test_obj instanceof ilObjTest) { - throw new NormalizingException('Invalid test object', $test_obj); - } + $test_obj = $config->getTestObject(); return [ 'obj_id' => $this->tt->normalize(new Id($test_obj->getId(), 'object')), @@ -154,24 +153,24 @@ private function denormalizeQuestionSetConfig(array $normalized, ilObjTest $test { if($normalized['type'] === ilObjTest::QUESTION_SET_TYPE_FIXED) { return new ilTestFixedQuestionSetConfig( - $this->dic->repositoryTree(), - $this->dic->database(), - $this->dic->language(), - $this->test_dic['logging.logger'], - $this->dic['component.repository'], + $this->tree, + $this->db, + $this->lng, + $this->logger, + $this->component_repository, $test_obj, - $this->test_dic['question.general_properties.repository'] + $this->repository ); } $config = new ilTestRandomQuestionSetConfig( - $this->dic->repositoryTree(), - $this->dic->database(), - $this->dic->language(), - $this->test_dic['logging.logger'], - $this->dic['component.repository'], + $this->tree, + $this->db, + $this->lng, + $this->logger, + $this->component_repository, $test_obj, - $this->test_dic['question.general_properties.repository'] + $this->repository ); $amount_mode = $this->tt->string($normalized['amount_mode']); @@ -190,7 +189,7 @@ private function denormalizeQuestionSetConfig(array $normalized, ilObjTest $test private function denormalizeSourcePoolDefinition(array $normalized, ilObjTest $test_obj): ilTestRandomQuestionSetSourcePoolDefinition { $definition = new ilTestRandomQuestionSetSourcePoolDefinition( - $this->dic->database(), + $this->db, $test_obj ); diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ilObjTestNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/ilObjTestNormalizer.php index 35b05ae5d353..1783cb80b698 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/ilObjTestNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/ilObjTestNormalizer.php @@ -23,14 +23,12 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ilObjTest; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer\IlObjectNormalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; /** * @implements Normalizer */ -#[Normalizes(ilObjTest::class)] class ilObjTestNormalizer extends IlObjectNormalizer implements Normalizer { /** diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSequenceNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSequenceNormalizer.php index 44cfb4556513..963661d932f1 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSequenceNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSequenceNormalizer.php @@ -21,12 +21,9 @@ namespace ILIAS\Test\ExportImport\Normalizer; use ilDBInterface; -use ILIAS\DI\Container; -use ILIAS\Test\TestDIC; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ilTestSequence; @@ -34,19 +31,13 @@ /** * @implements Normalizer */ -#[Normalizes(ilTestSequence::class)] class ilTestSequenceNormalizer implements Normalizer { - private readonly ilDBInterface $db; - private readonly GeneralQuestionPropertiesRepository $repository; - public function __construct( private readonly Transformations $tt, - Container $dic, - TestDIC $local_dic, + private readonly ilDBInterface $db, + private readonly GeneralQuestionPropertiesRepository $repository, ) { - $this->db = $dic->database(); - $this->repository = $local_dic['question.general_properties.repository']; } /** diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSkillLevelThresholdNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSkillLevelThresholdNormalizer.php index e788922778ee..2b44b04296ca 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSkillLevelThresholdNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSkillLevelThresholdNormalizer.php @@ -22,26 +22,20 @@ use ilTestSkillLevelThreshold; use ilDBInterface; -use ILIAS\DI\Container; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; /** * @implements Normalizer */ -#[Normalizes(ilTestSkillLevelThreshold::class)] class ilTestSkillLevelThresholdNormalizer implements Normalizer { - private readonly ilDBInterface $db; - public function __construct( private readonly Transformations $tt, - Container $dic + private readonly ilDBInterface $db, ) { - $this->db = $dic->database(); } /** diff --git a/components/ILIAS/Test/src/ExportImport/Pipes/CollectUserIds.php b/components/ILIAS/Test/src/ExportImport/Pipes/CollectUserIds.php index b099476a60e7..e670a7f6ae93 100644 --- a/components/ILIAS/Test/src/ExportImport/Pipes/CollectUserIds.php +++ b/components/ILIAS/Test/src/ExportImport/Pipes/CollectUserIds.php @@ -20,15 +20,15 @@ namespace ILIAS\Test\ExportImport\Pipes; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Pipe; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\NormalizeCarry; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Queue\Processor; /** - * Pipe stores all user IDs during normalization. This is used to export user information the test object has references + * Collects user IDs during normalization. This is used to export user information the test object has references * to (e.g. participants, feedback authors). */ -class CollectUserIds implements Pipe +class CollectUserIds implements Processor { /** * @var array $ids @@ -38,17 +38,15 @@ class CollectUserIds implements Pipe /** * @inheritDoc */ - public function handle(mixed $passable, \Closure $next): mixed + public function process(object $carry): void { if ( - $passable instanceof NormalizeCarry - && $passable->value instanceof Id - && $passable->value->getObject() === 'user' + $carry instanceof NormalizeCarry + && $carry->value() instanceof Id + && $carry->value()->getObject() === 'user' ) { - $this->ids[$passable->value->getId()] = true; + $this->ids[$carry->value()->getId()] = true; } - - return $next($passable); } /** diff --git a/components/ILIAS/Test/src/ExportImport/TestCollector.php b/components/ILIAS/Test/src/ExportImport/TestCollector.php index 65f2ec8b0c66..4a5062ce00eb 100644 --- a/components/ILIAS/Test/src/ExportImport/TestCollector.php +++ b/components/ILIAS/Test/src/ExportImport/TestCollector.php @@ -41,7 +41,6 @@ use ILIAS\Test\Settings\GlobalSettings\UserIdentifiers; use ILIAS\Test\TestManScoringDoneHelper; use ILIAS\TestQuestionPool\ExportImport\Export\CollectsQuestions; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\DataCollector; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ilObjTest; @@ -57,7 +56,7 @@ /** * Collector to aggregate data from the test object for export. */ -class TestCollector implements DataCollector +class TestCollector { use CollectsQuestions; diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index 5058cf4aca79..a91573a3c101 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -36,11 +36,10 @@ use ILIAS\Test\Questions\Properties\Repository as QuestionsRepository; use ILIAS\Test\Results\Data\Repository as ResultsRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportStep; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ExportDependencies; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportState; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Exporter; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Serializer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\CollectResources; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlSerializer; use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; @@ -50,7 +49,7 @@ class TestExporter implements Exporter { public function __construct( - private readonly Builder $builder, + private readonly TransformationsBuilder $builder, private readonly DataFactory $data_factory, private readonly ilDBInterface $db, private readonly ilTree $tree, @@ -69,7 +68,7 @@ public function __construct( /** * @inheritDoc */ - public function prepare(ExportDependencies $state): void + public function prepare(ExportState $state): void { $state->logger()->info('Preparing test export (1/3)...'); $state->assertStep(ExportStep::INIT); @@ -94,25 +93,23 @@ public function prepare(ExportDependencies $state): void ); $state->setCollector($collector); - $transformations = $this->builder - ->withAdditionalPipes([ - new CollectUserIds(), - new CollectQuestionImages( - new UUIDFactory(), - $object_id - ), - new CollectResources( - $this->irss, - $this->logger - ), - ]) - ->create(); + $transformations = $this->builder->forExport( + new CollectUserIds(), + new CollectQuestionImages( + new UUIDFactory(), + $object_id + ), + new CollectResources( + $this->irss, + $this->logger + ) + ); $state->setTransformations($transformations); $state->logger()->info('...Finished preparing test export (1/3)'); } - private function extractObjectId(ExportDependencies $state): ?ObjectId + private function extractObjectId(ExportState $state): ?ObjectId { $target_ids = $state->target()->getObjectIds(); @@ -130,10 +127,20 @@ private function extractObjectId(ExportDependencies $state): ?ObjectId return $this->data_factory->objId(array_shift($target_ids)); } + private function collector(ExportState $state): TestCollector + { + $collector = $state->collector(); + if (!($collector instanceof TestCollector)) { + throw new \LogicException('Unexpected test collector'); + } + + return $collector; + } + /** * @inheritDoc */ - public function process(ExportDependencies $state): void + public function process(ExportState $state): void { $state->logger()->info('Processing test export (2/3)...'); $state->assertStep(ExportStep::PREPARE); @@ -142,7 +149,7 @@ public function process(ExportDependencies $state): void $state->serializer()->group( 'general', fn() => $this->exportObject( - $state->collector(), + $this->collector($state), $state->transformations(), $state->serializer(), $state @@ -151,7 +158,7 @@ public function process(ExportDependencies $state): void $state->serializer()->group( 'settings', fn() => $this->exportSettings( - $state->collector(), + $this->collector($state), $state->transformations(), $state->serializer(), $state @@ -160,7 +167,7 @@ public function process(ExportDependencies $state): void $state->serializer()->group( 'questions', fn() => $this->exportQuestions( - $state->collector(), + $this->collector($state), $state->transformations(), $state->serializer(), $state @@ -169,7 +176,7 @@ public function process(ExportDependencies $state): void $state->serializer()->group( 'question_set_config', fn() => $this->exportQuestionSetConfig( - $state->collector(), + $this->collector($state), $state->transformations(), $state->serializer(), ) @@ -177,7 +184,7 @@ public function process(ExportDependencies $state): void $state->serializer()->group( 'additional_working_times', fn() => $this->exportAdditionalWorkingTimes( - $state->collector(), + $this->collector($state), $state->transformations(), $state->serializer(), ) @@ -185,7 +192,7 @@ public function process(ExportDependencies $state): void $state->serializer()->group( 'skill_assignments', fn() => $this->exportSkillAssignments( - $state->collector(), + $this->collector($state), $state->transformations(), $state->serializer(), ) @@ -193,7 +200,7 @@ public function process(ExportDependencies $state): void $state->serializer()->group( 'skill_thresholds', fn() => $this->exportSkillLevelThresholds( - $state->collector(), + $this->collector($state), $state->transformations(), $state->serializer(), ) @@ -208,12 +215,12 @@ public function process(ExportDependencies $state): void $state->logger()->info('...Finished processing test export (2/3)'); } - private function processResults(ExportDependencies $state): void + private function processResults(ExportState $state): void { $state->serializer()->group( 'participants', fn() => $this->exportParticipants( - $state->collector(), + $this->collector($state), $state->transformations(), $state->serializer() ) @@ -221,7 +228,7 @@ private function processResults(ExportDependencies $state): void $state->serializer()->group( 'results', fn() => $this->exportResults( - $state->collector(), + $this->collector($state), $state->transformations(), $state->serializer() ) @@ -231,17 +238,26 @@ private function processResults(ExportDependencies $state): void /** * @inheritDoc */ - public function write(ExportDependencies $state): void + public function write(ExportState $state): void { $state->logger()->info('Writing test export (3/3)...'); $state->assertStep(ExportStep::PROCESS); $state->setStep(ExportStep::WRITE); $export_dir = $state->path()->getPathToComponentExpDirInContainer(); - $question_image_pipe = $state->transformations()->context(CollectQuestionImages::class); - $resource_pipe = $state->transformations()->context(CollectResources::class); + $question_image_collector = $state->transformations() + ->normalizationProcessor(CollectQuestionImages::class); + if (!($question_image_collector instanceof CollectQuestionImages)) { + throw new \LogicException('Unexpected question image processor'); + } - foreach ($question_image_pipe->getFiles() as $file) { + $resource_collector = $state->transformations() + ->normalizationProcessor(CollectResources::class); + if (!($resource_collector instanceof CollectResources)) { + throw new \LogicException('Unexpected resource processor'); + } + + foreach ($question_image_collector->getFiles() as $file) { if (!file_exists($file['from'])) { $state->logger()->warning('Question image file not found: ' . $file['from']); continue; @@ -254,7 +270,7 @@ public function write(ExportDependencies $state): void $state->logger()->debug("Copied question image {$file['from']} to {$export_dir}/{$file['to']}"); } - foreach ($resource_pipe->getResources() as $id => $resource) { + foreach ($resource_collector->getResources() as $id => $resource) { $clean_id = str_replace(['-', '_'], '', $id); $file = "{$clean_id}.{$resource->getCurrentRevision()->getInformation()->getSuffix()}"; @@ -266,7 +282,7 @@ public function write(ExportDependencies $state): void } $this->writeMappings( - $state->collector(), + $this->collector($state), $state->transformations(), $state ); @@ -280,7 +296,7 @@ private function exportObject( TestCollector $collector, Transformations $transformations, Serializer $serializer, - ExportDependencies $state + ExportState $state ): void { $serializer->append('object', $transformations->normalize($collector->getObject())); @@ -298,7 +314,7 @@ private function exportSettings( TestCollector $collector, Transformations $transformations, Serializer $serializer, - ExportDependencies $state + ExportState $state ): void { $test = $collector->getObject(); $main_settings = $test->getMainSettings(); @@ -319,7 +335,7 @@ private function exportQuestions( TestCollector $collector, Transformations $transformations, Serializer $serializer, - ExportDependencies $state + ExportState $state ): void { $question_properties = $collector->getTestQuestionProperties(); @@ -418,16 +434,24 @@ private function exportAdditionalWorkingTimes( private function writeMappings( TestCollector $collector, Transformations $transformations, - ExportDependencies $state + ExportState $state ): void { $serializer = XmlSerializer::inMemory(); $serializer->createDocument('Test Export Mappings'); $serializer->startGroup('mappings'); - $user_ids = $transformations->context(CollectUserIds::class)->getIds(); + $user_id_processor = $transformations->normalizationProcessor(CollectUserIds::class); + if (!($user_id_processor instanceof CollectUserIds)) { + throw new \LogicException('Unexpected user ID processor'); + } + $user_ids = $user_id_processor->getIds(); $serializer->append('users', $collector->getUserMapping($user_ids)); - $resources = $transformations->context(CollectResources::class)->getResources(); + $resource_processor = $transformations->normalizationProcessor(CollectResources::class); + if (!($resource_processor instanceof CollectResources)) { + throw new \LogicException('Unexpected resource processor'); + } + $resources = $resource_processor->getResources(); $serializer->append( 'resources', array_map($transformations->normalize(...), $resources) diff --git a/components/ILIAS/Test/src/ExportImport/TestNormalizerRegistration.php b/components/ILIAS/Test/src/ExportImport/TestNormalizerRegistration.php new file mode 100644 index 000000000000..5596dd1f11c5 --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/TestNormalizerRegistration.php @@ -0,0 +1,83 @@ +register(\ilObjTest::class, new ilObjTestNormalizer($transformations)); + $registry->register( + \ilTestSequence::class, + new ilTestSequenceNormalizer( + $transformations, + $this->dic->database(), + $this->test_dic['question.general_properties.repository'] + ) + ); + $registry->register(AttemptResult::class, new AttemptResultNormalizer($transformations)); + $registry->register( + ParticipantResult::class, + new ParticipantResultNormalizer($transformations) + ); + $registry->register(Participant::class, new ParticipantNormalizer($transformations)); + $registry->register(Exportable::class, new ExportableNormalizer()); + $registry->register( + QuestionSetConfig::class, + new QuestionSetConfigNormalizer( + $transformations, + $this->dic->database(), + $this->dic->language(), + $this->dic->repositoryTree(), + $this->dic['component.repository'], + $this->test_dic['logging.logger'], + $this->test_dic['question.general_properties.repository'] + ) + ); + $registry->register( + \ilTestSkillLevelThreshold::class, + new ilTestSkillLevelThresholdNormalizer($transformations, $this->dic->database()) + ); + } +} diff --git a/components/ILIAS/Test/src/ExportImport/TransformationsBuilder.php b/components/ILIAS/Test/src/ExportImport/TransformationsBuilder.php new file mode 100644 index 000000000000..16f2ef85cd30 --- /dev/null +++ b/components/ILIAS/Test/src/ExportImport/TransformationsBuilder.php @@ -0,0 +1,87 @@ +create(null, $runtime_processors, []); + } + + public function forImport( + ?string $legacy_version, + Processor ...$runtime_processors + ): Transformations { + return $this->create($legacy_version, [], $runtime_processors); + } + + /** + * @param list $normalization_processors + * @param list $denormalization_processors + */ + private function create( + ?string $legacy_version, + array $normalization_processors, + array $denormalization_processors + ): Transformations { + $registry = new Registry(); + $transformations = new Transformations( + $this->global_dic->refinery(), + new Queue(...[ + ...$normalization_processors, + new NormalizingProcessor($registry), + new FinalizeNormalizing() + ]), + new Queue(...[ + new DenormalizingProcessor($registry, $legacy_version), + ...$denormalization_processors + ]) + ); + + (new FoundationNormalizerRegistration($this->global_dic)) + ->register($registry, $transformations); + (new QuestionPoolNormalizerRegistration($this->global_dic)) + ->register($registry, $transformations); + (new TestNormalizerRegistration($this->global_dic, $this->test_dic)) + ->register($registry, $transformations); + + return $transformations; + } +} diff --git a/components/ILIAS/Test/src/TestDIC.php b/components/ILIAS/Test/src/TestDIC.php index 97dd4496a5cf..347887b8d674 100755 --- a/components/ILIAS/Test/src/TestDIC.php +++ b/components/ILIAS/Test/src/TestDIC.php @@ -48,12 +48,12 @@ use ILIAS\Test\Logging\Factory as InteractionFactory; use ILIAS\Test\ExportImport\Factory as ExportImportFactory; use ILIAS\Test\ExportImport\DBRepository as ExportImportRepository; +use ILIAS\Test\ExportImport\TransformationsBuilder; use ILIAS\Test\Questions\Properties\Repository as TestQuestionsRepository; use ILIAS\Test\Questions\Properties\DatabaseRepository as TestQuestionsDatabaseRepository; use ILIAS\Test\Results\Data\Factory as ResultsDataFactory; use ILIAS\Test\Results\Presentation\Factory as ResultsPresentationFactory; use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\StateHolder; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionsImporter; use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter; @@ -250,15 +250,12 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic['exportimport.session'] = static fn($c): ImportSessionRepository => new ImportSessionRepository('tst'); - $dic['exportimport.builder'] = static fn($c): Builder => - new Builder( - $DIC, - $c - ); + $dic['exportimport.transformations_builder'] = static fn($c): TransformationsBuilder => + new TransformationsBuilder($DIC, $c); $dic['exportimport.exporter'] = static fn($c): TestExporter => new TestExporter( - $c['exportimport.builder'], + $c['exportimport.transformations_builder'], new DataFactory(), $DIC->database(), $DIC->repositoryTree(), @@ -318,7 +315,7 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic['exportimport.importer'] = static fn($c): TestImporter => new TestImporter( - $c['exportimport.builder'], + $c['exportimport.transformations_builder'], $DIC->database(), $c['exportimport.logging'](), $DIC->resourceStorage(), diff --git a/components/ILIAS/TestQuestionPool/classes/Setup/class.ilTestQuestionPoolSetupAgent.php b/components/ILIAS/TestQuestionPool/classes/Setup/class.ilTestQuestionPoolSetupAgent.php index d3e34d84d37b..06c48ffe8f48 100755 --- a/components/ILIAS/TestQuestionPool/classes/Setup/class.ilTestQuestionPoolSetupAgent.php +++ b/components/ILIAS/TestQuestionPool/classes/Setup/class.ilTestQuestionPoolSetupAgent.php @@ -19,8 +19,8 @@ use ILIAS\Setup\Agent\NullAgent; use ILIAS\Setup\Objective; use ILIAS\Setup\ObjectiveCollection; +use ILIAS\Setup\Objective\NullObjective; use ILIAS\Setup\Metrics; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Setup\NormalizerArtifactObjective; class ilTestQuestionPoolSetupAgent extends NullAgent { @@ -63,7 +63,7 @@ public function getMigrations(): array public function getBuildObjective(): Objective { - return new NormalizerArtifactObjective(); + return new NullObjective(); } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryState.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryState.php index 1c44eb1ed9cf..15b3c1fbad3f 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryState.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryState.php @@ -16,8 +16,7 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; /** * Class for true/false or yes/no answers @@ -187,23 +186,27 @@ public function setUnchecked(): void /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'checked' => $this->checked, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->setState($tt->bool($normalized['checked'])); - return $clone; - }); + $clone = parent::fromNormalized($normalized, $transformations); + $clone->setState($transformations->bool($normalized['checked'])); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php index 347c508aa7f0..0482ab8938fb 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php @@ -18,9 +18,8 @@ declare(strict_types=1); -use ILIAS\Refinery\Transformation; use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; /** * Class for answers with a binary state indicator @@ -84,25 +83,29 @@ public function hasImage(): bool /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(array $context): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'image' => $this->image ? - $tt->normalize(new QuestionImage($this->image, $context['question_id'] ?? null)) + $transformations->normalize(new QuestionImage($this->image, $context['question_id'] ?? null)) : null, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->setImage($tt->denormalize($normalized['image'], QuestionImage::class)?->getFilename()); - return $clone; - }); + $clone = parent::fromNormalized($normalized, $transformations); + $clone->setImage($transformations->denormalize($normalized['image'], QuestionImage::class)?->getFilename()); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerCloze.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerCloze.php index 04f4bd4381d7..cdb8bb6ba6cc 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerCloze.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerCloze.php @@ -16,8 +16,7 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; /** * Class for cloze question numeric answers @@ -157,27 +156,31 @@ public function getGapSize(): int /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'lower_bound' => $this->lowerBound, 'upper_bound' => $this->upperBound, 'gap_size' => $this->gap_size, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->lowerBound = $tt->nullableString($normalized['lower_bound']); - $clone->upperBound = $tt->nullableString($normalized['upper_bound']); - $clone->gap_size = $tt->int($normalized['gap_size']); - return $clone; - }); + $clone = parent::fromNormalized($normalized, $transformations); + $clone->lowerBound = $transformations->nullableString($normalized['lower_bound']); + $clone->upperBound = $transformations->nullableString($normalized['upper_bound']); + $clone->gap_size = $transformations->int($normalized['gap_size']); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerErrorText.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerErrorText.php index 0567ce2925a4..6cb9897d64d7 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerErrorText.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerErrorText.php @@ -16,9 +16,9 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; /** * Class for error text answers @@ -28,7 +28,7 @@ * * @ingroup components\ILIASTestQuestionPool */ -class assAnswerErrorText implements Normalizable +class assAnswerErrorText implements ToNormalized, FromNormalized { protected string $text_wrong; protected string $text_correct; @@ -102,26 +102,32 @@ public function getLength(): int /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ + return [ 'text_wrong' => $this->text_wrong, 'text_correct' => $this->text_correct, 'points' => $this->points, 'position' => $this->position, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): self { - return $tt->custom()->transformation(fn(array $normalized): self => new self( - $tt->string($normalized['text_wrong']), - $tt->string($normalized['text_correct']), - $tt->float($normalized['points']), - $tt->int($normalized['position']) - )); + return new self( + $transformations->string($normalized['text_wrong']), + $transformations->string($normalized['text_correct']), + $transformations->float($normalized['points']), + $transformations->int($normalized['position']) + ); } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerImagemap.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerImagemap.php index b3b69c42d554..b8e5a84bbd44 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerImagemap.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerImagemap.php @@ -16,8 +16,7 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; /** * Class for true/false or yes/no answers @@ -149,27 +148,31 @@ public function setPointsUnchecked($points_unchecked): void /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'coords' => $this->coords, 'area' => $this->area, 'points_unchecked' => $this->points_unchecked, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->setCoords($tt->string($normalized['coords'])); - $clone->setArea($tt->string($normalized['area'])); - $clone->setPointsUnchecked($tt->float($normalized['points_unchecked'])); - return $clone; - }); + $clone = parent::fromNormalized($normalized, $transformations); + $clone->setCoords($transformations->string($normalized['coords'])); + $clone->setArea($transformations->string($normalized['area'])); + $clone->setPointsUnchecked($transformations->float($normalized['points_unchecked'])); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatching.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatching.php index 71137aac7763..f385fa01b1e3 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatching.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatching.php @@ -16,9 +16,9 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; /** * Class for matching question answers @@ -28,7 +28,7 @@ * @author Helmut Schottmüller * @ingroup components\ILIASTestQuestionPool */ -class ASS_AnswerMatching implements Normalizable +class ASS_AnswerMatching implements ToNormalized, FromNormalized { public float $points; @@ -244,28 +244,32 @@ public function setPoints(float $points = 0.0): void /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ + return [ 'points' => $this->points, 'picture_or_definition' => $this->picture_or_definition, 'picture_or_definition_id' => $this->picture_or_definition_id, 'term_id' => $this->term_id, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = clone $this; - $clone->setPoints($tt->float($normalized['points'])); - $clone->setPicture($tt->string($normalized['picture_or_definition'])); - $clone->setPictureId($tt->int($normalized['picture_or_definition_id'])); - $clone->setTermId($tt->int($normalized['term_id'])); - return $clone; - }); + $clone = clone $this; + $clone->setPoints($transformations->float($normalized['points'])); + $clone->setPicture($transformations->string($normalized['picture_or_definition'])); + $clone->setPictureId($transformations->int($normalized['picture_or_definition_id'])); + $clone->setTermId($transformations->int($normalized['term_id'])); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingPair.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingPair.php index 9601aabf9026..7b68eab9d425 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingPair.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingPair.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; /** * Class for matching question pairs @@ -28,7 +28,7 @@ * @author Helmut Schottmüller * @ingroup components\ILIASTestQuestionPool */ -class assAnswerMatchingPair implements Normalizable +class assAnswerMatchingPair implements ToNormalized, FromNormalized { protected assAnswerMatchingTerm $term; protected assAnswerMatchingDefinition $definition; @@ -80,24 +80,28 @@ public function withPoints(float $points): self /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(array $context): array => [ + return [ 'points' => $this->points, - 'term' => $tt->normalize($this->term, $context), - 'definition' => $tt->normalize($this->definition, $context), - ]); + 'term' => $transformations->normalize($this->term, $context), + 'definition' => $transformations->normalize($this->definition, $context), + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): self { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - return $this->withPoints($tt->float($normalized['points'])) - ->withTerm($tt->denormalize($normalized['term'], $this->term)) - ->withDefinition($tt->denormalize($normalized['definition'], $this->definition)); - }); + return $this->withPoints($transformations->float($normalized['points'])) + ->withTerm($transformations->denormalize($normalized['term'], $this->term)) + ->withDefinition($transformations->denormalize($normalized['definition'], $this->definition)); } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php index d8a978e28193..c3d00fc9014f 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; /** @@ -29,7 +29,7 @@ * @author Helmut Schottmüller * @ingroup components\ILIASTestQuestionPool */ -class assAnswerMatchingTerm implements Normalizable +class assAnswerMatchingTerm implements ToNormalized, FromNormalized { protected string $text; protected string $picture; @@ -85,26 +85,30 @@ public function withIdentifier(int $identifier): self /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(array $context): array => [ + return [ 'text' => $this->text, 'picture' => $this->picture - ? $tt->normalize(new QuestionImage($this->picture, $context['question_id'] ?? null)) + ? $transformations->normalize(new QuestionImage($this->picture, $context['question_id'] ?? null)) : null, 'identifier' => $this->identifier, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): self { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - return $this->withText($tt->string($normalized['text'])) - ->withPicture($tt->denormalize($normalized['picture'], QuestionImage::class)?->getFilename() ?? '') - ->withIdentifier($tt->int($normalized['identifier'])); - }); + return $this->withText($transformations->string($normalized['text'])) + ->withPicture($transformations->denormalize($normalized['picture'], QuestionImage::class)?->getFilename() ?? '') + ->withIdentifier($transformations->int($normalized['identifier'])); } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponse.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponse.php index 331fd04594e2..6827a5a9dbce 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponse.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponse.php @@ -16,8 +16,7 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; /** * Class for true/false or yes/no answers @@ -104,23 +103,27 @@ public function getPointsChecked(): float /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'points_unchecked' => $this->points_unchecked, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->points_unchecked = $tt->nullableFloat($normalized['points_unchecked']); - return $clone; - }); + $clone = parent::fromNormalized($normalized, $transformations); + $clone->points_unchecked = $transformations->nullableFloat($normalized['points_unchecked']); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php index d1945f7f6146..b2e2c592b756 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php @@ -18,8 +18,7 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; /** @@ -82,25 +81,29 @@ public function hasImage(): bool /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(array $context): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'image' => $this->image - ? $tt->normalize(new QuestionImage($this->image, $context['question_id'] ?? null)) + ? $transformations->normalize(new QuestionImage($this->image, $context['question_id'] ?? null)) : null, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->setImage($tt->denormalize($normalized['image'], QuestionImage::class)?->getFilename()); - return $clone; - }); + $clone = parent::fromNormalized($normalized, $transformations); + $clone->setImage($transformations->denormalize($normalized['image'], QuestionImage::class)?->getFilename()); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php index 9a16c41b0b2a..bc0376c79a09 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php @@ -16,10 +16,10 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; -use ILIAS\Refinery\Transformation; /** * Class for simple answers @@ -31,7 +31,7 @@ * * @ingroup components\ILIASTestQuestionPool */ -class ASS_AnswerSimple implements Normalizable +class ASS_AnswerSimple implements ToNormalized, FromNormalized { protected string $answertext; @@ -220,26 +220,32 @@ public function setPoints($points = 0.0): void /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - 'id' => $tt->normalize(new Id($this->id, 'answer')), + return [ + 'id' => $transformations->normalize(new Id($this->id, 'answer')), 'answertext' => $this->answertext, 'points' => $this->points, 'order' => $this->order, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static { - return $tt->custom()->transformation(fn(array $normalized): static => new static( - $tt->string($normalized['answertext']), - $tt->float($normalized['points']), - $tt->int($normalized['order']), - $tt->denormalize($normalized['id'], Id::class)->getId() - )); + return new static( + $transformations->string($normalized['answertext']), + $transformations->float($normalized['points']), + $transformations->int($normalized['order']), + $transformations->denormalize($normalized['id'], Id::class)->getId() + ); } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerTrueFalse.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerTrueFalse.php index 2ce73b3e9940..3d07d5664b4f 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerTrueFalse.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerTrueFalse.php @@ -16,8 +16,7 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; /** * Class for true/false or yes/no answers @@ -154,23 +153,27 @@ public function setFalse(): void /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'correctness' => $this->correctness, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->setCorrectness($tt->string($normalized['correctness'])); - return $clone; - }); + $clone = parent::fromNormalized($normalized, $transformations); + $clone->setCorrectness($transformations->string($normalized['correctness'])); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php b/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php index 99c9138e23e7..b46806894cdc 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php @@ -16,8 +16,9 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\Refinery\Transformation; /** @@ -32,7 +33,7 @@ * * @ingroup components\ILIASTestQuestionPool */ -class assClozeGap implements Normalizable +class assClozeGap implements ToNormalized, FromNormalized { public const TYPE_TEXT = 0; public const TYPE_SELECT = 1; @@ -459,31 +460,35 @@ public function numericRangeExists(): bool /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ + return [ 'type' => $this->type, 'shuffle' => $this->shuffle, 'gap_size' => $this->gap_size, - 'items' => $tt->normalize($this->items), - ]); + 'items' => $transformations->normalize($this->items), + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): self { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = new self($normalized['type']); - $clone->setShuffle($normalized['shuffle']); - $clone->setGapSize($normalized['gap_size']); - $clone->items = array_map( - fn(array $item) => $tt->denormalize($item, new assAnswerCloze()), - $normalized['items'] - ); - - return $clone; - }); + $clone = new self($normalized['type']); + $clone->setShuffle($normalized['shuffle']); + $clone->setGapSize($normalized['gap_size']); + $clone->items = array_map( + fn(array $item) => $transformations->denormalize($item, new assAnswerCloze()), + $normalized['items'] + ); + + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php b/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php index 00a80a5af323..423e4f44e540 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\Questions\QuestionPartiallySaveable; @@ -38,7 +38,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assClozeTest extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionPartiallySaveable, QuestionLMExportable, QuestionAutosaveable, Normalizable +class assClozeTest extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionPartiallySaveable, QuestionLMExportable, QuestionAutosaveable, ToNormalized, FromNormalized { /** * The gaps of the cloze question @@ -1774,42 +1774,46 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'feedback_mode' => $this->feedbackMode, 'cloze_text' => $this->cloze_text, 'textgap_rating' => $this->textgap_rating, 'identical_scoring' => $this->identical_scoring, 'fixed_text_length' => $this->fixed_text_length, - 'gaps' => $tt->normalize($this->gaps), + 'gaps' => $transformations->normalize($this->gaps), 'gap_combinations' => $this->gap_combinations, 'gap_combinations_exist' => $this->gap_combinations_exist, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->feedbackMode = $tt->string($normalized['feedback_mode']); - $clone->cloze_text = $tt->string($normalized['cloze_text']); - $clone->textgap_rating = $tt->string($normalized['textgap_rating']); - $clone->identical_scoring = $tt->bool($normalized['identical_scoring']); - $clone->fixed_text_length = $tt->nullableInt($normalized['fixed_text_length']); - $clone->gap_combinations_exist = $tt->bool($normalized['gap_combinations_exist']); - $clone->gap_combinations = $normalized['gap_combinations']; - - foreach ($normalized['gaps'] as $gap) { - $type = $tt->int($gap['type']); - $clone->gaps[] = $tt->denormalize($gap, new assClozeGap($type)); - } + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static + { + $clone = parent::fromNormalized($normalized, $transformations); + $clone->feedbackMode = $transformations->string($normalized['feedback_mode']); + $clone->cloze_text = $transformations->string($normalized['cloze_text']); + $clone->textgap_rating = $transformations->string($normalized['textgap_rating']); + $clone->identical_scoring = $transformations->bool($normalized['identical_scoring']); + $clone->fixed_text_length = $transformations->nullableInt($normalized['fixed_text_length']); + $clone->gap_combinations_exist = $transformations->bool($normalized['gap_combinations_exist']); + $clone->gap_combinations = $normalized['gap_combinations']; + + foreach ($normalized['gaps'] as $gap) { + $type = $transformations->int($gap['type']); + $clone->gaps[] = $transformations->denormalize($gap, new assClozeGap($type)); + } - return $clone; - }); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php b/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php index b98d14cab935..91c63fa86a1d 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; @@ -37,7 +37,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assErrorText extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable, Normalizable +class assErrorText extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable, ToNormalized, FromNormalized { protected const ERROR_TYPE_WORD = 1; protected const ERROR_TYPE_PASSAGE = 2; @@ -1007,33 +1007,37 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): stri /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'errortext' => $this->errortext, 'errortext_parsed' => $this->parsed_errortext, - 'errordata' => $tt->normalize($this->errordata), + 'errordata' => $transformations->normalize($this->errordata), 'points_wrong' => $this->points_wrong, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->errortext = $tt->string($normalized['errortext']); - $clone->parsed_errortext = $normalized['errortext_parsed']; - $clone->points_wrong = $tt->nullableFloat($normalized['points_wrong']); - $clone->errordata = array_map( - fn(array $error) => $tt->denormalize($error, new assAnswerErrorText()), - $normalized['errordata'] - ); + $clone = parent::fromNormalized($normalized, $transformations); + $clone->errortext = $transformations->string($normalized['errortext']); + $clone->parsed_errortext = $normalized['errortext_parsed']; + $clone->points_wrong = $transformations->nullableFloat($normalized['points_wrong']); + $clone->errordata = array_map( + fn(array $error) => $transformations->denormalize($error, new assAnswerErrorText()), + $normalized['errordata'] + ); - return $clone; - }); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php b/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php index 8bdffab786e8..389ee090e78c 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\Test\Participants\ParticipantRepository; use ILIAS\Test\Logging\AdditionalInformationGenerator; @@ -38,7 +38,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assFileUpload extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjFileHandlingQuestionType, Normalizable +class assFileUpload extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjFileHandlingQuestionType, ToNormalized, FromNormalized { public const REUSE_FILES_TBL_POSTVAR = 'reusefiles'; public const DELETE_FILES_TBL_POSTVAR = 'deletefiles'; @@ -957,28 +957,32 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): stri /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'maxsize' => $this->maxsize, 'allowedextensions' => $this->allowedextensions, 'completion_by_submission' => $this->completion_by_submission, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->maxsize = $tt->nullableInt($normalized['maxsize']); - $clone->allowedextensions = $tt->string($normalized['allowedextensions']); - $clone->completion_by_submission = $tt->bool($normalized['completion_by_submission']); + $clone = parent::fromNormalized($normalized, $transformations); + $clone->maxsize = $transformations->nullableInt($normalized['maxsize']); + $clone->allowedextensions = $transformations->string($normalized['allowedextensions']); + $clone->completion_by_submission = $transformations->bool($normalized['completion_by_submission']); - return $clone; - }); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php index 8ad50006b908..b6ba5a4fede5 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; @@ -31,7 +31,7 @@ * @version $Id: class.assFormulaQuestion.php 1236 2010-02-15 15:44:16Z hschottm $ * @ingroup components\ILIASTestQuestionPool */ -class assFormulaQuestion extends assQuestion implements iQuestionCondition, QuestionAutosaveable, Normalizable +class assFormulaQuestion extends assQuestion implements iQuestionCondition, QuestionAutosaveable, ToNormalized, FromNormalized { private array $variables; private array $results; @@ -1493,34 +1493,38 @@ function (array $c, array $v): array { /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), - 'variables' => $tt->normalize($this->variables), - 'results' => $tt->normalize($this->results) - ]); + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), + 'variables' => $transformations->normalize($this->variables), + 'results' => $transformations->normalize($this->results) + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); + $clone = parent::fromNormalized($normalized, $transformations); - foreach ($normalized['variables'] as $key => $data) { - $dummy = new assFormulaQuestionVariable('', '', ''); - $clone->variables[$key] = $tt->denormalize($data, $dummy); - } + foreach ($normalized['variables'] as $key => $data) { + $dummy = new assFormulaQuestionVariable('', '', ''); + $clone->variables[$key] = $transformations->denormalize($data, $dummy); + } - foreach ($normalized['results'] as $key => $data) { - $dummy = new assFormulaQuestionResult('', '', '', 0, null, '', 0, 0); - $clone->results[$key] = $tt->denormalize($data, $dummy); - } + foreach ($normalized['results'] as $key => $data) { + $dummy = new assFormulaQuestionResult('', '', '', 0, null, '', 0, 0); + $clone->results[$key] = $transformations->denormalize($data, $dummy); + } - return $clone; - }); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php index 329dc24b70be..1d0193b0e305 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php @@ -17,9 +17,9 @@ *********************************************************************/ use ILIAS\Refinery\Factory as Refinery; -use ILIAS\Refinery\Transformation; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; /** * Formula Question Result @@ -27,7 +27,7 @@ * @version $Id: class.assFormulaQuestionResult.php 944 2009-11-09 16:11:30Z hschottm $ * @ingroup components\ILIASTestQuestionPool * */ -class assFormulaQuestionResult implements Normalizable +class assFormulaQuestionResult implements ToNormalized, FromNormalized { public const RESULT_NO_SELECTION = 0; public const RESULT_DEC = 1; @@ -869,17 +869,20 @@ public function getAvailableResultUnits($question_id): array /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - 'available_units' => $tt->normalize($this->available_units), + return [ + 'available_units' => $transformations->normalize($this->available_units), 'range_min' => $this->range_min, 'range_max' => $this->range_max, 'range_min_txt' => $this->range_min_txt, 'range_max_txt' => $this->range_max_txt, 'result' => $this->result, 'tolerance' => $this->tolerance, - 'unit' => $tt->normalize($this->unit), + 'unit' => $transformations->normalize($this->unit), 'formula' => $this->formula, 'points' => $this->points, 'precision' => $this->precision, @@ -888,37 +891,38 @@ public function toNormalized(Transformations $tt): Transformation 'rating_value' => $this->rating_value, 'rating_unit' => $this->rating_unit, 'result_type' => $this->result_type, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = clone $this; - $clone->available_units = array_map( - static fn(array $unit): assFormulaQuestionUnit => $tt->denormalize($unit, new assFormulaQuestionUnit()), - $normalized['available_units'] - ); - $clone->range_min = $tt->float($normalized['range_min']); - $clone->range_max = $tt->float($normalized['range_max']); - $clone->range_min_txt = $tt->string($normalized['range_min_txt']); - $clone->range_max_txt = $tt->string($normalized['range_max_txt']); - $clone->result = $tt->string($normalized['result']); - $clone->tolerance = $tt->float($normalized['tolerance']); - $clone->unit = $tt->denormalize($normalized['unit'], new assFormulaQuestionUnit()); - $clone->formula = $tt->string($normalized['formula']); - $clone->points = $tt->float($normalized['points']); - $clone->precision = $tt->int($normalized['precision']); - $clone->rating_simple = $tt->bool($normalized['rating_simple']); - $clone->rating_sign = $tt->float($normalized['rating_sign']); - $clone->rating_value = $tt->float($normalized['rating_value']); - $clone->rating_unit = $tt->float($normalized['rating_unit']); - $clone->result_type = $tt->int($normalized['result_type']); - - return $clone; - }); + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static + { + $clone = clone $this; + $clone->available_units = array_map( + static fn(array $unit): assFormulaQuestionUnit => $transformations->denormalize($unit, new assFormulaQuestionUnit()), + $normalized['available_units'] + ); + $clone->range_min = $transformations->float($normalized['range_min']); + $clone->range_max = $transformations->float($normalized['range_max']); + $clone->range_min_txt = $transformations->string($normalized['range_min_txt']); + $clone->range_max_txt = $transformations->string($normalized['range_max_txt']); + $clone->result = $transformations->string($normalized['result']); + $clone->tolerance = $transformations->float($normalized['tolerance']); + $clone->unit = $transformations->denormalize($normalized['unit'], new assFormulaQuestionUnit()); + $clone->formula = $transformations->string($normalized['formula']); + $clone->points = $transformations->float($normalized['points']); + $clone->precision = $transformations->int($normalized['precision']); + $clone->rating_simple = $transformations->bool($normalized['rating_simple']); + $clone->rating_sign = $transformations->float($normalized['rating_sign']); + $clone->rating_value = $transformations->float($normalized['rating_value']); + $clone->rating_unit = $transformations->float($normalized['rating_unit']); + $clone->result_type = $transformations->int($normalized['result_type']); + + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php index 085204feb2fe..a0f57031c4fd 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\Refinery\Transformation; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; /** @@ -28,7 +28,7 @@ * @author Helmut Schottmüller * @ingroup components\ILIASTestQuestionPool */ -class assFormulaQuestionUnit implements Normalizable +class assFormulaQuestionUnit implements ToNormalized, FromNormalized { private int $id = 0; private string $unit = ''; @@ -168,35 +168,39 @@ private function sanitizeString(string $string): string /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - 'id' => $tt->normalize(new Id($this->id, 'unit')), + return [ + 'id' => $transformations->normalize(new Id($this->id, 'unit')), 'unit' => $this->unit, 'factor' => $this->factor, - 'category_id' => $tt->normalize(new Id($this->category, 'unit_category')), + 'category_id' => $transformations->normalize(new Id($this->category, 'unit_category')), 'sequence' => $this->sequence, - 'baseunit' => $tt->normalize(new Id($this->baseunit, 'unit')), + 'baseunit' => $transformations->normalize(new Id($this->baseunit, 'unit')), 'baseunit_title' => $this->baseunit_title, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = clone $this; - $clone->id = $tt->denormalize($normalized['id'], Id::class)->getId(); - $clone->unit = $tt->string($normalized['unit']); - $clone->factor = $tt->float($normalized['factor']); - $clone->category = $tt->denormalize($normalized['category_id'], Id::class)->getId(); - $clone->sequence = $tt->int($normalized['sequence']); - $clone->baseunit = $tt->denormalize($normalized['baseunit'], Id::class)->getId(); - $clone->baseunit_title = $tt->string($normalized['baseunit_title']); - - return $clone; - }); + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static + { + $clone = clone $this; + $clone->id = $transformations->denormalize($normalized['id'], Id::class)->getId(); + $clone->unit = $transformations->string($normalized['unit']); + $clone->factor = $transformations->float($normalized['factor']); + $clone->category = $transformations->denormalize($normalized['category_id'], Id::class)->getId(); + $clone->sequence = $transformations->int($normalized['sequence']); + $clone->baseunit = $transformations->denormalize($normalized['baseunit'], Id::class)->getId(); + $clone->baseunit_title = $transformations->string($normalized['baseunit_title']); + + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php index 4ce48ecb7caf..97dd73b41bd0 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\Refinery\Transformation; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; /** @@ -28,7 +28,7 @@ * @author Helmut Schottmüller * @ingroup components\ILIASTestQuestionPool */ -class assFormulaQuestionUnitCategory implements Normalizable +class assFormulaQuestionUnitCategory implements ToNormalized, FromNormalized { private int $id = 0; private string $category = ''; @@ -95,27 +95,31 @@ private function sanitizeString(string $string): string /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - 'id' => $tt->normalize(new Id($this->id, 'unit_category')), + return [ + 'id' => $transformations->normalize(new Id($this->id, 'unit_category')), 'name' => $this->category, - 'question_id' => $tt->normalize(new Id($this->question_fi, 'question')), - ]); + 'question_id' => $transformations->normalize(new Id($this->question_fi, 'question')), + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = clone $this; - $clone->id = $tt->denormalize($normalized['id'], Id::class)->getId(); - $clone->category = $tt->string($normalized['name']); - $clone->question_fi = $tt->denormalize($normalized['question_id'], Id::class)->getId(); - - return $clone; - }); + $clone = clone $this; + $clone->id = $transformations->denormalize($normalized['id'], Id::class)->getId(); + $clone->category = $transformations->string($normalized['name']); + $clone->question_fi = $transformations->denormalize($normalized['question_id'], Id::class)->getId(); + + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionVariable.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionVariable.php index 3469ca82afcd..528327929bf0 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionVariable.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionVariable.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; /** * Formula Question Variable @@ -28,7 +28,7 @@ * @version $Id: class.assFormulaQuestionVariable.php 465 2009-06-29 08:27:36Z hschottm $ * @ingroup components\ILIASTestQuestionPool * */ -class assFormulaQuestionVariable implements Normalizable +class assFormulaQuestionVariable implements ToNormalized, FromNormalized { private $value = null; private float $range_min; @@ -205,37 +205,41 @@ public function getRangeMinTxt(): string /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ + return [ 'variable' => $this->variable, 'range_min' => $this->range_min, 'range_max' => $this->range_max, 'range_min_txt' => $this->range_min_txt, 'range_max_txt' => $this->range_max_txt, - 'unit' => $tt->normalize($this->unit), + 'unit' => $transformations->normalize($this->unit), 'precision' => $this->precision, 'intprecision' => $this->intprecision, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = clone $this; - $clone->variable = $tt->string($normalized['variable']); - $clone->range_min = $tt->float($normalized['range_min']); - $clone->range_max = $tt->float($normalized['range_max']); - $clone->range_min_txt = $tt->string($normalized['range_min_txt']); - $clone->range_max_txt = $tt->string($normalized['range_max_txt']); - $clone->unit = $tt->denormalize($normalized['unit'], new assFormulaQuestionUnit()); - $clone->precision = $tt->int($normalized['precision']); - $clone->intprecision = $tt->int($normalized['intprecision']); - - return $clone; - }); + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static + { + $clone = clone $this; + $clone->variable = $transformations->string($normalized['variable']); + $clone->range_min = $transformations->float($normalized['range_min']); + $clone->range_max = $transformations->float($normalized['range_max']); + $clone->range_min_txt = $transformations->string($normalized['range_min_txt']); + $clone->range_max_txt = $transformations->string($normalized['range_max_txt']); + $clone->unit = $transformations->denormalize($normalized['unit'], new assFormulaQuestionUnit()); + $clone->precision = $transformations->int($normalized['precision']); + $clone->intprecision = $transformations->int($normalized['intprecision']); + + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php index 34078ddbac20..7c78bc8a319d 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php @@ -18,14 +18,14 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\TestQuestionPool\RequestDataCollector; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\Refinery\Transformation; /** * Class for image map questions @@ -40,7 +40,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assImagemapQuestion extends assQuestion implements ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, Normalizable +class assImagemapQuestion extends assQuestion implements ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, ToNormalized, FromNormalized { private RequestDataCollector $request; // Hate it. @@ -935,31 +935,35 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), - 'image' => $tt->normalize(new QuestionImage($this->image_filename, $this->getId())), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), + 'image' => $transformations->normalize(new QuestionImage($this->image_filename, $this->getId())), 'multiple_choice' => $this->is_multiple_choice, - 'answers' => $tt->normalize($this->answers), - ]); + 'answers' => $transformations->normalize($this->answers), + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->image_filename = $tt->denormalize($normalized['image'], QuestionImage::class)->getFilename(); - $clone->is_multiple_choice = $tt->bool($normalized['multiple_choice']); - $clone->answers = array_map( - static fn(array $answer): ASS_AnswerImagemap => $tt->denormalize($answer, new ASS_AnswerImagemap()), - $normalized['answers'] - ); + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static + { + $clone = parent::fromNormalized($normalized, $transformations); + $clone->image_filename = $transformations->denormalize($normalized['image'], QuestionImage::class)->getFilename(); + $clone->is_multiple_choice = $transformations->bool($normalized['multiple_choice']); + $clone->answers = array_map( + static fn(array $answer): ASS_AnswerImagemap => $transformations->denormalize($answer, new ASS_AnswerImagemap()), + $normalized['answers'] + ); - return $clone; - }); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php index 475be6e0d14d..da0b620eb286 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php @@ -18,13 +18,13 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\ManipulateImagesInChoiceQuestionsTrait; use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\Refinery\Transformation; /** * @author Björn Heyser @@ -32,7 +32,7 @@ * * @package components\ILIAS/TestQuestionPool */ -class assKprimChoice extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, ilAssSpecificFeedbackOptionLabelProvider, QuestionLMExportable, QuestionAutosaveable, Normalizable +class assKprimChoice extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, ilAssSpecificFeedbackOptionLabelProvider, QuestionLMExportable, QuestionAutosaveable, ToNormalized, FromNormalized { use ManipulateImagesInChoiceQuestionsTrait; @@ -927,10 +927,13 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'shuffle_answers' => $this->shuffle_answers_enabled, 'answer_type' => $this->answerType, 'option_label' => $this->option_label, @@ -938,30 +941,31 @@ public function toNormalized(Transformations $tt): Transformation 'custom_false_option_label' => $this->customFalseOptionLabel, 'score_partial_solution' => $this->scorePartialSolutionEnabled, 'specific_feedback_setting' => $this->specific_feedback_setting, - 'answers' => $tt->normalize($this->answers, ['question_id' => $this->getId()]), - ]); + 'answers' => $transformations->normalize($this->answers, ['question_id' => $this->getId()]), + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->shuffle_answers_enabled = $tt->bool($normalized['shuffle_answers']); - $clone->answerType = $tt->string($normalized['answer_type']); - $clone->option_label = $tt->string($normalized['option_label']); - $clone->customTrueOptionLabel = $tt->string($normalized['custom_true_option_label']); - $clone->customFalseOptionLabel = $tt->string($normalized['custom_false_option_label']); - $clone->scorePartialSolutionEnabled = $tt->bool($normalized['score_partial_solution']); - $clone->specific_feedback_setting = $tt->int($normalized['specific_feedback_setting']); - $clone->answers = array_map( - static fn(array $answer): ilAssKprimChoiceAnswer => $tt->denormalize($answer, new ilAssKprimChoiceAnswer()), - $normalized['answers'] - ); + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static + { + $clone = parent::fromNormalized($normalized, $transformations); + $clone->shuffle_answers_enabled = $transformations->bool($normalized['shuffle_answers']); + $clone->answerType = $transformations->string($normalized['answer_type']); + $clone->option_label = $transformations->string($normalized['option_label']); + $clone->customTrueOptionLabel = $transformations->string($normalized['custom_true_option_label']); + $clone->customFalseOptionLabel = $transformations->string($normalized['custom_false_option_label']); + $clone->scorePartialSolutionEnabled = $transformations->bool($normalized['score_partial_solution']); + $clone->specific_feedback_setting = $transformations->int($normalized['specific_feedback_setting']); + $clone->answers = array_map( + static fn(array $answer): ilAssKprimChoiceAnswer => $transformations->denormalize($answer, new ilAssKprimChoiceAnswer()), + $normalized['answers'] + ); - return $clone; - }); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php b/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php index addc75f602c3..8bf5b7bcfcb1 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php @@ -18,14 +18,14 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\Refinery\Transformation; -class assLongMenu extends assQuestion implements ilObjQuestionScoringAdjustable, QuestionLMExportable, QuestionAutosaveable, Normalizable +class assLongMenu extends assQuestion implements ilObjQuestionScoringAdjustable, QuestionLMExportable, QuestionAutosaveable, ToNormalized, FromNormalized { public const ANSWER_TYPE_SELECT_VAL = 0; public const ANSWER_TYPE_TEXT_VAL = 1; @@ -840,34 +840,38 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'long_menu_text' => $this->long_menu_text, 'json_structure' => $this->json_structure, 'min_auto_complete' => $this->minAutoComplete, 'identical_scoring' => $this->identical_scoring, 'correct_answers' => $this->correct_answers, 'answers' => $this->answers, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->long_menu_text = $tt->string($normalized['long_menu_text']); - $clone->json_structure = $tt->string($normalized['json_structure']); - $clone->minAutoComplete = $tt->int($normalized['min_auto_complete']); - $clone->identical_scoring = $tt->bool($normalized['identical_scoring']); - $clone->correct_answers = $normalized['correct_answers']; - $clone->answers = $normalized['answers']; - - return $clone; - }); + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static + { + $clone = parent::fromNormalized($normalized, $transformations); + $clone->long_menu_text = $transformations->string($normalized['long_menu_text']); + $clone->json_structure = $transformations->string($normalized['json_structure']); + $clone->minAutoComplete = $transformations->int($normalized['min_auto_complete']); + $clone->identical_scoring = $transformations->bool($normalized['identical_scoring']); + $clone->correct_answers = $normalized['correct_answers']; + $clone->answers = $normalized['answers']; + + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php index 80979e2b8692..6ab6ce1e4101 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php @@ -18,14 +18,14 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; use ILIAS\Refinery\Random\Group as RandomGroup; use ILIAS\Refinery\Random\Seed\RandomSeed; -use ILIAS\Refinery\Transformation; /** * Class for matching questions @@ -40,7 +40,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assMatchingQuestion extends assQuestion implements ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable, Normalizable +class assMatchingQuestion extends assQuestion implements ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable, ToNormalized, FromNormalized { public const MT_TERMS_PICTURES = 0; public const MT_TERMS_DEFINITIONS = 1; @@ -1430,40 +1430,44 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'shuffle_mode' => $this->shufflemode, 'matching_mode' => $this->matching_mode, 'matching_type' => $this->matching_type, 'thumb_geometry' => $this->thumb_geometry, - 'matching_pairs' => $tt->normalize($this->matchingpairs, ['question_id' => $this->getId()]), - ]); + 'matching_pairs' => $transformations->normalize($this->matchingpairs, ['question_id' => $this->getId()]), + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->shufflemode = $tt->int($normalized['shuffle_mode']); - $clone->matching_mode = $tt->string($normalized['matching_mode']); - $clone->matching_type = $tt->int($normalized['matching_type']); - $clone->thumb_geometry = $tt->int($normalized['thumb_geometry']); - - foreach ($normalized['matching_pairs'] as $matching_pair) { - $term = $tt->denormalize($matching_pair['term'], new assAnswerMatchingTerm()); - $definition = $tt->denormalize($matching_pair['definition'], new assAnswerMatchingDefinition()); - - $clone->matchingpairs[] = new assAnswerMatchingPair($term, $definition, $tt->float($matching_pair['points'])); - $clone->terms[] = $term; - $clone->definitions[] = $definition; - } + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static + { + $clone = parent::fromNormalized($normalized, $transformations); + $clone->shufflemode = $transformations->int($normalized['shuffle_mode']); + $clone->matching_mode = $transformations->string($normalized['matching_mode']); + $clone->matching_type = $transformations->int($normalized['matching_type']); + $clone->thumb_geometry = $transformations->int($normalized['thumb_geometry']); + + foreach ($normalized['matching_pairs'] as $matching_pair) { + $term = $transformations->denormalize($matching_pair['term'], new assAnswerMatchingTerm()); + $definition = $transformations->denormalize($matching_pair['definition'], new assAnswerMatchingDefinition()); + + $clone->matchingpairs[] = new assAnswerMatchingPair($term, $definition, $transformations->float($matching_pair['points'])); + $clone->terms[] = $term; + $clone->definitions[] = $definition; + } - return $clone; - }); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php index afbe2646a5aa..6b66e5cccfb9 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php @@ -18,13 +18,13 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\ManipulateImagesInChoiceQuestionsTrait; use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\Refinery\Transformation; /** * Class for multiple choice tests. @@ -41,7 +41,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assMultipleChoice extends assQuestion implements ilObjAnswerScoringAdjustable, iQuestionCondition, ilAssSpecificFeedbackOptionLabelProvider, QuestionLMExportable, QuestionAutosaveable, Normalizable +class assMultipleChoice extends assQuestion implements ilObjAnswerScoringAdjustable, iQuestionCondition, ilAssSpecificFeedbackOptionLabelProvider, QuestionLMExportable, QuestionAutosaveable, ToNormalized, FromNormalized { use ManipulateImagesInChoiceQuestionsTrait; @@ -963,31 +963,35 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'selection_limit' => $this->selection_limit, 'single_line' => $this->is_singleline, - 'answers' => $tt->normalize($this->answers, ['question_id' => $this->getId()]), - ]); + 'answers' => $transformations->normalize($this->answers, ['question_id' => $this->getId()]), + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->selection_limit = $tt->nullableInt($normalized['selection_limit']); - $clone->is_singleline = $tt->bool($normalized['single_line']); - $clone->answers = array_map( - static fn(array $answer): ASS_AnswerMultipleResponseImage => $tt->denormalize($answer, new ASS_AnswerMultipleResponseImage()), - $normalized['answers'] - ); + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static + { + $clone = parent::fromNormalized($normalized, $transformations); + $clone->selection_limit = $transformations->nullableInt($normalized['selection_limit']); + $clone->is_singleline = $transformations->bool($normalized['single_line']); + $clone->answers = array_map( + static fn(array $answer): ASS_AnswerMultipleResponseImage => $transformations->denormalize($answer, new ASS_AnswerMultipleResponseImage()), + $normalized['answers'] + ); - return $clone; - }); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php b/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php index cb598474a73e..99e6bde05fc7 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php @@ -18,11 +18,11 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\Refinery\Transformation; /** * Class for numeric questions @@ -39,7 +39,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assNumeric extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionAutosaveable, Normalizable +class assNumeric extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionAutosaveable, ToNormalized, FromNormalized { protected $lower_limit; protected $upper_limit; @@ -470,28 +470,32 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): stri /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'lower_limit' => $this->lower_limit, 'upper_limit' => $this->upper_limit, 'maxchars' => $this->maxchars, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->lower_limit = $tt->string($normalized['lower_limit']); - $clone->upper_limit = $tt->string($normalized['upper_limit']); - $clone->maxchars = $tt->int($normalized['maxchars']); - - return $clone; - }); + $clone = parent::fromNormalized($normalized, $transformations); + $clone->lower_limit = $transformations->string($normalized['lower_limit']); + $clone->upper_limit = $transformations->string($normalized['upper_limit']); + $clone->maxchars = $transformations->int($normalized['maxchars']); + + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php b/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php index d38540a1d712..a435a4c49f0b 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php @@ -18,12 +18,12 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\Refinery\Transformation; /** * Class for horizontal ordering questions @@ -36,7 +36,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assOrderingHorizontal extends assQuestion implements ilObjQuestionScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable, Normalizable +class assOrderingHorizontal extends assQuestion implements ilObjQuestionScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable, ToNormalized, FromNormalized { protected const HAS_SPECIFIC_FEEDBACK = false; protected const DEFAULT_TEXT_SIZE = 100; @@ -589,30 +589,34 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): stri /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'ordertext' => $this->ordertext, 'textsize' => $this->textsize, 'separator' => $this->separator, 'answer_separator' => $this->answer_separator, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->ordertext = $tt->string($normalized['ordertext']); - $clone->textsize = $tt->float($normalized['textsize']); - $clone->separator = $tt->string($normalized['separator']); - $clone->answer_separator = $tt->string($normalized['answer_separator']); - - return $clone; - }); + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static + { + $clone = parent::fromNormalized($normalized, $transformations); + $clone->ordertext = $transformations->string($normalized['ordertext']); + $clone->textsize = $transformations->float($normalized['textsize']); + $clone->separator = $transformations->string($normalized['separator']); + $clone->answer_separator = $transformations->string($normalized['answer_separator']); + + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php index a6472faa4e14..6ac3f13f6e9b 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php @@ -18,10 +18,10 @@ declare(strict_types=1); -use ILIAS\Refinery\Transformation; use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\Questions\Ordering\OrderingQuestionDatabaseRepository as OQRepository; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; @@ -40,7 +40,7 @@ * * @ingroup components\ILIASTestQuestionPool */ -class assOrderingQuestion extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable, Normalizable +class assOrderingQuestion extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable, ToNormalized, FromNormalized { public const ORDERING_ELEMENT_FORM_FIELD_POSTVAR = 'order_elems'; @@ -1373,33 +1373,37 @@ function (ilAssOrderingElement $v): string { /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'ordering_type' => $this->ordering_type, - 'ordering_elements' => $tt->normalize( + 'ordering_elements' => $transformations->normalize( $this->getOrderingElementList()->getElements(), ['question_id' => $this->getId()] ), - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->ordering_type = $tt->int($normalized['ordering_type']); - $denormalized_elements = array_map( - static fn(array $element): ilAssOrderingElement => $tt->denormalize($element, new ilAssOrderingElement()), - $normalized['ordering_elements'] - ); - $clone->element_list_for_deferred_saving = new ilAssOrderingElementList(null, $denormalized_elements); + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static + { + $clone = parent::fromNormalized($normalized, $transformations); + $clone->ordering_type = $transformations->int($normalized['ordering_type']); + $denormalized_elements = array_map( + static fn(array $element): ilAssOrderingElement => $transformations->denormalize($element, new ilAssOrderingElement()), + $normalized['ordering_elements'] + ); + $clone->element_list_for_deferred_saving = new ilAssOrderingElementList(null, $denormalized_elements); - return $clone; - }); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php index ca0a2bfdd85f..b11ba7d41b7e 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php @@ -36,7 +36,7 @@ use ILIAS\Test\Logging\TestQuestionAdministrationInteractionTypes; use ILIAS\Test\Results\Data\Repository as TestResultRepository; use ILIAS\Test\TestDIC; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\TestQuestionPool\Questions\Files\QuestionFiles; @@ -2965,13 +2965,17 @@ public function getVariablesAsTextArray( } /** - * @inheritDoc + * @param array $context + * @return array|float|bool|int|string|null */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - 'id' => $tt->normalize(new Id($this->id, 'question')), - 'parent_id' => $tt->normalize(new Id($this->obj_id, 'object')), + return [ + 'id' => $transformations->normalize(new Id($this->id, 'question')), + 'parent_id' => $transformations->normalize(new Id($this->obj_id, 'object')), 'original_id' => $this->original_id, 'external_id' => $this->external_id, 'type' => $this->getQuestionType(), @@ -2981,49 +2985,48 @@ public function toNormalized(Transformations $tt): Transformation 'question_text' => $this->question, 'available_points' => $this->points, 'nr_of_tries' => $this->nr_of_tries, - 'lifecycle' => $tt->normalize($this->lifecycle), + 'lifecycle' => $transformations->normalize($this->lifecycle), 'author' => $this->author, 'updated_timestamp' => $this->lastChange, 'additional_content_editing_mode' => $this->additionalContentEditingMode, 'thumb_size' => $this->thumb_size, 'shuffle' => $this->shuffle, - 'suggested_solutions' => $tt->normalize($this->suggested_solutions), - ]); + 'suggested_solutions' => $transformations->normalize($this->suggested_solutions), + ]; } - /** - * @inheritDoc - */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = clone $this; - $clone->id = $tt->denormalize($normalized['id'], Id::class)->getId(); - $clone->obj_id = $tt->denormalize($normalized['parent_id'], Id::class)->getId(); - $clone->original_id = $tt->nullableInt($normalized['original_id']); - $clone->external_id = $tt->nullableString($normalized['external_id']); - $clone->owner = $tt->int($normalized['owner']); - $clone->title = $tt->string($normalized['title']); - $clone->comment = $tt->string($normalized['description']); - $clone->question = $tt->string($normalized['question_text']); - $clone->points = $tt->float($normalized['available_points']); - $clone->nr_of_tries = $tt->int($normalized['nr_of_tries']); - $clone->lifecycle = $tt->denormalize($normalized['lifecycle'], $clone->lifecycle); - $clone->author = $tt->string($normalized['author']); - $clone->lastChange = $tt->nullableInt($normalized['updated_timestamp']); - $clone->additionalContentEditingMode = $tt->string($normalized['additional_content_editing_mode']); - $clone->thumb_size = $tt->int($normalized['thumb_size']); - $clone->shuffle = $tt->bool($normalized['shuffle']); - - $clone->suggested_solutions = array_map( - static fn(array $suggested_solution): SuggestedSolution => $tt->denormalize( - $suggested_solution, - SuggestedSolution::class - ), - $normalized['suggested_solutions'] - ); + /** @param array $normalized */ + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static + { + $clone = clone $this; + $clone->id = $transformations->denormalize($normalized['id'], Id::class)->getId(); + $clone->obj_id = $transformations->denormalize($normalized['parent_id'], Id::class)->getId(); + $clone->original_id = $transformations->nullableInt($normalized['original_id']); + $clone->external_id = $transformations->nullableString($normalized['external_id']); + $clone->owner = $transformations->int($normalized['owner']); + $clone->title = $transformations->string($normalized['title']); + $clone->comment = $transformations->string($normalized['description']); + $clone->question = $transformations->string($normalized['question_text']); + $clone->points = $transformations->float($normalized['available_points']); + $clone->nr_of_tries = $transformations->int($normalized['nr_of_tries']); + $clone->lifecycle = $transformations->denormalize($normalized['lifecycle'], $clone->lifecycle); + $clone->author = $transformations->string($normalized['author']); + $clone->lastChange = $transformations->nullableInt($normalized['updated_timestamp']); + $clone->additionalContentEditingMode = $transformations->string($normalized['additional_content_editing_mode']); + $clone->thumb_size = $transformations->int($normalized['thumb_size']); + $clone->shuffle = $transformations->bool($normalized['shuffle']); + + $clone->suggested_solutions = array_map( + static fn(array $suggested_solution): SuggestedSolution => $transformations->denormalize( + $suggested_solution, + SuggestedSolution::class + ), + $normalized['suggested_solutions'] + ); - return $clone; - }); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php index 150c10d4dc16..4fd9ef2864ea 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php @@ -18,13 +18,13 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\ManipulateImagesInChoiceQuestionsTrait; use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\Refinery\Transformation; /** * Class for single choice questions @@ -39,7 +39,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assSingleChoice extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, ilAssSpecificFeedbackOptionLabelProvider, QuestionLMExportable, QuestionAutosaveable, Normalizable +class assSingleChoice extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, ilAssSpecificFeedbackOptionLabelProvider, QuestionLMExportable, QuestionAutosaveable, ToNormalized, FromNormalized { use ManipulateImagesInChoiceQuestionsTrait; @@ -951,31 +951,33 @@ function (array $c, ASS_AnswerBinaryStateImage $v): array { /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(function (array $normalized) use ($tt): array { - $normalized = $tt->normalize(parent::toNormalized($tt)); - $normalized['is_singleline'] = $this->is_singleline; - $normalized['feedback_setting'] = $this->feedback_setting; - $normalized['answers'] = $tt->normalize($this->answers, ['question_id' => $this->getId()]); - return $normalized; - }); + $normalized = $transformations->normalize(parent::toNormalized($transformations, $context)); + $normalized['is_singleline'] = $this->is_singleline; + $normalized['feedback_setting'] = $this->feedback_setting; + $normalized['answers'] = $transformations->normalize($this->answers, ['question_id' => $this->getId()]); + return $normalized; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->is_singleline = $tt->bool($normalized['is_singleline']); - $clone->feedback_setting = $tt->int($normalized['feedback_setting']); - $clone->answers = array_map( - static fn(array $answer): ASS_AnswerBinaryStateImage => $tt->denormalize($answer, new ASS_AnswerBinaryStateImage()), - $normalized['answers'] - ); - return $clone; - }); + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static + { + $clone = parent::fromNormalized($normalized, $transformations); + $clone->is_singleline = $transformations->bool($normalized['is_singleline']); + $clone->feedback_setting = $transformations->int($normalized['feedback_setting']); + $clone->answers = array_map( + static fn(array $answer): ASS_AnswerBinaryStateImage => $transformations->denormalize($answer, new ASS_AnswerBinaryStateImage()), + $normalized['answers'] + ); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php index 19cee4cf0744..5d5d83384d28 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php @@ -18,10 +18,10 @@ declare(strict_types=1); -use ILIAS\Refinery\Transformation; use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; @@ -38,7 +38,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assTextQuestion extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, QuestionLMExportable, QuestionAutosaveable, Normalizable +class assTextQuestion extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, QuestionLMExportable, QuestionAutosaveable, ToNormalized, FromNormalized { protected const HAS_SPECIFIC_FEEDBACK = false; @@ -862,40 +862,44 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'word_counter_enabled' => $this->word_counter_enabled, 'max_num_of_chars' => $this->max_num_of_chars, 'text_rating' => $this->text_rating, 'matchcondition' => $this->matchcondition, 'keyword_relation' => $this->keyword_relation, - 'answers' => $tt->normalize($this->answers), - ]); + 'answers' => $transformations->normalize($this->answers), + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->word_counter_enabled = $tt->bool($normalized['word_counter_enabled']); - $clone->max_num_of_chars = $tt->int($normalized['max_num_of_chars']); - $clone->text_rating = $tt->string($normalized['text_rating']); - $clone->matchcondition = $tt->int($normalized['matchcondition']); - $clone->keyword_relation = $tt->string($normalized['keyword_relation']); - $clone->answers = array_map( - static fn(array $answer): ASS_AnswerMultipleResponseImage => $tt->denormalize( - $answer, - new ASS_AnswerMultipleResponseImage() - ), - $normalized['answers'] - ); + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static + { + $clone = parent::fromNormalized($normalized, $transformations); + $clone->word_counter_enabled = $transformations->bool($normalized['word_counter_enabled']); + $clone->max_num_of_chars = $transformations->int($normalized['max_num_of_chars']); + $clone->text_rating = $transformations->string($normalized['text_rating']); + $clone->matchcondition = $transformations->int($normalized['matchcondition']); + $clone->keyword_relation = $transformations->string($normalized['keyword_relation']); + $clone->answers = array_map( + static fn(array $answer): ASS_AnswerMultipleResponseImage => $transformations->denormalize( + $answer, + new ASS_AnswerMultipleResponseImage() + ), + $normalized['answers'] + ); - return $clone; - }); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php b/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php index 41017b4d8873..aa3790fff8fe 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php @@ -18,10 +18,10 @@ declare(strict_types=1); -use ILIAS\Refinery\Transformation; use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; @@ -40,7 +40,7 @@ * * @ingroup ModulesTestQuestionPool */ -class assTextSubset extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable, Normalizable +class assTextSubset extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition, QuestionLMExportable, QuestionAutosaveable, ToNormalized, FromNormalized { public array $answers = []; public int $correctanswers = 0; @@ -762,33 +762,37 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ - ...$tt->normalize(parent::toNormalized($tt)), + return [ + ...$transformations->normalize(parent::toNormalized($transformations, $context)), 'text_rating' => $this->text_rating, 'correct_answers' => $this->correctanswers, - 'answers' => $tt->normalize($this->answers), - ]); + 'answers' => $transformations->normalize($this->answers), + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = parent::fromNormalized($tt)->transform($normalized); - $clone->text_rating = $tt->string($normalized['text_rating']); - $clone->correctanswers = $tt->int($normalized['correct_answers']); - $clone->answers = array_map( - static fn(array $answer): ASS_AnswerBinaryStateImage => $tt->denormalize( - $answer, - new ASS_AnswerBinaryStateImage() - ), - $normalized['answers'] - ); - return $clone; - }); + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static + { + $clone = parent::fromNormalized($normalized, $transformations); + $clone->text_rating = $transformations->string($normalized['text_rating']); + $clone->correctanswers = $transformations->int($normalized['correct_answers']); + $clone->answers = array_map( + static fn(array $answer): ASS_AnswerBinaryStateImage => $transformations->denormalize( + $answer, + new ASS_AnswerBinaryStateImage() + ), + $normalized['answers'] + ); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php b/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php index 25449d7ab303..a1b1ef8bb988 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php @@ -1,9 +1,9 @@ getImageWebDir() . $this->getThumbPrefix() . $this->getImageFile(); } - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(array $context): array => [ + return [ 'position' => $this->position, 'answertext' => $this->answertext, 'image' => $this->imageFile - ? $tt->normalize(new QuestionImage($this->imageFile, $context['question_id'] ?? null)) + ? $transformations->normalize(new QuestionImage($this->imageFile, $context['question_id'] ?? null)) : null, 'correctness' => $this->correctness, - ]); + ]; } - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): static { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = clone $this; - $clone->position = $tt->int($normalized['position']); - $clone->answertext = $tt->nullableString($normalized['answertext']); - $clone->imageFile = $tt->denormalize($normalized['image'], QuestionImage::class)?->getFilename(); - $clone->correctness = $tt->int($normalized['correctness']); - return $clone; - }); + $clone = clone $this; + $clone->position = $transformations->int($normalized['position']); + $clone->answertext = $transformations->nullableString($normalized['answertext']); + $clone->imageFile = $transformations->denormalize($normalized['image'], QuestionImage::class)?->getFilename(); + $clone->correctness = $transformations->int($normalized['correctness']); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php index 8b7c174f35b4..7f52cb345b3d 100755 --- a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php +++ b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php @@ -18,10 +18,10 @@ declare(strict_types=1); -use ILIAS\Refinery\Transformation; use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; /** @@ -31,7 +31,7 @@ * @version $Id$ * @package Modules/TestQuestionPool */ -class ilAssOrderingElement implements Normalizable +class ilAssOrderingElement implements ToNormalized, FromNormalized { public const EXPORT_IDENT_PROPERTY_SEPARATOR = '_'; @@ -450,40 +450,44 @@ public function withContent(string $content): self /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(array $options): array => [ - 'id' => $tt->normalize(new Id($this->id, 'ordering')), + return [ + 'id' => $transformations->normalize(new Id($this->id, 'ordering')), 'random_identifier' => $this->random_identifier, 'solution_identifier' => $this->solution_identifier, 'position' => $this->position, 'indentation' => $this->indentation, - 'content' => $this->content ? $tt->normalize( - new QuestionImage($this->content, $options['question_id'] ?? null) + 'content' => $this->content ? $transformations->normalize( + new QuestionImage($this->content, $context['question_id'] ?? null) ) : null, - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation - { - return $tt->custom()->transformation(function (array $normalized) use ($tt): self { - $clone = $this->withRandomIdentifier($tt->int($normalized['random_identifier'])) - ->withSolutionIdentifier($tt->int($normalized['solution_identifier'])) - ->withPosition($tt->int($normalized['position'])) - ->withIndentation($tt->int($normalized['indentation'])); - - if (is_array($normalized['content'])) { - $clone->setContent($tt->denormalize($normalized['content'], QuestionImage::class)?->getFilename()); - } else { - $clone->setContent((string) $normalized['content']); - } + public function fromNormalized( + array $normalized, + Transformations $transformations + ): self + { + $clone = $this->withRandomIdentifier($transformations->int($normalized['random_identifier'])) + ->withSolutionIdentifier($transformations->int($normalized['solution_identifier'])) + ->withPosition($transformations->int($normalized['position'])) + ->withIndentation($transformations->int($normalized['indentation'])); + + if (is_array($normalized['content'])) { + $clone->setContent($transformations->denormalize($normalized['content'], QuestionImage::class)?->getFilename()); + } else { + $clone->setContent((string) $normalized['content']); + } - $clone->setId($tt->denormalize($normalized['id'], Id::class)->getId()); + $clone->setId($transformations->denormalize($normalized['id'], Id::class)->getId()); - return $clone; - }); + return $clone; } } diff --git a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionLifecycle.php b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionLifecycle.php index 49ba023e22c2..24c0a24e8ce9 100755 --- a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionLifecycle.php +++ b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionLifecycle.php @@ -16,16 +16,16 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\Refinery\Transformation; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; /** * Class ilAssQuestionLifecycle * @author Björn Heyser * @package components\ILIAS/TestQuestionPool */ -class ilAssQuestionLifecycle implements Normalizable +class ilAssQuestionLifecycle implements ToNormalized, FromNormalized { public const DRAFT = 'draft'; public const REVIEW = 'review'; @@ -161,20 +161,24 @@ public static function getDraftInstance(): self /** * @inheritDoc */ - public function toNormalized(Transformations $tt): Transformation + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null { - return $tt->custom()->transformation(fn(): array => [ + return [ 'identifier' => $this->getIdentifier(), - ]); + ]; } /** * @inheritDoc */ - public function fromNormalized(Transformations $tt): Transformation + public function fromNormalized( + array $normalized, + Transformations $transformations + ): self { - return $tt->custom()->transformation( - static fn(array $normalized): ilAssQuestionLifecycle => self::getInstance($normalized['identifier']) - ); + return self::getInstance($normalized['identifier']); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/Feedback.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/Feedback.php index 7450284c25d0..d2190d8514ec 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/Feedback.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/Feedback.php @@ -21,7 +21,7 @@ namespace ILIAS\TestQuestionPool\ExportImport\Envelopes; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Envelope; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; /** diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/QuestionImage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/QuestionImage.php index bc3f6621e67b..f2221793f476 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/QuestionImage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Envelopes/QuestionImage.php @@ -21,7 +21,7 @@ namespace ILIAS\TestQuestionPool\ExportImport\Envelopes; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Envelope; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; /** * Wrapper object for an image file within a question (e.g. an image map question). diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolCollector.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolCollector.php index 6117764f053d..00125011c7d6 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolCollector.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolCollector.php @@ -22,7 +22,6 @@ use ilDBInterface; use ILIAS\Data\ObjectId; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\DataCollector; use ILIAS\TestQuestionPool\Questions\GeneralQuestionProperties; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ilObjQuestionPool; @@ -30,7 +29,7 @@ /** * Collector to aggregate data from the question pool for export. */ -class QuestionPoolCollector implements DataCollector +class QuestionPoolCollector { use CollectsQuestions; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php index 5aa1af67461c..a05acf64ca63 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php @@ -27,23 +27,21 @@ use ILIAS\Data\UUID\Factory as UUIDFactory; use ILIAS\Taxonomy\DomainService as Taxonomy; use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportStep; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ExportDependencies; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportState; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Exporter; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Serializer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\TransformationsBuilder; use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; /** - * Orchestrates the export of a question pool. It uses the Builder to create a pipeline of transformations that are used - * to normalize the data and then writes the normalized data to the serializer. It also copies the needed files to the - * export directory. + * Orchestrates normalization and serialization of a question pool export and copies its files. */ class QuestionPoolExporter implements Exporter { public function __construct( - private readonly Builder $builder, + private readonly TransformationsBuilder $builder, private readonly DataFactory $data_factory, private readonly GeneralQuestionPropertiesRepository $question_repository, private readonly ilDBInterface $db, @@ -52,9 +50,9 @@ public function __construct( } /** - * Prepares the export by creating the transformations and the question image pipe. + * Prepares the export transformations and question image collector. */ - public function prepare(ExportDependencies $state): void + public function prepare(ExportState $state): void { $state->logger()->info('Preparing question pool export (1/3)...'); $state->assertStep(ExportStep::INIT); @@ -73,11 +71,9 @@ public function prepare(ExportDependencies $state): void ); $state->setCollector($collector); - $question_image_pipe = new CollectQuestionImages(new UUIDFactory(), $pool_id); + $question_image_collector = new CollectQuestionImages(new UUIDFactory(), $pool_id); - $transformations = $this->builder - ->withAdditionalPipes([$question_image_pipe]) - ->create(); + $transformations = $this->builder->forExport($question_image_collector); $state->setTransformations($transformations); $state->logger()->info('...Finished preparing question pool export (1/3)'); @@ -87,7 +83,7 @@ public function prepare(ExportDependencies $state): void * Normalizes the question pool object and its questions and writes them to the serializer. It also collects the * dependencies of the export. */ - public function process(ExportDependencies $state): void + public function process(ExportState $state): void { $state->logger()->info('Processing question pool export (2/3)...'); $state->assertStep(ExportStep::PREPARE); @@ -96,7 +92,7 @@ public function process(ExportDependencies $state): void $state->serializer()->group( 'general', fn() => $this->exportObject( - $state->collector(), + $this->collector($state), $state->transformations(), $state->serializer(), $state @@ -105,7 +101,7 @@ public function process(ExportDependencies $state): void $state->serializer()->group( 'questions', fn() => $this->exportQuestions( - $state->collector(), + $this->collector($state), $state->transformations(), $state->serializer(), $state @@ -114,7 +110,7 @@ public function process(ExportDependencies $state): void $state->serializer()->group( 'skill_assignments', fn() => $this->exportSkillAssignments( - $state->collector(), + $this->collector($state), $state->transformations(), $state->serializer(), ) @@ -126,17 +122,21 @@ public function process(ExportDependencies $state): void /** * Finalizes the export by copying the question images to the export directory and returning the export context. */ - public function write(ExportDependencies $state): void + public function write(ExportState $state): void { $state->logger()->info('Writing question pool export (3/3)...'); $state->assertStep(ExportStep::PROCESS); $state->setStep(ExportStep::WRITE); $export_dir = $state->path()->getPathToComponentExpDirInContainer(); - $question_image_pipe = $state->transformations()->context(CollectQuestionImages::class); + $question_image_collector = $state->transformations() + ->normalizationProcessor(CollectQuestionImages::class); + if (!($question_image_collector instanceof CollectQuestionImages)) { + throw new \LogicException('Unexpected question image processor'); + } $state->logger()->debug("Copying question images to export directory {$export_dir}"); - foreach ($question_image_pipe->getFiles() as $file) { + foreach ($question_image_collector->getFiles() as $file) { $state->writer()->writeFileByFilePath( $file['from'], "{$export_dir}/" . $file['to'] @@ -148,7 +148,7 @@ public function write(ExportDependencies $state): void } - private function extractObjectId(ExportDependencies $state): ?ObjectId + private function extractObjectId(ExportState $state): ?ObjectId { $target_ids = $state->target()->getObjectIds(); @@ -166,11 +166,21 @@ private function extractObjectId(ExportDependencies $state): ?ObjectId return $this->data_factory->objId(array_shift($target_ids)); } + private function collector(ExportState $state): QuestionPoolCollector + { + $collector = $state->collector(); + if (!($collector instanceof QuestionPoolCollector)) { + throw new \LogicException('Unexpected question pool collector'); + } + + return $collector; + } + private function exportObject( QuestionPoolCollector $collector, Transformations $transformations, Serializer $serializer, - ExportDependencies $state + ExportState $state ): void { $serializer->append('object', $transformations->normalize($collector->getObject())); @@ -189,7 +199,7 @@ private function exportQuestions( QuestionPoolCollector $collector, Transformations $transformations, Serializer $serializer, - ExportDependencies $state + ExportState $state ): void { foreach ($collector->getQuestionObjects() as $question) { $normalized = [ diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php index 2bae15d3b356..4519ee26ac06 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Bridge/ExportState.php @@ -24,25 +24,23 @@ use ILIAS\Export\ExportHandler\I\Consumer\ExportWriter\HandlerInterface as ExportWriter; use ILIAS\Export\ExportHandler\I\Info\Export\Path\HandlerInterface as ExportPath; use ILIAS\Export\ExportHandler\I\Target\HandlerInterface as ExportTarget; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\DataCollector; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ExportDependencies; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Serializer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use Psr\Log\LoggerInterface as Logger; use RuntimeException; -class ExportState implements ExportDependencies +class ExportState { private ExportStep $step = ExportStep::INIT; private ?Logger $logger = null; private ?ExportPath $path_info = null; private ?Transformations $transformations = null; - private ?DataCollector $collector = null; + private ?object $collector = null; private ?Serializer $serializer = null; private ?ExportWriter $writer = null; /** - * @var array}> $dependencies + * @var array}> $dependencies */ private array $dependencies = []; @@ -109,13 +107,13 @@ public function setPathInfo(ExportPath $path_info): void $this->path_info = $path_info; } - public function collector(): DataCollector + public function collector(): object { $this->assertNotNull($this->collector, 'collector'); return $this->collector; } - public function setCollector(DataCollector $collector): void + public function setCollector(object $collector): void { $this->collector = $collector; } @@ -153,11 +151,13 @@ public function setWriter(ExportWriter $writer): void $this->writer = $writer; } + /** @return list}> */ public function getDependencies(): array { return array_values($this->dependencies); } + /** @param list $ids */ public function addDependency(string $component, string $entity, array $ids): void { $key = "{$component}::{$entity}"; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Builder.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Builder.php deleted file mode 100644 index 724f5cd01b88..000000000000 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Builder.php +++ /dev/null @@ -1,292 +0,0 @@ - $prepend_pipes */ - private array $prepend_pipes = []; - - /** @var list $append_pipes */ - private array $append_pipes = []; - - /** @var list $containers */ - private array $containers = []; - - public function __construct( - private readonly ILIASContainer $dic, - Container ...$local_containers, - ) { - $this->containers = $local_containers; - } - - /* - Fluent interface methods - */ - - public function withDefaultNormalizers(bool $enable = true): self - { - $clone = clone $this; - $clone->default_normalizers = $enable; - return $clone; - } - - public function withLegacyNormalizers(string $version): self - { - $clone = clone $this; - $clone->legacy_version = $version; - return $clone; - } - - /** - * @param list $append - * @param list $prepend - */ - public function withAdditionalPipes(array $prepend = [], array $append = []): self - { - $clone = clone $this; - $clone->append_pipes += $append; - $clone->prepend_pipes += $prepend; - return $clone; - } - - /* - Object creation - */ - - /** - * Create a Transformations instance which was configured by the builder. - */ - - public function create(): TransformationsContract - { - $pipeline = new Pipeline(); - $object = new Transformations($this->dic->refinery(), $pipeline); - - foreach ($this->prepend_pipes as $pipe) { - $pipeline->pipe($pipe); - } - - if ($this->legacy_version !== null) { - $registry = $this->buildRegistry($object, $this->legacy_version); - $pipeline->pipe(new NormalizingPipe($registry)); - $pipeline->pipe(new DenormalizingPipe($registry)); - } - - if ($this->default_normalizers) { - $registry = $this->buildRegistry($object); - $pipeline->pipe(new NormalizingPipe($registry)); - $pipeline->pipe(new DenormalizingPipe($registry)); - } - - foreach ($this->append_pipes as $pipe) { - $pipeline->pipe($pipe); - } - - $pipeline->pipe(new FinalizeNormalizing()); - - return $object; - } - - /** - * Register all normalizer classes from the type map artifact by checking for the given version and skipping if - * the normalizer is already registered. - */ - private function buildRegistry(Transformations $object, string $version = NormalizerArtifactObjective::DEFAULT_KEY): Registry - { - $type_map = require NormalizerArtifactObjective::PATH(); - $registry = new Registry(); - - foreach ($type_map as $type => $normalizer_classes) { - if ($registry->hasNormalizer($type)) { - continue; - } - - $resolved_key = $this->resolveVersionKey($version, $normalizer_classes); - if (!isset($normalizer_classes[$resolved_key])) { - continue; - } - - $registry->registerNormalizer( - $type, - fn() => $this->createInstance($normalizer_classes[$resolved_key], $object) - ); - } - - return $registry; - } - - /** - * Resolve the best matching version key from the available normalizer versions. - * - * Priority: exact match > nearest concrete version before requested > wildcard (major.*) > default. - * Uses version_compare for comparing ILIAS versions (major.minor, optional suffixes like alpha). - * - * @param array $available_versions - */ - private function resolveVersionKey(string $requested_version, array $available_versions): string - { - if (isset($available_versions[$requested_version])) { - return $requested_version; - } - - if ($requested_version === NormalizerArtifactObjective::DEFAULT_KEY) { - return $requested_version; - } - - $requested_major = strstr($requested_version, '.', true) ?: $requested_version; - - $best_concrete = null; - foreach (array_keys($available_versions) as $version) { - if ( - $version === NormalizerArtifactObjective::DEFAULT_KEY - || $this->isWildcardVersion($version) - ) { - continue; - } - - $version_major = strstr($version, '.', true) ?: $version; - if ($version_major !== $requested_major) { - continue; - } - - if ( - version_compare($version, $requested_version, '<=') - && ($best_concrete === null || version_compare($version, $best_concrete, '>')) - ) { - $best_concrete = $version; - } - } - - if ($best_concrete !== null) { - return $best_concrete; - } - - foreach (["{$requested_major}.*", $requested_major] as $wildcard) { - if (isset($available_versions[$wildcard])) { - return $wildcard; - } - } - - return NormalizerArtifactObjective::DEFAULT_KEY; - } - - private function isWildcardVersion(string $version): bool - { - return !str_contains($version, '.') || str_ends_with($version, '.*'); - } - - /* - Factory & Autowiring - */ - - /** - * Create an instance of a class by resolving the constructor arguments. - * - * @template T of object - * - * @param class-string $class_name - * @param Transformations $transformations - * @return T - */ - private function createInstance(string $class_name, Transformations $transformations): object - { - $reflection_class = new ReflectionClass($class_name); - $constructor = $reflection_class->getConstructor(); - - if ($constructor === null || $constructor->getNumberOfParameters() === 0) { - return $reflection_class->newInstance(); - } - - $arguments = []; - foreach ($constructor->getParameters() as $parameter) { - $arguments[] = $this->resolveConstructorArgument( - $parameter, - $transformations, - $class_name - ); - } - - return $reflection_class->newInstanceArgs($arguments); - } - - /** - * Resolve a constructor argument by trying to resolve it from the global ilias container, the local container or - * the default value. - * - * @throws RuntimeException if the argument cannot be resolved - */ - private function resolveConstructorArgument( - ReflectionParameter $parameter, - Transformations $transformations, - string $class_name - ) { - if ($parameter->isDefaultValueAvailable()) { - return $parameter->getDefaultValue(); - } - - $type = $parameter->getType(); - if ($type instanceof ReflectionNamedType && $type->allowsNull()) { - return null; - } - - - if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) { - $type_name = $type->getName(); - - if ( - $type_name === TransformationsContract::class - || in_array(TransformationsContract::class, class_implements($type_name)) - ) { - return $transformations; - } - - foreach ([$this->dic, ...$this->containers] as $container) { - if ($type_name === get_class($container)) { - return $container; - } - } - } - - throw new RuntimeException("Unable to resolve constructor parameter \${$parameter->getName()} for class {$class_name}."); - } -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Envelope.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Envelope.php index 6e92eec7f59a..7cfcc9472e92 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Envelope.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Envelope.php @@ -20,7 +20,7 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; /** * An envelope carries intermediate data between pipes before the final normalization step. diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ExportDependencies.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ExportDependencies.php deleted file mode 100644 index 6736fca44114..000000000000 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ExportDependencies.php +++ /dev/null @@ -1,81 +0,0 @@ -}> - */ - public function getDependencies(): array; - - /** - * @param array $ids - */ - public function addDependency(string $component, string $entity, array $ids): void; - - public function getContent(): string; -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Exporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Exporter.php index 5cfd1f10966c..c21a66c6b440 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Exporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Exporter.php @@ -20,6 +20,8 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportState; + /** * An Exporter is responsible for orchestrating the export process. It prepares the dependencies, collects the data to * export, transforms the data and writes the data to the serializer and the file system. @@ -34,7 +36,7 @@ interface Exporter * - Config * - Logger */ - public function prepare(ExportDependencies $state): void; + public function prepare(ExportState $state): void; /** * Processes the export by collecting the data to export, transforming the data and writing the data to the @@ -46,7 +48,7 @@ public function prepare(ExportDependencies $state): void; * - Logger * - Serializer */ - public function process(ExportDependencies $state): void; + public function process(ExportState $state): void; /** * Finalizes the export by persisting additional data (which cannot be provided by the serializer) to the file @@ -60,5 +62,5 @@ public function process(ExportDependencies $state): void; * - Writer * - Path */ - public function write(ExportDependencies $state): void; + public function write(ExportState $state): void; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Pipe.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/FromNormalized.php similarity index 56% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Pipe.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/FromNormalized.php index 92b68b1a4e49..d3a7dfc7abaa 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Pipe.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/FromNormalized.php @@ -20,20 +20,21 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; + /** - * A pipe is responsible for processing the passable object. It can manipulate the passable object and pass it to the - * next pipe. - * - * @template TPassable + * @deprecated fromNormalized should be implement in a factory. This is only a temporary solution to support current + * active record implementations. */ -interface Pipe +interface FromNormalized { - /** - * Handle the passable object and return it - * - * @param TPassable $passable - * @param \Closure $next - * @return TPassable + /** + * Restore the object from its normalized representation. + * + * @param array $normalized */ - public function handle(mixed $passable, \Closure $next): mixed; + public function fromNormalized( + array $normalized, + Transformations $transformations + ): object; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Normalizable.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Normalizable.php deleted file mode 100644 index a5dedfcabc03..000000000000 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Normalizable.php +++ /dev/null @@ -1,62 +0,0 @@ - $pipes - */ - public function through(array $pipes): self; - - /** - * Push additional pipe onto the pipeline. - * - * @param PipeParam $pipes - */ - public function pipe(\Closure|Pipe $pipe): self; - - /** - * Push additional pipe onto the pipeline which will be executed when the condition is met. - * - * @param \Closure(TPassable): bool $condition - * @param PipeParam $pipe - */ - public function pipeWhen(\Closure $condition, \Closure|Pipe $pipe): self; - - /** - * Push additional pipe onto the pipeline which will be executed when the condition is not met. - * - * @param \Closure(TPassable): bool $condition - * @param PipeParam $pipe - */ - public function pipeUnless(\Closure $condition, \Closure|Pipe $pipe): self; - - /** - * Run the pipeline with a final destination callback. - * - * @param \Closure(TPassable): TPassable $destination - * @return TPassable|mixed - */ - public function then(\Closure $destination): mixed; - - /** - * Run the pipeline and return the result. - * - * @return TPassable - */ - public function thenReturn(): mixed; - - /** - * Set a final callback to be executed after the pipeline ends regardless of the outcome. - * - * @param \Closure(TPassable): TPassable $callback - */ - public function finally(\Closure $callback): self; - - /** - * Get the array of pipes. - * - * @return list - */ - public function pipes(): array; -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ToNormalized.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ToNormalized.php new file mode 100644 index 000000000000..c4e19ab32679 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/ToNormalized.php @@ -0,0 +1,39 @@ + $context + * @return array|float|bool|int|string|null + */ + public function toNormalized( + Transformations $transformations, + array $context = [] + ): array|float|bool|int|string|null; +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Transformations.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Transformations.php deleted file mode 100644 index 36fa32689e40..000000000000 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/Transformations.php +++ /dev/null @@ -1,105 +0,0 @@ -|T $expected - * @return T|null - * - * @throws NormalizingException if the type is not supported - */ - public function denormalize(array|float|bool|int|string|null $normalized, string|object $expected): mixed; - - /* - Transformations - */ - - /** - * Returns the pipe of the given class from the pipeline. - * - * @template T of Pipe - * @param class-string $pipe_class - * @return T - * - * @throws \InvalidArgumentException if the pipe is not found - */ - public function context(string $pipe_class): Pipe; - - /** - * Returns a group of transformations that can be used to create custom transformations. - */ - public function custom(): Group; - - /** - * @throws \InvalidArgumentException if the value cannot be transformed into an integer - */ - public function int(mixed $value): int; - - /** - * @throws \InvalidArgumentException if the value cannot be transformed into a float - */ - public function float(mixed $value): float; - - /** - * @throws \InvalidArgumentException if the value cannot be transformed into a string - */ - public function string(mixed $value): string; - - /** - * @throws \InvalidArgumentException if the value cannot be transformed into a boolean - */ - public function bool(mixed $value): bool; - - public function nullableInt(mixed $value): ?int; - - public function nullableFloat(mixed $value): ?float; - - public function nullableString(mixed $value): ?string; - - public function nullableBool(mixed $value): ?bool; -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/Normalizes.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/Normalizes.php deleted file mode 100644 index e53a0e3358ea..000000000000 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/Normalizes.php +++ /dev/null @@ -1,42 +0,0 @@ - */ - public array $types; - - /** - * @param class-string ...$types - */ - public function __construct(string ...$types) { - $this->types = $types; - } -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/NormalizesLegacy.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/NormalizesLegacy.php deleted file mode 100644 index f7343aa82a4e..000000000000 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Attributes/NormalizesLegacy.php +++ /dev/null @@ -1,48 +0,0 @@ - */ - public array $versions; - - /** @var list */ - public array $types; - - /** - * @param class-string ...$types - */ - public function __construct( - string|array $version, - string ...$types - ) { - $this->versions = is_array($version) ? $version : [$version]; - $this->types = $types; - } -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Envelopes/Id.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Envelopes/Id.php index 190a54a83da2..125d81906da7 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Envelopes/Id.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Envelopes/Id.php @@ -21,7 +21,7 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Envelope; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; /** diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/FoundationNormalizerRegistration.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/FoundationNormalizerRegistration.php new file mode 100644 index 000000000000..df09710e70c1 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/FoundationNormalizerRegistration.php @@ -0,0 +1,68 @@ +register(\DateTime::class, $date_time); + $registry->register(\DateTimeImmutable::class, $date_time); + + $uuid = new UUIDNormalizer(); + $registry->register(Uuid::class, $uuid); + $registry->register(Uuid::class, new Legacy11UUIDNormalizer(), '11'); + + $registry->register(Envelope::class, new EnvelopeNormalizer($transformations)); + $registry->register(Transformation::class, new TransformationNormalizer($this->dic->refinery())); + + if (isset($this->dic[InitResourceStorage::D_REPOSITORIES])) { + $resource = new ResourceNormalizer( + $transformations, + $this->dic[InitResourceStorage::D_REPOSITORIES]->getResourceRepository() + ); + $registry->register(ResourceIdentification::class, $resource); + $registry->register(StorableResource::class, $resource); + } + + $registry->register(\ilObject::class, new IlObjectNormalizer($transformations)); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/DateTimeNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/DateTimeNormalizer.php index ffec74f76c48..9c30c0acb473 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/DateTimeNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/DateTimeNormalizer.php @@ -23,13 +23,11 @@ use DateTime; use DateTimeImmutable; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; /** - * @implements Normalizer + * @implements Normalizer */ -#[Normalizes(DateTime::class, DateTimeImmutable::class)] class DateTimeNormalizer implements Normalizer { /** diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/EnvelopeNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/EnvelopeNormalizer.php index 697a6765077f..4a5319c890d5 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/EnvelopeNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/EnvelopeNormalizer.php @@ -22,14 +22,12 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Envelope; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; /** * @implements Normalizer */ -#[Normalizes(Envelope::class)] class EnvelopeNormalizer implements Normalizer { public function __construct( diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php index 497b3d022ceb..d01650b9c9f0 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php @@ -31,8 +31,7 @@ use ILIAS\ILIASObject\Properties\Property; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; use ilObject; use ilObjectFactory; @@ -40,7 +39,6 @@ /** * @implements Normalizer */ -#[Normalizes(ilObject::class)] class IlObjectNormalizer implements Normalizer { public function __construct( diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Legacy11UUIDNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Legacy11UUIDNormalizer.php index 2d6374b7328a..d1c4cca5f127 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Legacy11UUIDNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Legacy11UUIDNormalizer.php @@ -21,7 +21,6 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer; use ILIAS\Data\UUID\Uuid; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\NormalizesLegacy; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; /** @@ -34,7 +33,6 @@ * In this case the normalizer is a subclass of the UUIDNormalizer. This may be useful if you want to reuse basic * functionality. You can also implement the Normalizer interface directly if you don't need to reuse functionality. */ -#[NormalizesLegacy('11', Uuid::class)] class Legacy11UUIDNormalizer extends UUIDNormalizer { /** diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Registry.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Registry.php index 6987ffd78df4..471bac140a72 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Registry.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Registry.php @@ -26,96 +26,65 @@ /** * Registry for normalizers. It is used to register and lookup normalizers for specific class types. */ -class Registry +final class Registry { - /** - * @var array - */ + /** @var array> */ private array $type_map = []; - /** - * Register a normalizer resolving callable for a type. The callable allows to defer the instantiation of the - * normalizer until it is actually needed. - * - * @param class-string $type - * @param callable():Normalizer $normalizer - * - * @throws NormalizingException if the type is already registered - */ - public function registerNormalizer(string $type, callable $normalizer): void + public function register( + string $type, + Normalizer $normalizer, + ?string $legacy_version = null + ): void { - if ($this->hasNormalizer($type)) { - throw new NormalizingException("Type {$type} is already registered"); + $version = $legacy_version ?? ''; + if (isset($this->type_map[$type][$version])) { + $label = $legacy_version === null ? 'current' : $legacy_version; + throw new NormalizingException("Type {$type} and version {$label} are already registered"); } - $this->type_map[$type] = $normalizer; + + $this->type_map[$type][$version] = $normalizer; } - /** - * Check if a normalizer is registered for a type. - * - * @param class-string $type - */ - public function hasNormalizer(string $type): bool + public function forNormalization(string $type): ?Normalizer { - return isset($this->type_map[$type]); + return $this->normalizerFor($type, null); } - /** - * Return the normalizer that should handle the given type. Resolves to the most specific - * registered type (child classes / implementing classes before parents/interfaces). - * - * @param class-string $type - * @return Normalizer|null null if no normalizer supports this type - */ - public function getNormalizerFor(string $type): ?Normalizer + public function forDenormalization(string $type, ?string $legacy_version): ?Normalizer { - $candidates = $this->findCandidateTypes($type); - if ($candidates === []) { - return null; - } - $key = $this->selectMostSpecificType($type, $candidates); - - // Instantiate the normalizer on demand - if (is_callable($this->type_map[$key])) { - $this->type_map[$key] = $this->type_map[$key](); - } - - return $this->type_map[$key]; + return $this->normalizerFor($type, $legacy_version); } - /** - * Types S from registry such that $type is assignable to S (same class or subclass/implementation). - * - * @param class-string $type - * @return list - */ - private function findCandidateTypes(string $type): array + private function normalizerFor(string $type, ?string $legacy_version): ?Normalizer { - $candidates = []; - foreach (array_keys($this->type_map) as $registeredType) { - if ($type === $registeredType || is_subclass_of($type, $registeredType)) { - $candidates[] = $registeredType; + foreach ($this->candidateTypes($type) as $candidate) { + $versions = $this->type_map[$candidate]; + if ($legacy_version !== null) { + $legacy_key = $this->resolveLegacyVersion($legacy_version, $versions); + if ($legacy_key !== null) { + return $versions[$legacy_key]; + } + } + + if (isset($versions[''])) { + return $versions['']; } } - return $candidates; + return null; } - /** - * Among candidate types (all are assignable from $type), return the most specific one - * (the one closest to $type: child classes before parent classes/interfaces). - * - * @param class-string $type - * @param list $candidate_types - * @return class-string - */ - private function selectMostSpecificType(string $type, array $candidate_types): string + /** @return list */ + private function candidateTypes(string $type): array { - if (count($candidate_types) === 1) { - return $candidate_types[0]; - } + $candidates = array_values(array_filter( + array_keys($this->type_map), + static fn(string $registered_type): bool => + $type === $registered_type || is_subclass_of($type, $registered_type) + )); - usort($candidate_types, function (string $a, string $b): int { + usort($candidates, static function (string $a, string $b): int { if ($a === $b) { return 0; } @@ -128,6 +97,51 @@ private function selectMostSpecificType(string $type, array $candidate_types): s return 0; }); - return $candidate_types[0]; + return $candidates; + } + + /** + * @param array $versions + */ + private function resolveLegacyVersion(string $requested, array $versions): ?string + { + if (isset($versions[$requested])) { + return $requested; + } + + $requested_major = explode('.', $requested, 2)[0]; + $best = null; + foreach (array_keys($versions) as $version_key) { + $version = (string) $version_key; + if ($version === '' || $this->isWildcard($version)) { + continue; + } + + $major = explode('.', $version, 2)[0]; + if ( + $major === $requested_major + && version_compare($version, $requested, '<=') + && ($best === null || version_compare($version, $best, '>')) + ) { + $best = $version; + } + } + + if ($best !== null) { + return $best; + } + + foreach (["{$requested_major}.*", $requested_major] as $wildcard) { + if (isset($versions[$wildcard])) { + return $wildcard; + } + } + + return null; + } + + private function isWildcard(string $version): bool + { + return !str_contains($version, '.') || str_ends_with($version, '.*'); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php index d0a131617c05..012e4d1c75bd 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php @@ -20,34 +20,27 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer; -use ILIAS\DI\Container; use ILIAS\ResourceStorage\Identification\ResourceIdentification; use ILIAS\ResourceStorage\Resource\Repository\ResourceRepository; use ILIAS\ResourceStorage\Resource\ResourceType; use ILIAS\ResourceStorage\Resource\StorableResource; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; -use InitResourceStorage; /** - * @implements Normalizer + * @implements Normalizer */ -#[Normalizes(ResourceIdentification::class, StorableResource::class)] class ResourceNormalizer implements Normalizer { private const string KEY_TYPE = 'type'; private const string TYPE_RID = 'rid'; private const string TYPE_RESOURCE = 'resource'; - private readonly ResourceRepository $resource_repository; - public function __construct( - private readonly Transformations $tt, - Container $dic + private readonly Transformations $transformations, + private readonly ResourceRepository $resource_repository ) { - $this->resource_repository = $dic[InitResourceStorage::D_REPOSITORIES]->getResourceRepository(); } /** @@ -84,7 +77,7 @@ private function normalizeResource(StorableResource $resource): array 'title' => $resource->getCurrentRevision()->getTitle(), 'mime_type' => $resource->getCurrentRevision()->getInformation()->getMimeType(), 'suffix' => $resource->getCurrentRevision()->getInformation()->getSuffix(), - 'creation_date' => $this->tt->normalize($resource->getCurrentRevision()->getInformation()->getCreationDate()), + 'creation_date' => $this->transformations->normalize($resource->getCurrentRevision()->getInformation()->getCreationDate()), ]; } @@ -98,7 +91,7 @@ public function denormalize(array|float|bool|int|string|null $value, string $typ } if ($type === StorableResource::class) { - $this->denormalizeResource($value); + return $this->denormalizeResource($value); } throw new NormalizingException('Invalid type', $type); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/TransformationNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/TransformationNormalizer.php index 3d79aaff8653..d2684442d37d 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/TransformationNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/TransformationNormalizer.php @@ -20,25 +20,17 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer; -use ILIAS\DI\Container; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; /** - * @implements Normalizer + * @implements Normalizer */ -#[Normalizes(Transformation::class)] class TransformationNormalizer implements Normalizer { - private readonly Refinery $refinery; - - public function __construct( - Container $dic - ) { - $this->refinery = $dic->refinery(); + public function __construct(private readonly Refinery $refinery) { } /** diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/UUIDNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/UUIDNormalizer.php index a0ef76fd1bfe..10d28c2779d4 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/UUIDNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/UUIDNormalizer.php @@ -23,13 +23,11 @@ use ILIAS\Data\UUID\Factory; use ILIAS\Data\UUID\Uuid; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; /** * @implements Normalizer */ -#[Normalizes(Uuid::class)] class UUIDNormalizer implements Normalizer { private readonly Factory $factory; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php index 9dba92bb2a28..7f0bb7fd866c 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php @@ -23,16 +23,15 @@ use ILIAS\ResourceStorage\Identification\ResourceIdentification; use ILIAS\ResourceStorage\Resource\StorableResource; use ILIAS\ResourceStorage\Services as IRSS; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Pipe; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\NormalizeCarry; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\DenormalizeCarry; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Queue\Processor; use Psr\Log\LoggerInterface; /** - * Pipe that collects all resources by their identification during normalization. During denormalization, it will replace - * the resource ids with the mapped new resource ids. + * Collects resources during normalization and replaces mapped IDs during denormalization. */ -class CollectResources implements Pipe +class CollectResources implements Processor { /** * @var array $resources @@ -73,17 +72,19 @@ public function storeMapping(string $old_id, ResourceIdentification $new_id): vo /** * @inheritDoc */ - public function handle(mixed $passable, \Closure $next): mixed + public function process(object $carry): void { - if ($passable instanceof NormalizeCarry && $passable->value instanceof ResourceIdentification) { - $this->handleNormalization($passable->value); + if ($carry instanceof NormalizeCarry && $carry->value() instanceof ResourceIdentification) { + $this->handleNormalization($carry->value()); } - if ($passable instanceof DenormalizeCarry && $passable->expected === ResourceIdentification::class) { - $passable->setResult($this->replaceRid($passable->result())); + if ( + $carry instanceof DenormalizeCarry + && $carry->expected() === ResourceIdentification::class + && $carry->hasResult() + ) { + $carry->setResult($this->replaceRid($carry->result())); } - - return $next($passable); } private function handleNormalization(ResourceIdentification $rid): void diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizeCarry.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizeCarry.php index b3987aa12cca..29746a869411 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizeCarry.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizeCarry.php @@ -2,32 +2,52 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; /** - * Carry object (passable) for the denormalization pipeline. It wraps the caller object and the value to denormalize. - * The result will be set by the pipes in the pipeline. If it's not set on the end of the pipeline, an exception will be - * thrown. + * Carries normalized data, its expected type and eventual result through denormalization processors. */ -class DenormalizeCarry +final class DenormalizeCarry { - protected mixed $result; + private bool $has_result = false; + private mixed $result; + /** + * @param array|float|bool|int|string|null $normalized + */ public function __construct( - public readonly Transformations $transformations, - public readonly array|float|bool|int|string|null $normalized, - public readonly string|object $expected, + private readonly Transformations $transformations, + private readonly array|float|bool|int|string|null $normalized, + private readonly string|object $expected, ) { } - /** - * Set the result of the denormalization carry. - */ - public function setResult(mixed $result): self + public function transformations(): Transformations + { + return $this->transformations; + } + + /** @return array|float|bool|int|string|null */ + public function normalized(): array|float|bool|int|string|null + { + return $this->normalized; + } + + public function expected(): string|object + { + return $this->expected; + } + + public function hasResult(): bool + { + return $this->has_result; + } + + public function setResult(mixed $result): void { $this->result = $result; - return $this; + $this->has_result = true; } /** @@ -37,10 +57,12 @@ public function setResult(mixed $result): self */ public function result(): mixed { - if (!isset($this->result)) { - $expected_type = get_debug_type($this->expected); - $normalized_type = get_debug_type($this->normalized); - throw new NormalizingException("Unsupported value, expected: {$expected_type}, got: {$normalized_type}"); + if ($this->has_result === false) { + throw new NormalizingException(sprintf( + 'Unsupported value, expected: %s, got: %s', + get_debug_type($this->expected), + get_debug_type($this->normalized) + )); } return $this->result; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizingPipe.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizingPipe.php deleted file mode 100644 index 233b8b825bf2..000000000000 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizingPipe.php +++ /dev/null @@ -1,62 +0,0 @@ - - */ -class DenormalizingPipe implements Pipe -{ - public function __construct( - private readonly Registry $registry - ) { - } - - public function handle(mixed $passable, \Closure $next): mixed - { - if (!$passable instanceof DenormalizeCarry) { - return $next($passable); - } - - // Check if normalizer for the expected type is registered. - if (is_string($passable->expected) && $normalizer = $this->registry->getNormalizerFor($passable->expected)) { - return $next( - $passable->setResult($normalizer->denormalize($passable->normalized, $passable->expected)) - ); - } - - // Use the fromNormalized method of the expected object to set the state of the object from the normalized form. - if ($passable->expected instanceof Normalizable) { - return $next( - $passable->setResult($passable->expected->fromNormalized($passable->transformations)->transform($passable->normalized)) - ); - } - - return $next($passable); - } -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizingProcessor.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizingProcessor.php new file mode 100644 index 000000000000..0e12dbe6ea08 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizingProcessor.php @@ -0,0 +1,72 @@ +normalized() === null) { + $carry->setResult(null); + return; + } + + if (is_string($carry->expected())) { + $normalizer = $this->registry->forDenormalization( + $carry->expected(), + $this->legacy_version + ); + if ($normalizer !== null) { + $carry->setResult( + $normalizer->denormalize($carry->normalized(), $carry->expected()) + ); + return; + } + } + + if ($carry->expected() instanceof FromNormalized) { + if (!is_array($carry->normalized())) { + throw new NormalizingException( + 'Expected normalized array, got ' . get_debug_type($carry->normalized()) + ); + } + $carry->setResult($carry->expected()->fromNormalized( + $carry->normalized(), + $carry->transformations() + )); + } + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/FinalizeNormalizing.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/FinalizeNormalizing.php index 4964f65e3bd1..f14d075c0c16 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/FinalizeNormalizing.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/FinalizeNormalizing.php @@ -20,25 +20,25 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Pipe; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\NormalizeCarry; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Queue\Processor; /** * Finalize the normalization of the value by ensuring that the value is normalized. It will check on the top level * structure by recursively iterating over the normalized value. If the value is neither a scalar nor an array, an * exception will be thrown. */ -class FinalizeNormalizing implements Pipe +final class FinalizeNormalizing implements Processor { - public function handle(mixed $passable, \Closure $next): mixed + public function process(object $carry): void { - if (!$passable instanceof NormalizeCarry) { - return $next($passable); + if (!($carry instanceof NormalizeCarry)) { + return; } - $this->ensureNormalized($passable->result()); - return $next($passable); + if ($carry->hasResult()) { + $this->ensureNormalized($carry->result()); + } } private function ensureNormalized(mixed $value): mixed diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/IdMappingPipe.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/IdMappingPipe.php deleted file mode 100644 index 5645ed580eef..000000000000 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/IdMappingPipe.php +++ /dev/null @@ -1,87 +0,0 @@ - - */ -class IdMappingPipe implements Pipe -{ - private array $unresolved = []; - - public function __construct( - private readonly ilImportMapping $mapping, - private readonly string $component, - private readonly LoggerInterface $log - ) { - } - - public function handle(mixed $passable, \Closure $next): mixed - { - if (!$passable instanceof DenormalizeCarry || $passable->expected !== Id::class) { - return $next($passable); - } - - $envelope = $passable->result(); - if (!$envelope instanceof Id) { - throw new NormalizingException('Expected id envelope, got ' . get_debug_type($envelope)); - } - - if ($new_id = $this->mapping->getMapping($this->component, $envelope->getObject(), (string) $envelope->getId())) { - // Replace the envelope with the mapped new id - if (is_int($envelope->getId())) { - $new_id = (int) $new_id; - } - - $passable->setResult(new Id($new_id, $envelope->getObject())); - $this->log->debug("Replaced id {$envelope->getObject()}:{$envelope->getId()} with {$new_id}"); - } else { - $this->unresolved[] = $envelope; - $this->log->warning("Unresolved id {$envelope->getObject()}:{$envelope->getId()}"); - } - - return $next($passable); - } - - /** - * @return list - */ - public function unresolved(): array - { - return $this->unresolved; - } - - public function mapping(): ilImportMapping - { - return $this->mapping; - } -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/IdMappingProcessor.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/IdMappingProcessor.php new file mode 100644 index 000000000000..6ba720f1f163 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/IdMappingProcessor.php @@ -0,0 +1,88 @@ +> */ + private array $unresolved = []; + + public function __construct( + private readonly \ilImportMapping $mapping, + private readonly string $component, + private readonly LoggerInterface $log + ) { + } + + public function process(object $carry): void + { + if ( + !($carry instanceof DenormalizeCarry) + || $carry->expected() !== Id::class + || !$carry->hasResult() + ) { + return; + } + + $envelope = $carry->result(); + if (!($envelope instanceof Id)) { + throw new NormalizingException('Expected id envelope, got ' . get_debug_type($envelope)); + } + + $new_id = $this->mapping->getMapping( + $this->component, + $envelope->getObject(), + (string) $envelope->getId() + ); + if ($new_id === null) { + $this->unresolved[] = $envelope; + $this->log->warning( + "Unresolved id {$envelope->getObject()}:{$envelope->getId()}" + ); + return; + } + + if (is_int($envelope->getId())) { + $new_id = (int) $new_id; + } + + $carry->setResult(new Id($new_id, $envelope->getObject())); + $this->log->debug( + "Replaced id {$envelope->getObject()}:{$envelope->getId()} with {$new_id}" + ); + } + + /** @return list> */ + public function unresolved(): array + { + return $this->unresolved; + } + + public function mapping(): \ilImportMapping + { + return $this->mapping; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizeCarry.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizeCarry.php index 27cebb51fdad..c01cce959d48 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizeCarry.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizeCarry.php @@ -2,32 +2,54 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; /** - * Carry object (passable) for the normalization pipeline. It wraps the caller object and the value to normalize. - * The result will be set by the pipes in the pipeline. If it's not set on the end of the pipeline, an exception will be - * thrown. + * Carries a value, context and eventual result through normalization processors. */ -class NormalizeCarry +final class NormalizeCarry { - protected array|float|bool|int|string|null $result = null; + private bool $has_result = false; + /** @var array|float|bool|int|string|null */ + private array|float|bool|int|string|null $result = null; + /** + * @param array $context + */ public function __construct( - public readonly Transformations $transformations, - public readonly mixed $value, - public readonly array $context, + private readonly Transformations $transformations, + private readonly mixed $value, + private readonly array $context, ) { } - /** - * Set the result of the normalization carry. - */ - public function setResult(array|float|bool|int|string|null $result): self + public function transformations(): Transformations + { + return $this->transformations; + } + + public function value(): mixed + { + return $this->value; + } + + /** @return array */ + public function context(): array + { + return $this->context; + } + + public function hasResult(): bool + { + return $this->has_result; + } + + /** @param array|float|bool|int|string|null $result */ + public function setResult(array|float|bool|int|string|null $result): void { $this->result = $result; - return $this; + $this->has_result = true; } /** @@ -35,9 +57,10 @@ public function setResult(array|float|bool|int|string|null $result): self * * @throws NormalizingException if the result is not set */ + /** @return array|float|bool|int|string|null */ public function result(): array|float|bool|int|string|null { - if ($this->result === null) { + if ($this->has_result === false) { throw new NormalizingException('Unsupported value', $this->value); } return $this->result; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizingPipe.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizingPipe.php deleted file mode 100644 index fa255289d8c9..000000000000 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizingPipe.php +++ /dev/null @@ -1,66 +0,0 @@ - - */ -class NormalizingPipe implements Pipe -{ - public function __construct( - private readonly Registry $registry - ) { - } - - public function handle(mixed $passable, \Closure $next): mixed - { - if (!$passable instanceof NormalizeCarry) { - return $next($passable); - } - - if (is_scalar($passable->value)) { - return $next($passable->setResult($passable->value)); - } - - // Check if object is self-normalizable and use the toNormalized method - if ($passable->value instanceof Normalizable) { - $normalized = $passable->value->toNormalized($passable->transformations)->transform($passable->context); - - return $next($passable->setResult($normalized)); - } - - // Lookup normalizer for the object type and use the normalize method - if ($normalizer = $this->registry->getNormalizerFor(get_class($passable->value))) { - return $next( - $passable->setResult($normalizer->normalize($passable->value)) - ); - } - - return $next($passable); - } -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizingProcessor.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizingProcessor.php new file mode 100644 index 000000000000..b0876a1224ce --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/NormalizingProcessor.php @@ -0,0 +1,58 @@ +value() === null || is_scalar($carry->value())) { + $carry->setResult($carry->value()); + return; + } + + if ($carry->value() instanceof ToNormalized) { + $carry->setResult($carry->value()->toNormalized( + $carry->transformations(), + $carry->context() + )); + return; + } + + $normalizer = $this->registry->forNormalization(get_class($carry->value())); + if ($normalizer !== null) { + $carry->setResult($normalizer->normalize($carry->value())); + } + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Transformations.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Transformations.php index 3c49d26c6d61..f1781f41cda0 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Transformations.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Transformations.php @@ -3,25 +3,23 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing; use Generator; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Pipe; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Pipeline; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\DenormalizeCarry; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\NormalizeCarry; -use ILIAS\Refinery\Custom\Group; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Queue\Processor; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Queue\Queue; use ILIAS\Refinery\Factory as Refinery; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations as TransformationsContract; -use InvalidArgumentException; /** * Provides a set of transformations for normalizing and denormalizing values. It uses the Refinery library to perform * the transformations. It also provides a registry of normalizers, which are used to handle the normalization and * denormalization of complex objects. */ -class Transformations implements TransformationsContract +final class Transformations { public function __construct( - protected readonly Refinery $refinery, - protected readonly Pipeline $pipeline + private readonly Refinery $refinery, + private readonly Queue $normalization_queue, + private readonly Queue $denormalization_queue ) { } @@ -30,14 +28,11 @@ public function __construct( */ /** - * @inheritDoc + * @param array $context + * @return array|float|bool|int|string|null */ public function normalize(mixed $value, array $context = []): array|float|bool|int|string|null { - if ($value === null) { - return null; - } - if (is_object($value) && $value instanceof Generator) { $value = iterator_to_array($value); } @@ -46,47 +41,28 @@ public function normalize(mixed $value, array $context = []): array|float|bool|i return array_map(fn(mixed $value) => $this->normalize($value, $context), $value); } - return $this->pipeline->send(new NormalizeCarry($this, $value, $context)) - ->then(static fn(NormalizeCarry $carry): mixed => $carry->result()); + $carry = new NormalizeCarry($this, $value, $context); + $this->normalization_queue->process($carry); + return $carry->result(); } /** - * @inheritDoc + * @param array|float|bool|int|string|null $normalized */ public function denormalize(array|float|bool|int|string|null $normalized, string|object $expected): mixed { - if ($normalized === null) { - return null; - } - - return $this->pipeline->send(new DenormalizeCarry($this, $normalized, $expected)) - ->then(static fn(DenormalizeCarry $carry): mixed => $carry->result()); - } - - /* - Transformations - */ - - /** - * @inheritDoc - */ - public function context(string $pipe_class): Pipe - { - foreach ($this->pipeline->pipes() as $pipe) { - if ($pipe instanceof $pipe_class) { - return $pipe; - } - } - throw new InvalidArgumentException("Pipe {$pipe_class} not found"); + $carry = new DenormalizeCarry($this, $normalized, $expected); + $this->denormalization_queue->process($carry); + return $carry->result(); } - public function custom(): Group + public function normalizationProcessor(string $processor_class): Processor { - return $this->refinery->custom(); + return $this->normalization_queue->get($processor_class); } /** - * @throws InvalidArgumentException if the value cannot be transformed into an integer + * @throws \InvalidArgumentException if the value cannot be transformed into an integer */ public function int(mixed $value): int { @@ -94,7 +70,7 @@ public function int(mixed $value): int } /** - * @throws InvalidArgumentException if the value cannot be transformed into a float + * @throws \InvalidArgumentException if the value cannot be transformed into a float */ public function float(mixed $value): float { @@ -102,7 +78,7 @@ public function float(mixed $value): float } /** - * @throws InvalidArgumentException if the value cannot be transformed into a string + * @throws \InvalidArgumentException if the value cannot be transformed into a string */ public function string(mixed $value): string { @@ -110,7 +86,7 @@ public function string(mixed $value): string } /** - * @throws InvalidArgumentException if the value cannot be transformed into a boolean + * @throws \InvalidArgumentException if the value cannot be transformed into a boolean */ public function bool(mixed $value): bool { diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Pipeline.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Pipeline.php deleted file mode 100644 index ed79290c6917..000000000000 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Pipeline.php +++ /dev/null @@ -1,202 +0,0 @@ - - * - * @phpstan-type PipeParam Closure(TPassable, PipeParam)|Pipe: TPassable - */ -class Pipeline implements PipelineContract -{ - /** - * The object being passed through the pipeline. - * - * @var TPassable $passable - */ - protected mixed $passable; - - /** - * The array of class pipes. - * - * @var list $pipes - */ - protected array $pipes = []; - - /** - * The final callback to be executed after the pipeline ends regardless of the outcome. - */ - protected ?Closure $finally = null; - - - /** - * @inheritDoc - */ - public function send(mixed $passable): self - { - $this->passable = $passable; - return $this; - } - - /** - * @inheritDoc - */ - public function through(array $pipes): self - { - $this->pipes = $pipes; - return $this; - } - - /** - * @inheritDoc - */ - public function pipe(Closure|Pipe $pipe): self - { - $this->pipes[] = $pipe; - return $this; - } - - /** - * @inheritDoc - */ - public function pipeWhen(Closure $condition, Closure|Pipe $pipe): self - { - return $this->pipe(fn($passable, $next) => $condition($passable) - ? $this->executePipe($pipe, $passable, $next) - : $next($passable)); - } - - /** - * @inheritDoc - */ - public function pipeUnless(Closure $condition, Closure|Pipe $pipe): self - { - return $this->pipeWhen(fn($passable) => !$condition($passable), $pipe); - } - - /** - * @inheritDoc - */ - public function then(Closure $destination): mixed - { - $pipeline = array_reduce( - array_reverse($this->pipes), - $this->carry(), - $this->prepareDestination($destination) - ); - - try { - return $pipeline($this->passable); - } finally { - if ($this->finally) { - ($this->finally)($this->passable); - } - } - } - - /** - * @inheritDoc - */ - public function thenReturn(): mixed - { - return $this->then(fn(mixed $passable): mixed => $passable); - } - - /** - * @inheritDoc - */ - public function finally(Closure $callback): self - { - $this->finally = $callback; - return $this; - } - - /** - * @inheritDoc - */ - public function pipes(): array - { - return $this->pipes; - } - - /** - * Get the final piece of the Closure onion. - */ - protected function prepareDestination(Closure $destination): Closure - { - return function (mixed $passable) use ($destination): mixed { - try { - return $destination($passable); - } catch (Throwable $e) { - $this->handleException($passable, $e); - } - }; - } - - /** - * Get a Closure that represents a slice of the application onion. - */ - protected function carry(): Closure - { - return fn($stack, $pipe) => function ($passable) use ($stack, $pipe): mixed { - try { - return $this->executePipe($pipe, $passable, $stack); - } catch (Throwable $e) { - $this->handleException($passable, $e); - } - }; - } - - /** - * Execute a single pipe, handling both Closure and Pipe instances. - * Pipe instances are skipped when their skip() method returns true. - */ - protected function executePipe(Closure|Pipe $pipe, mixed $passable, Closure $next): mixed - { - if ($pipe instanceof Pipe) { - return $pipe->handle($passable, $next); - } - - if (is_callable($pipe)) { - return $pipe($passable, $next); - } - - throw new \InvalidArgumentException('Invalid pipe'); - } - - /** - * Handle the given exception. - * - * @throws \Throwable - */ - protected function handleException(mixed $passable, Throwable $e): void - { - // Maybe we want to handle specific exceptions differently in the future. - // For now, we'll just re-throw the exception.' - throw $e; - } -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/DataCollector.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Queue/Processor.php similarity index 50% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/DataCollector.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Queue/Processor.php index b23c8eb65133..b3bc835db02a 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Contracts/DataCollector.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Queue/Processor.php @@ -18,18 +18,9 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts; +namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Queue; -/** - * Interface for data collectors that extract and assemble data relevant for - * question exports into exportable structures. - * - * Implementations use the provided repositories and domain objects as return - * values and follow a builder-style API: configuration methods are fluent and - * return the collector instance itself, while query methods typically expose - * generators to stream potentially large data sets efficiently for export. - */ -interface DataCollector +interface Processor { - // + public function process(object $carry): void; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Queue/Queue.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Queue/Queue.php new file mode 100644 index 000000000000..c24b1cd5154f --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Queue/Queue.php @@ -0,0 +1,65 @@ + */ +final class Queue implements \IteratorAggregate +{ + /** @var list */ + private readonly array $processors; + + public function __construct(Processor ...$processors) + { + $this->processors = $processors; + } + + public function process(object $carry): object + { + foreach ($this->processors as $processor) { + $processor->process($carry); + } + + return $carry; + } + + public function get(string $class): Processor + { + $matches = array_values(array_filter( + $this->processors, + static fn(Processor $processor): bool => $processor instanceof $class + )); + + if (count($matches) !== 1) { + throw new \InvalidArgumentException( + count($matches) === 0 + ? "Processor {$class} not found" + : "Processor {$class} is ambiguous" + ); + } + + return $matches[0]; + } + + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->processors); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Setup/NormalizerArtifactObjective.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Setup/NormalizerArtifactObjective.php deleted file mode 100644 index 8260d1cebbaa..000000000000 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Setup/NormalizerArtifactObjective.php +++ /dev/null @@ -1,90 +0,0 @@ -getMatchingClassNames(Normalizer::class) as $class_name) { - foreach ($this->getDeclarations(new ReflectionClass($class_name)) as [$types, $versions]) { - $entry = array_fill_keys($versions, $class_name); - foreach ($types as $type) { - // union instead of array_merge: numeric version keys would be reindexed - $type_map[$type] = $entry + ($type_map[$type] ?? []); - } - } - } - - return new ArrayArtifact($type_map); - } - - /** - * Both attributes declare the same thing: every combination of type and version is served by the given - * class. Normalize is the special case where the only version is the default one. - * - * @param ReflectionClass $ref - * @return Generator, list}> - */ - private function getDeclarations(ReflectionClass $ref): Generator - { - if (($attribute = $ref->getAttributes(Normalizes::class)[0] ?? null) !== null) { - yield [$attribute->newInstance()->types, [self::DEFAULT_KEY]]; - } - - if (($attribute = $ref->getAttributes(NormalizesLegacy::class)[0] ?? null) !== null) { - $legacy = $attribute->newInstance(); - yield [$legacy->types, $legacy->versions]; - } - } -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php index 0ae80565e394..c45965307d59 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php @@ -23,25 +23,23 @@ use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\ReferenceId; use ILIAS\Data\UUID\Factory; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Deserializer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\IdMappingPipe; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\IdMappingProcessor; +use ILIAS\TestQuestionPool\ExportImport\TransformationsBuilder; use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; use ilImportMapping; use ilObjQuestionPool; use Psr\Log\LoggerInterface; /** - * Orchestrates the import of a question pool. It uses the Builder to create a pipeline of transformations that are used - * to normalize the data provided by the deserializer. It imports the question pool object and its content (questions, - * skill assignments, etc.) into the database using repository classes and legacy active record models. + * Orchestrates denormalization and persistence of a question pool import. */ class QuestionPoolImporter { public function __construct( - private readonly Builder $builder, + private readonly TransformationsBuilder $builder, private readonly LoggerInterface $log, private readonly DataFactory $data_factory, private readonly QuestionsImporter $questions_importer, @@ -59,16 +57,19 @@ public function import( ReferenceId $parent_id, ImportContext $context ): ImportContext { - $id_mapping_pipe = new IdMappingPipe($mapping, 'components/ILIAS/TestQuestionPool', $this->log); - $images_pipe = new CollectQuestionImages(new Factory(), $this->data_factory->objId(0)); - $tt = $this->builder->withAdditionalPipes(append: [$id_mapping_pipe, $images_pipe])->create(); + $images_collector = new CollectQuestionImages(new Factory(), $this->data_factory->objId(0)); + $transformations = $this->builder->forImport( + null, + new IdMappingProcessor($mapping, 'components/ILIAS/TestQuestionPool', $this->log), + $images_collector + ); $deserializer->addHandler( 'general', - function (array $objects) use ($tt, $mapping, $parent_id, &$context): void { + function (array $objects) use ($transformations, $mapping, $parent_id, &$context): void { $new_pool_id = $this->importQuestionPool( array_pop($objects), - $tt, + $transformations, $mapping, $parent_id ); @@ -78,11 +79,11 @@ function (array $objects) use ($tt, $mapping, $parent_id, &$context): void { $deserializer->addHandler( 'questions', - function (array $questions) use ($tt, $mapping, $context): void { + function (array $questions) use ($transformations, $mapping, $context): void { foreach ($questions as $question) { $this->questions_importer->importQuestion( $question, - $tt, + $transformations, $mapping, $context->selectedQuestionIds() ); @@ -92,11 +93,11 @@ function (array $questions) use ($tt, $mapping, $context): void { $deserializer->addHandler( 'skill_assignments', - function (array $assignments) use ($tt, $mapping, &$context): void { + function (array $assignments) use ($transformations, $mapping, &$context): void { $result = $this->skill_importer->import( $assignments, $context->installId(), - $tt, + $transformations, $mapping, ); $context = $context->withSkillAssignments($result); @@ -112,7 +113,7 @@ function (array $assignments) use ($tt, $mapping, &$context): void { $context->poolObjId(), $mapping, $context, - $images_pipe + $images_collector ); $this->log->info('...Finished importing question images'); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php index 29d5a5040ee7..cf7913a219c5 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php @@ -35,7 +35,7 @@ use ILIAS\Filesystem\Util\Convert\Images; use ILIAS\Language\Language; use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Envelopes\Feedback; @@ -108,11 +108,11 @@ public function importQuestionImages( int $parent_obj_id, ilImportMapping $mapping, ImportContext $context, - CollectQuestionImages $pipe, + CollectQuestionImages $collector, ): void { $import_dir = dirname($context->componentImportFile()) . '/expDir_1'; - foreach ($pipe->getEnvelopes() as $filename => $envelope) { + foreach ($collector->getEnvelopes() as $filename => $envelope) { $source_path = $import_dir . DIRECTORY_SEPARATOR . $filename; if (!file_exists($source_path)) { $this->log->error("Imported image path does not exist: {$source_path}"); @@ -293,7 +293,7 @@ private function importFormulaQuestion( $mapping->addMapping($this->component, 'unit_category', (string) $old_category_id, (string) $category->getId()); } - // Ensure base units are imported first so they can be referenced by the units. The mapping pipe will ensure + // Ensure base units are imported first so they can be referenced by the units. The mapping processor will ensure // that the category id, question id and base unit id are mapped to the new ids. $normalized_units = array_merge($formula['base_units'], $formula['units']); foreach ($normalized_units as $normalized_unit) { diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php index 6aed319e2474..0cb16a3e6f03 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php @@ -21,7 +21,7 @@ namespace ILIAS\TestQuestionPool\ExportImport\Import; use ilAssQuestionSkillAssignment; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\Skill\Service\SkillUsageService; use ilImportMapping; use ilSkillTreeRepository; @@ -60,7 +60,7 @@ public function import( $result = ['failed' => [], 'success' => []]; foreach ($normalized_assignments as $item) { - // The mapping pipe replaces ParentObjID and QuestionID + // The mapping processor replaces ParentObjID and QuestionID. $assignment = $transformations->denormalize($item, ilAssQuestionSkillAssignment::class); $skill_data = $this->getSkillIdMapping( diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/SuggestedSolutionNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/SuggestedSolutionNormalizer.php index 3f962e121309..52d574f89b55 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/SuggestedSolutionNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/SuggestedSolutionNormalizer.php @@ -22,8 +22,7 @@ use DateTimeImmutable; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; @@ -32,9 +31,7 @@ use ILIAS\TestQuestionPool\Questions\SuggestedSolution\SuggestedSolutionLink; /** - * @implements Normalizer */ -#[Normalizes(SuggestedSolution::class)] class SuggestedSolutionNormalizer implements Normalizer { public function __construct( diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php index f8ea08567123..4ffe639984b6 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php @@ -23,26 +23,19 @@ use ilAssQuestionSkillAssignment; use ilAssQuestionSolutionComparisonExpression; use ilDBInterface; -use ILIAS\DI\Container; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; /** - * @implements Normalizer */ -#[Normalizes(ilAssQuestionSkillAssignment::class)] class ilAssQuestionSkillAssignmentNormalizer implements Normalizer { - private readonly ilDBInterface $db; - public function __construct( private readonly Transformations $tt, - Container $dic + private readonly ilDBInterface $db ) { - $this->db = $dic->database(); } /** diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilObjQuestionPoolNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilObjQuestionPoolNormalizer.php index d4cfb429eadd..d00e9f483250 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilObjQuestionPoolNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilObjQuestionPoolNormalizer.php @@ -22,14 +22,11 @@ use ilObjQuestionPool; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Attributes\Normalizes; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer\IlObjectNormalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; /** - * @implements Normalizer */ -#[Normalizes(ilObjQuestionPool::class)] class ilObjQuestionPoolNormalizer extends IlObjectNormalizer implements Normalizer { /** diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php index 41777693b1c0..4284f01e5355 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php @@ -22,18 +22,17 @@ use ILIAS\Data\ObjectId; use ILIAS\Data\UUID\Factory; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Pipe; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\DenormalizeCarry; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\NormalizeCarry; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Queue\Processor; use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; use ILIAS\TestQuestionPool\Questions\Files\QuestionFiles; /** - * Pipe that enriches QuestionImage envelopes with UUID-based IDs and records source-to-target file mappings during - * normalization. + * Enriches question images with UUIDs and records their source-to-target file mappings. */ -class CollectQuestionImages implements Pipe +class CollectQuestionImages implements Processor { private readonly QuestionFiles $question_files; @@ -57,17 +56,19 @@ public function __construct( /** * @inheritDoc */ - public function handle(mixed $passable, \Closure $next): mixed + public function process(object $carry): void { - if ($passable instanceof NormalizeCarry && $passable->value instanceof QuestionImage) { - $this->handleNormalization($passable->value); + if ($carry instanceof NormalizeCarry && $carry->value() instanceof QuestionImage) { + $this->handleNormalization($carry->value()); } - if ($passable instanceof DenormalizeCarry && $passable->expected === QuestionImage::class) { - $this->handleDenormalization($passable); + if ( + $carry instanceof DenormalizeCarry + && $carry->expected() === QuestionImage::class + && $carry->hasResult() + ) { + $this->handleDenormalization($carry); } - - return $next($passable); } private function handleNormalization(QuestionImage $envelope): void @@ -93,6 +94,9 @@ private function handleNormalization(QuestionImage $envelope): void private function handleDenormalization(DenormalizeCarry $passable): void { $envelope = $passable->result(); + if ($envelope === null) { + return; + } if (!$envelope instanceof QuestionImage) { throw new NormalizingException('Expected question image envelope, got ' . get_debug_type($envelope)); } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolNormalizerRegistration.php b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolNormalizerRegistration.php new file mode 100644 index 000000000000..73ac1889b447 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolNormalizerRegistration.php @@ -0,0 +1,51 @@ +register( + \ilObjQuestionPool::class, + new ilObjQuestionPoolNormalizer($transformations) + ); + $registry->register( + SuggestedSolution::class, + new SuggestedSolutionNormalizer($transformations) + ); + $registry->register( + \ilAssQuestionSkillAssignment::class, + new ilAssQuestionSkillAssignmentNormalizer($transformations, $this->dic->database()) + ); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/TransformationsBuilder.php b/components/ILIAS/TestQuestionPool/src/ExportImport/TransformationsBuilder.php new file mode 100644 index 000000000000..7a449e30c781 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/TransformationsBuilder.php @@ -0,0 +1,80 @@ +create(null, $runtime_processors, []); + } + + public function forImport( + ?string $legacy_version, + Processor ...$runtime_processors + ): Transformations { + return $this->create($legacy_version, [], $runtime_processors); + } + + /** + * @param list $normalization_processors + * @param list $denormalization_processors + */ + private function create( + ?string $legacy_version, + array $normalization_processors, + array $denormalization_processors + ): Transformations { + $registry = new Registry(); + $transformations = new Transformations( + $this->dic->refinery(), + new Queue(...[ + ...$normalization_processors, + new NormalizingProcessor($registry), + new FinalizeNormalizing() + ]), + new Queue(...[ + new DenormalizingProcessor($registry, $legacy_version), + ...$denormalization_processors + ]) + ); + + (new FoundationNormalizerRegistration($this->dic)) + ->register($registry, $transformations); + (new QuestionPoolNormalizerRegistration($this->dic)) + ->register($registry, $transformations); + + return $transformations; + } +} diff --git a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php index 18dcd71d7c64..3621a66c3a3c 100755 --- a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php +++ b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php @@ -22,11 +22,11 @@ use ILIAS\Data\Factory as DataFactory; use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\StateHolder; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Export\QuestionPoolExporter; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionPoolImporter; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionsImporter; +use ILIAS\TestQuestionPool\ExportImport\TransformationsBuilder; use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter; use ILIAS\TestQuestionPool\ExportImport\LoggingProvider; use Pimple\Container as PimpleContainer; @@ -78,16 +78,13 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic['exportimport.logging'] = static fn($c): LoggingProvider => new LoggingProvider(); - $dic['exportimport.builder'] = static fn($c): Builder => - new Builder( - $DIC, - $c - ); + $dic['exportimport.transformations_builder'] = static fn($c): TransformationsBuilder => + new TransformationsBuilder($DIC); $dic['exportimport.state_holder'] = static fn($c): StateHolder => new StateHolder(); $dic['exportimport.exporter'] = static fn($c): QuestionPoolExporter => new QuestionPoolExporter( - $c['exportimport.builder'], + $c['exportimport.transformations_builder'], new DataFactory(), $c['question.general_properties.repository'], $DIC->database(), @@ -117,7 +114,7 @@ protected static function buildDIC(ILIASContainer $DIC): self ); $dic['exportimport.importer'] = static fn($c): QuestionPoolImporter => new QuestionPoolImporter( - $c['exportimport.builder'], + $c['exportimport.transformations_builder'], $c['exportimport.logging'](), new DataFactory(), $c['exportimport.questions_importer'], diff --git a/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php b/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php index ec548a462f89..8d0a92e3b857 100644 --- a/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php +++ b/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php @@ -20,13 +20,19 @@ use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\UUID\Factory as UUIDFactory; +use ILIAS\Data\UUID\Uuid; use ILIAS\Refinery\Factory as RefineryFactory; +use ILIAS\Test\ExportImport\TransformationsBuilder as TestTransformationsBuilder; +use ILIAS\Test\TestDIC; use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Deserializer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Builder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Queue\Processor; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\DenormalizeCarry; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\NormalizeCarry; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlDeserializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlSerializer; +use ILIAS\TestQuestionPool\ExportImport\TransformationsBuilder; use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; use ILIAS\TestQuestionPool\QuestionPoolDIC; @@ -50,6 +56,9 @@ protected function setUp(): void 'refinery', new RefineryFactory(new DataFactory(), $GLOBALS['DIC']['lng']) ); + $this->addGlobal_mail(); + (new ReflectionClass(QuestionPoolDIC::class))->getProperty('dic')->setValue(null, null); + (new ReflectionClass(TestDIC::class))->getProperty('dic')->setValue(null, null); } protected function tearDown(): void @@ -81,9 +90,9 @@ public function testSingleChoiceRoundtripPreservesQuestionAndCollectsImages(): v new UUIDFactory(), (new DataFactory())->objId(42) ); - $export_transformations = (new Builder($DIC, QuestionPoolDIC::dic())) - ->withAdditionalPipes([$export_images]) - ->create(); + $factory = QuestionPoolDIC::dic()['exportimport.transformations_builder']; + $this->assertInstanceOf(TransformationsBuilder::class, $factory); + $export_transformations = $factory->forExport($export_images); $normalized = $export_transformations->normalize($question); @@ -102,9 +111,7 @@ public function testSingleChoiceRoundtripPreservesQuestionAndCollectsImages(): v new UUIDFactory(), (new DataFactory())->objId(0) ); - $import_transformations = (new Builder($DIC, QuestionPoolDIC::dic())) - ->withAdditionalPipes(append: [$import_images]) - ->create(); + $import_transformations = $factory->forImport(null, $import_images); $restored = $import_transformations->denormalize($normalized, new assSingleChoice()); @@ -124,6 +131,72 @@ public function testSingleChoiceRoundtripPreservesQuestionAndCollectsImages(): v ); } + public function testTransformationFactoriesKeepRuntimeProcessorsInTheirDirection(): void + { + global $DIC; + + $processor = new class () implements Processor { + public int $normalized = 0; + public int $denormalized = 0; + + public function process(object $carry): void + { + if ($carry instanceof NormalizeCarry) { + ++$this->normalized; + $carry->setResult('runtime result'); + } + if ($carry instanceof DenormalizeCarry) { + ++$this->denormalized; + } + } + }; + $factory = new TransformationsBuilder($DIC); + + $this->assertSame('value', $factory->forExport($processor)->normalize('value')); + $this->assertSame(1, $processor->normalized); + $this->assertSame(0, $processor->denormalized); + + $factory->forImport(null, $processor)->denormalize( + '123e4567-e89b-12d3-a456-426614174000', + Uuid::class + ); + $this->assertSame(1, $processor->normalized); + $this->assertSame(1, $processor->denormalized); + } + + public function testLegacyNormalizerSelectionIsLimitedToImport(): void + { + global $DIC; + + $factory = new TransformationsBuilder($DIC); + $uuid = '123e4567_e89b_12d3_a456_426614174000'; + + $this->assertNull($factory->forExport()->normalize(null)); + $this->assertNull($factory->forImport(null)->denormalize(null, Uuid::class)); + $this->assertSame( + '123e4567-e89b-12d3-a456-426614174000', + $factory->forImport('11.7')->denormalize($uuid, Uuid::class)->toString() + ); + $this->assertSame( + '123e4567-e89b-12d3-a456-426614174000', + $factory->forExport()->normalize( + $factory->forImport('11.7')->denormalize($uuid, Uuid::class) + ) + ); + } + + public function testTestDicProvidesConfiguredTransformationsFactory(): void + { + $factory = TestDIC::dic()['exportimport.transformations_builder']; + $uuid = (new UUIDFactory())->uuid4(); + + $this->assertInstanceOf(TestTransformationsBuilder::class, $factory); + $this->assertSame( + $uuid->toString(), + $factory->forExport()->normalize($uuid) + ); + } + public function testXmlStringAndFileDeserializersProduceTheSameData(): void { $serializer = XmlSerializer::inMemory(); From c19267bb9f4f80cfcd6b06d854e8effbe65f689b Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Tue, 22 Sep 2026 10:32:17 +0200 Subject: [PATCH 56/66] refactor: align exportimport namespaces --- .../ILIAS/Test/classes/class.ilObjTestGUI.php | 8 ++-- .../Test/classes/class.ilTestExporter.php | 4 +- .../Test/classes/class.ilTestImporter.php | 4 +- .../classes/class.ilTestLegacyImporter.php | 2 +- ...tRandomQuestionSetSourcePoolDefinition.php | 8 ++-- .../ExportOptions/XMLWithResultsOption.php | 2 +- .../src/ExportImport/Import/PersistStage.php | 10 ++-- .../Import/RandomTestConfigImporter.php | 2 +- .../Import/SkillLevelThresholdsImporter.php | 2 +- .../src/ExportImport/Import/TestImporter.php | 16 +++---- .../Import/TestResultsImporter.php | 42 ++++++++--------- .../Envelopes/AdditionalWorkingTime.php | 8 ++-- .../Envelopes/Attempt.php} | 12 ++--- .../Envelopes/ManualFeedback.php | 8 ++-- .../Envelopes/QuestionResult.php | 8 ++-- .../Envelopes/QuestionSetConfig.php | 2 +- .../Envelopes/RandomTestQuestion.php | 8 ++-- .../{ => Normalize}/Envelopes/WorkingTime.php | 8 ++-- .../Normalizer/AttemptResultNormalizer.php | 10 ++-- .../Normalizer/ExportableNormalizer.php | 6 +-- .../Normalizer/ParticipantNormalizer.php | 10 ++-- .../ParticipantResultNormalizer.php | 10 ++-- .../QuestionSetConfigNormalizer.php | 12 ++--- .../Normalizer/ilObjTestNormalizer.php | 10 ++-- .../Normalizer/ilTestSequenceNormalizer.php | 10 ++-- .../ilTestSkillLevelThresholdNormalizer.php | 10 ++-- .../Processors}/CollectUserIds.php | 6 +-- .../Test/src/ExportImport/TestCollector.php | 24 +++++----- .../Test/src/ExportImport/TestExporter.php | 18 ++++---- .../TestNormalizerRegistration.php | 22 ++++----- .../ExportImport/TransformationsBuilder.php | 12 ++--- components/ILIAS/Test/src/TestDIC.php | 4 +- .../classes/class.assAnswerBinaryState.php | 2 +- .../class.assAnswerBinaryStateImage.php | 4 +- .../classes/class.assAnswerCloze.php | 2 +- .../classes/class.assAnswerErrorText.php | 6 +-- .../classes/class.assAnswerImagemap.php | 2 +- .../classes/class.assAnswerMatching.php | 6 +-- .../classes/class.assAnswerMatchingPair.php | 6 +-- .../classes/class.assAnswerMatchingTerm.php | 8 ++-- .../class.assAnswerMultipleResponse.php | 2 +- .../class.assAnswerMultipleResponseImage.php | 4 +- .../classes/class.assAnswerSimple.php | 8 ++-- .../classes/class.assAnswerTrueFalse.php | 2 +- .../classes/class.assClozeGap.php | 6 +-- .../classes/class.assClozeTest.php | 6 +-- .../classes/class.assErrorText.php | 6 +-- .../classes/class.assFileUpload.php | 6 +-- .../classes/class.assFormulaQuestion.php | 6 +-- .../class.assFormulaQuestionResult.php | 6 +-- .../classes/class.assFormulaQuestionUnit.php | 8 ++-- .../class.assFormulaQuestionUnitCategory.php | 8 ++-- .../class.assFormulaQuestionVariable.php | 6 +-- .../classes/class.assImagemapQuestion.php | 8 ++-- .../classes/class.assKprimChoice.php | 6 +-- .../classes/class.assLongMenu.php | 6 +-- .../classes/class.assMatchingQuestion.php | 6 +-- .../classes/class.assMultipleChoice.php | 6 +-- .../classes/class.assNumeric.php | 6 +-- .../classes/class.assOrderingHorizontal.php | 6 +-- .../classes/class.assOrderingQuestion.php | 6 +-- .../classes/class.assQuestion.php | 4 +- .../classes/class.assSingleChoice.php | 6 +-- .../classes/class.assTextQuestion.php | 6 +-- .../classes/class.assTextSubset.php | 6 +-- .../classes/class.ilAssKprimChoiceAnswer.php | 8 ++-- .../classes/class.ilObjQuestionPoolGUI.php | 10 ++-- .../class.ilTestQuestionPoolExporter.php | 4 +- .../class.ilTestQuestionPoolImporter.php | 4 +- ...class.ilTestQuestionPoolLegacyImporter.php | 2 +- .../questions/class.ilAssOrderingElement.php | 10 ++-- .../class.ilAssQuestionLifecycle.php | 6 +-- .../ExportImport/Export/CollectsQuestions.php | 4 +- .../Export/QuestionPoolExporter.php | 12 ++--- .../{Bridge => Export}/ExportState.php | 6 +-- .../{Bridge => Export}/ExportStep.php | 2 +- .../{Contracts => Export}/Exporter.php | 4 +- .../{Bridge => Export}/StateHolder.php | 2 +- .../{Bridge => Export}/XmlExporterBridge.php | 6 +-- .../{Importing => Import}/ImportContext.php | 2 +- .../ImportSessionRepository.php | 2 +- .../{Contracts => Import}/ImportStage.php | 6 +-- .../ImportStageRunner.php | 4 +- .../{Importing => Import}/StageResult.php | 2 +- .../{Importing => Import}/StageResultType.php | 2 +- .../{Contracts => Normalize}/Envelope.php | 4 +- .../Envelopes/Id.php | 8 ++-- .../FoundationNormalizerRegistration.php | 20 ++++---- .../FromNormalized.php | 4 +- .../{Contracts => Normalize}/Normalizer.php | 4 +- .../Normalizer/DateTimeNormalizer.php | 6 +-- .../Normalizer/EnvelopeNormalizer.php | 10 ++-- .../Normalizer/IlObjectNormalizer.php | 10 ++-- .../Normalizer/Legacy11UUIDNormalizer.php | 4 +- .../Normalizer/Registry.php | 6 +-- .../Normalizer/ResourceNormalizer.php | 8 ++-- .../Normalizer/TransformationNormalizer.php | 6 +-- .../Normalizer/UUIDNormalizer.php | 6 +-- .../NormalizingException.php | 2 +- .../Processors}/CollectResources.php | 6 +-- .../Processors}/DenormalizeCarry.php | 6 +-- .../Processors}/DenormalizingProcessor.php | 8 ++-- .../Processors}/FinalizeNormalizing.php | 4 +- .../Processors}/IdMappingProcessor.php | 6 +-- .../Processors}/NormalizeCarry.php | 6 +-- .../Processors}/NormalizingProcessor.php | 6 +-- .../{Contracts => Normalize}/ToNormalized.php | 4 +- .../Transformations.php | 6 +-- .../{Contracts => Serialize}/Deserializer.php | 2 +- .../{Contracts => Serialize}/Serializer.php | 2 +- .../XmlDeserializer.php | 4 +- .../XmlSerializer.php | 4 +- .../src/ExportImport/Import/CleanupStage.php | 6 +-- .../Import/DetectLegacyImportStage.php | 6 +-- .../src/ExportImport/Import/PersistStage.php | 8 ++-- .../Import/QuestionPoolImporter.php | 10 ++-- .../Import/QuestionSelectionStage.php | 8 ++-- .../ExportImport/Import/QuestionsImporter.php | 12 ++--- .../Import/SkillAssignmentsImporter.php | 2 +- .../Import/UploadValidationStage.php | 6 +-- .../{ => Normalize}/Envelopes/Feedback.php | 8 ++-- .../Envelopes/QuestionImage.php | 6 +-- .../SuggestedSolutionNormalizer.php | 13 +++--- ...ilAssQuestionSkillAssignmentNormalizer.php | 11 +++-- .../ilObjQuestionPoolNormalizer.php | 9 ++-- .../Processors}/CollectQuestionImages.php | 10 ++-- .../QuestionPoolNormalizerRegistration.php | 10 ++-- .../ExportImport/TransformationsBuilder.php | 12 ++--- .../TestQuestionPool/src/QuestionPoolDIC.php | 4 +- .../ExportImportCharacterizationTest.php | 46 +++++++++++++++---- .../tests/ExportImport/StateHolderTest.php | 2 +- 131 files changed, 491 insertions(+), 458 deletions(-) rename components/ILIAS/Test/src/ExportImport/{ => Normalize}/Envelopes/AdditionalWorkingTime.php (86%) rename components/ILIAS/Test/src/ExportImport/{Envelopes/Solution.php => Normalize/Envelopes/Attempt.php} (88%) rename components/ILIAS/Test/src/ExportImport/{ => Normalize}/Envelopes/ManualFeedback.php (90%) rename components/ILIAS/Test/src/ExportImport/{ => Normalize}/Envelopes/QuestionResult.php (90%) rename components/ILIAS/Test/src/ExportImport/{ => Normalize}/Envelopes/QuestionSetConfig.php (97%) rename components/ILIAS/Test/src/ExportImport/{ => Normalize}/Envelopes/RandomTestQuestion.php (89%) rename components/ILIAS/Test/src/ExportImport/{ => Normalize}/Envelopes/WorkingTime.php (86%) rename components/ILIAS/Test/src/ExportImport/{ => Normalize}/Normalizer/AttemptResultNormalizer.php (88%) rename components/ILIAS/Test/src/ExportImport/{ => Normalize}/Normalizer/ExportableNormalizer.php (86%) rename components/ILIAS/Test/src/ExportImport/{ => Normalize}/Normalizer/ParticipantNormalizer.php (92%) rename components/ILIAS/Test/src/ExportImport/{ => Normalize}/Normalizer/ParticipantResultNormalizer.php (86%) rename components/ILIAS/Test/src/ExportImport/{ => Normalize}/Normalizer/QuestionSetConfigNormalizer.php (94%) rename components/ILIAS/Test/src/ExportImport/{ => Normalize}/Normalizer/ilObjTestNormalizer.php (81%) rename components/ILIAS/Test/src/ExportImport/{ => Normalize}/Normalizer/ilTestSequenceNormalizer.php (91%) rename components/ILIAS/Test/src/ExportImport/{ => Normalize}/Normalizer/ilTestSkillLevelThresholdNormalizer.php (87%) rename components/ILIAS/Test/src/ExportImport/{Pipes => Normalize/Processors}/CollectUserIds.php (86%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Bridge => Export}/ExportState.php (96%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Bridge => Export}/ExportStep.php (91%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Contracts => Export}/Exporter.php (92%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Bridge => Export}/StateHolder.php (96%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Bridge => Export}/XmlExporterBridge.php (95%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Importing => Import}/ImportContext.php (99%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Importing => Import}/ImportSessionRepository.php (96%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Contracts => Import}/ImportStage.php (88%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Importing => Import}/ImportStageRunner.php (96%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Importing => Import}/StageResult.php (97%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Importing => Import}/StageResultType.php (94%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Contracts => Normalize}/Envelope.php (90%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing => Normalize}/Envelopes/Id.php (90%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing => Normalize}/FoundationNormalizerRegistration.php (72%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Contracts => Normalize}/FromNormalized.php (87%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Contracts => Normalize}/Normalizer.php (91%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing => Normalize}/Normalizer/DateTimeNormalizer.php (90%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing => Normalize}/Normalizer/EnvelopeNormalizer.php (82%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing => Normalize}/Normalizer/IlObjectNormalizer.php (94%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing => Normalize}/Normalizer/Legacy11UUIDNormalizer.php (94%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing => Normalize}/Normalizer/Registry.php (96%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing => Normalize}/Normalizer/ResourceNormalizer.php (94%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing => Normalize}/Normalizer/TransformationNormalizer.php (89%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing => Normalize}/Normalizer/UUIDNormalizer.php (89%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing => Normalize}/NormalizingException.php (99%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing/Pipes => Normalize/Processors}/CollectResources.php (94%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing/Pipes => Normalize/Processors}/DenormalizeCarry.php (90%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing/Pipes => Normalize/Processors}/DenormalizingProcessor.php (89%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing/Pipes => Normalize/Processors}/FinalizeNormalizing.php (94%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing/Pipes => Normalize/Processors}/IdMappingProcessor.php (93%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing/Pipes => Normalize/Processors}/NormalizeCarry.php (90%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing/Pipes => Normalize/Processors}/NormalizingProcessor.php (90%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Contracts => Normalize}/ToNormalized.php (88%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Normalizing => Normalize}/Transformations.php (95%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Contracts => Serialize}/Deserializer.php (94%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Contracts => Serialize}/Serializer.php (94%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Serializing => Serialize}/XmlDeserializer.php (98%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/{Serializing => Serialize}/XmlSerializer.php (98%) rename components/ILIAS/TestQuestionPool/src/ExportImport/{ => Normalize}/Envelopes/Feedback.php (88%) rename components/ILIAS/TestQuestionPool/src/ExportImport/{ => Normalize}/Envelopes/QuestionImage.php (90%) rename components/ILIAS/TestQuestionPool/src/ExportImport/{ => Normalize}/Normalizer/SuggestedSolutionNormalizer.php (91%) rename components/ILIAS/TestQuestionPool/src/ExportImport/{ => Normalize}/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php (92%) rename components/ILIAS/TestQuestionPool/src/ExportImport/{ => Normalize}/Normalizer/ilObjQuestionPoolNormalizer.php (82%) rename components/ILIAS/TestQuestionPool/src/ExportImport/{Pipes => Normalize/Processors}/CollectQuestionImages.php (89%) diff --git a/components/ILIAS/Test/classes/class.ilObjTestGUI.php b/components/ILIAS/Test/classes/class.ilObjTestGUI.php index ed8ee9160c6b..966b95c19ae2 100755 --- a/components/ILIAS/Test/classes/class.ilObjTestGUI.php +++ b/components/ILIAS/Test/classes/class.ilObjTestGUI.php @@ -69,10 +69,10 @@ use ILIAS\Test\Settings\Templates\PersonalSettingsTableShowAction; use ILIAS\Test\TestDIC; use ILIAS\Test\Utilities\TitleColumnsBuilder; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportStageRunner; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResultType; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSessionRepository; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportStageRunner; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\StageResultType; use ILIAS\TestQuestionPool\ExportImport\Import\CleanupStage; use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionSelectionStage; diff --git a/components/ILIAS/Test/classes/class.ilTestExporter.php b/components/ILIAS/Test/classes/class.ilTestExporter.php index 28640ac3e920..e66ade8befa0 100755 --- a/components/ILIAS/Test/classes/class.ilTestExporter.php +++ b/components/ILIAS/Test/classes/class.ilTestExporter.php @@ -21,8 +21,8 @@ use ILIAS\Export\ExportHandler\Factory as ExportHandler; use ILIAS\Test\ExportImport\Types; use ILIAS\Test\TestDIC; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportStep; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\XmlExporterBridge; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportStep; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\XmlExporterBridge; class ilTestExporter extends ilXmlExporter { diff --git a/components/ILIAS/Test/classes/class.ilTestImporter.php b/components/ILIAS/Test/classes/class.ilTestImporter.php index 4f8d1bffa700..fa395dc680f3 100755 --- a/components/ILIAS/Test/classes/class.ilTestImporter.php +++ b/components/ILIAS/Test/classes/class.ilTestImporter.php @@ -21,8 +21,8 @@ use ILIAS\Data\ReferenceId; use ILIAS\Test\ExportImport\Import\TestImporter; use ILIAS\Test\TestDIC; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlDeserializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSessionRepository; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\XmlDeserializer; class ilTestImporter extends ilXmlImporter { diff --git a/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php b/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php index a7809d6513f3..a52f5fa45fb3 100755 --- a/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php +++ b/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php @@ -20,7 +20,7 @@ use ILIAS\ResourceStorage\Services as ResourceStorage; use ILIAS\Test\RequestDataCollector; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSessionRepository; use ILIAS\TestQuestionPool\Import\TestQuestionsImportTrait; use ILIAS\Test\TestDIC; use ILIAS\Test\Logging\TestLogger; diff --git a/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php b/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php index b8a48ae6a683..ddc5fbce1abf 100755 --- a/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php +++ b/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php @@ -18,10 +18,10 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; /** * @author Björn Heyser diff --git a/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php b/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php index 1ce13929c0a5..669164749c1a 100644 --- a/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php +++ b/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php @@ -28,7 +28,7 @@ use ILIAS\Language\Language; use ILIAS\Test\ExportImport\Types; use ILIAS\Test\TestDIC; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\StateHolder; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\StateHolder; class XMLWithResultsOption extends BasicLegacyExportOption { diff --git a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php index 9c54fe3f65e3..730f5bce56e6 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php +++ b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php @@ -21,11 +21,11 @@ namespace ILIAS\Test\ExportImport\Import; use ILIAS\Language\Language; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ImportStage; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\StageResult; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlDeserializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportStage; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSessionRepository; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\StageResult; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\XmlDeserializer; use ilImport; use Psr\Log\LoggerInterface; diff --git a/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php b/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php index 2b31f75c63db..084d7a433f17 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php @@ -22,7 +22,7 @@ use ilDBInterface; use ILIAS\Data\Factory as DataFactory; -use ILIAS\Test\ExportImport\Envelopes\QuestionSetConfig; +use ILIAS\Test\ExportImport\Normalize\Envelopes\QuestionSetConfig; use ilImportMapping; use ilObjTest; use ilTestRandomQuestionSetSourcePoolDefinition; diff --git a/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php b/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php index 3a529c108996..8f86ff0fe030 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php @@ -25,7 +25,7 @@ use ilSkillTreeRepository; use ilTestSkillLevelThreshold; use ilTestSkillLevelThresholdList; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use Psr\Log\LoggerInterface; /** diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php index b3ddc299a2dc..1b354795923a 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php @@ -28,7 +28,7 @@ use ILIAS\Data\UUID\Factory as UUIDFactory; use ILIAS\Filesystem\Stream\Streams; use ILIAS\ResourceStorage\Services as IRSS; -use ILIAS\Test\ExportImport\Envelopes\QuestionSetConfig; +use ILIAS\Test\ExportImport\Normalize\Envelopes\QuestionSetConfig; use ILIAS\Test\Participants\Participant; use ILIAS\Test\Scoring\Marks\MarkSchema; use ILIAS\Test\Scoring\Marks\MarksRepository; @@ -36,15 +36,15 @@ use ILIAS\Test\Settings\MainSettings\MainSettings; use ILIAS\Test\Settings\ScoreReporting\ScoreSettings; use ILIAS\Test\ExportImport\TransformationsBuilder; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Deserializer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\CollectResources; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\IdMappingProcessor; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\Deserializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\CollectResources; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\IdMappingProcessor; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionsImporter; use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter; -use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; +use TestQuestionPool\ExportImport\Normalize\Processors\CollectQuestionImages; use ilImportMapping; use ilObjTest; use ilTestPage; diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php index 63d9e855f658..c11be7cce412 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php @@ -22,17 +22,17 @@ use ilDBConstants; use ilDBInterface; -use ILIAS\Test\ExportImport\Envelopes\AdditionalWorkingTime; -use ILIAS\Test\ExportImport\Envelopes\ManualFeedback; -use ILIAS\Test\ExportImport\Envelopes\QuestionResult; -use ILIAS\Test\ExportImport\Envelopes\RandomTestQuestion; -use ILIAS\Test\ExportImport\Envelopes\Solution; -use ILIAS\Test\ExportImport\Envelopes\WorkingTime; +use ILIAS\Test\ExportImport\Normalize\Envelopes\AdditionalWorkingTime; +use ILIAS\Test\ExportImport\Normalize\Envelopes\ManualFeedback; +use ILIAS\Test\ExportImport\Normalize\Envelopes\QuestionResult; +use ILIAS\Test\ExportImport\Normalize\Envelopes\RandomTestQuestion; +use ILIAS\Test\ExportImport\Normalize\Envelopes\Attempt; +use ILIAS\Test\ExportImport\Normalize\Envelopes\WorkingTime; use ILIAS\Test\Results\Data\AttemptResult; use ILIAS\Test\Results\Data\ParticipantResult; use ILIAS\Test\TestManScoringDoneHelper; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; use ilTestSequence; use Psr\Log\LoggerInterface; @@ -48,7 +48,7 @@ public function __construct( } /** - * Import test results from a list of normalized data. It will import the test sequences, solutions and evaluation + * Import test results from a list of normalized data. It will import the test sequences, attempts and evaluation * results, as well as the working times and manual feedback. */ public function import(array $list, Transformations $tt): void @@ -57,7 +57,7 @@ public function import(array $list, Transformations $tt): void foreach ($set as $name => $data) { match($name) { 'sequences' => $this->importTestSequences($data, $tt), - 'solutions' => $this->importSolutions($data, $tt), + 'question_attempts' => $this->importQuestionAttempts($data, $tt), 'results' => $this->importQuestionResults($data, $tt), 'attempts' => $this->importAttemptResults($data, $tt), 'test_result' => $this->importTestResult($data, $tt), @@ -81,29 +81,29 @@ public function importTestSequences(array $list, Transformations $tt): void } } - public function importSolutions(array $list, Transformations $tt): void + public function importQuestionAttempts(array $list, Transformations $tt): void { foreach ($list as $normalized) { // The mapping processor replaces ActiveID and QuestionID - $solution = $tt->denormalize($normalized, Solution::class); + $attempt = $tt->denormalize($normalized, Attempt::class); $next_id = $this->database->nextId('tst_solutions'); $this->database->insert( 'tst_solutions', [ 'solution_id' => [ilDBConstants::T_INTEGER, $next_id], - 'active_fi' => [ilDBConstants::T_INTEGER, $solution->active_id->getId()], - 'question_fi' => [ilDBConstants::T_INTEGER, $solution->question_id->getId()], - 'pass' => [ilDBConstants::T_INTEGER, $solution->attempt], - 'value1' => [ilDBConstants::T_TEXT, $solution->value1 !== null ? (string) $solution->value1 : null], - 'value2' => [ilDBConstants::T_TEXT, $solution->value2], - 'points' => [ilDBConstants::T_FLOAT, $solution->points], - 'step' => [ilDBConstants::T_INTEGER, $solution->step], - 'authorized' => [ilDBConstants::T_INTEGER, $solution->authorized ? 1 : 0], + 'active_fi' => [ilDBConstants::T_INTEGER, $attempt->active_id->getId()], + 'question_fi' => [ilDBConstants::T_INTEGER, $attempt->question_id->getId()], + 'pass' => [ilDBConstants::T_INTEGER, $attempt->attempt], + 'value1' => [ilDBConstants::T_TEXT, $attempt->value1 !== null ? (string) $attempt->value1 : null], + 'value2' => [ilDBConstants::T_TEXT, $attempt->value2], + 'points' => [ilDBConstants::T_FLOAT, $attempt->points], + 'step' => [ilDBConstants::T_INTEGER, $attempt->step], + 'authorized' => [ilDBConstants::T_INTEGER, $attempt->authorized ? 1 : 0], 'tstamp' => [ilDBConstants::T_INTEGER, time()], ] ); - $this->log->debug("Stored solution in database: {$next_id}"); + $this->log->debug("Stored attempt in database: {$next_id}"); } } diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/AdditionalWorkingTime.php b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/AdditionalWorkingTime.php similarity index 86% rename from components/ILIAS/Test/src/ExportImport/Envelopes/AdditionalWorkingTime.php rename to components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/AdditionalWorkingTime.php index 380869203893..e7d49f4b3b64 100644 --- a/components/ILIAS/Test/src/ExportImport/Envelopes/AdditionalWorkingTime.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/AdditionalWorkingTime.php @@ -18,11 +18,11 @@ declare(strict_types=1); -namespace ILIAS\Test\ExportImport\Envelopes; +namespace ILIAS\Test\ExportImport\Normalize\Envelopes; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Envelope; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelope; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; class AdditionalWorkingTime implements Envelope { diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/Solution.php b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/Attempt.php similarity index 88% rename from components/ILIAS/Test/src/ExportImport/Envelopes/Solution.php rename to components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/Attempt.php index 413f1eb08a72..536a00bb55e6 100644 --- a/components/ILIAS/Test/src/ExportImport/Envelopes/Solution.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/Attempt.php @@ -18,15 +18,15 @@ declare(strict_types=1); -namespace ILIAS\Test\ExportImport\Envelopes; +namespace ILIAS\Test\ExportImport\Normalize\Envelopes; use ILIAS\ResourceStorage\Identification\ResourceIdentification; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Envelope; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer\ResourceNormalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelope; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer\ResourceNormalizer; -class Solution implements Envelope +class Attempt implements Envelope { public function __construct( public readonly Id $active_id, diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/ManualFeedback.php b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/ManualFeedback.php similarity index 90% rename from components/ILIAS/Test/src/ExportImport/Envelopes/ManualFeedback.php rename to components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/ManualFeedback.php index 01ecba4e6ec6..572b28e83319 100644 --- a/components/ILIAS/Test/src/ExportImport/Envelopes/ManualFeedback.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/ManualFeedback.php @@ -18,11 +18,11 @@ declare(strict_types=1); -namespace ILIAS\Test\ExportImport\Envelopes; +namespace ILIAS\Test\ExportImport\Normalize\Envelopes; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Envelope; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelope; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; class ManualFeedback implements Envelope { diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/QuestionResult.php b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/QuestionResult.php similarity index 90% rename from components/ILIAS/Test/src/ExportImport/Envelopes/QuestionResult.php rename to components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/QuestionResult.php index 5bc7dd307310..43ceaa4cb74a 100644 --- a/components/ILIAS/Test/src/ExportImport/Envelopes/QuestionResult.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/QuestionResult.php @@ -18,11 +18,11 @@ declare(strict_types=1); -namespace ILIAS\Test\ExportImport\Envelopes; +namespace ILIAS\Test\ExportImport\Normalize\Envelopes; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Envelope; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelope; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; class QuestionResult implements Envelope { diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/QuestionSetConfig.php b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/QuestionSetConfig.php similarity index 97% rename from components/ILIAS/Test/src/ExportImport/Envelopes/QuestionSetConfig.php rename to components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/QuestionSetConfig.php index 6e1a75dececb..43dc8be80ef8 100644 --- a/components/ILIAS/Test/src/ExportImport/Envelopes/QuestionSetConfig.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/QuestionSetConfig.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\Test\ExportImport\Envelopes; +namespace ILIAS\Test\ExportImport\Normalize\Envelopes; use ilTestQuestionSetConfig; use ilTestRandomQuestionSetConfig; diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/RandomTestQuestion.php b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/RandomTestQuestion.php similarity index 89% rename from components/ILIAS/Test/src/ExportImport/Envelopes/RandomTestQuestion.php rename to components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/RandomTestQuestion.php index c9718e7dbb52..50cc9f2ccbae 100644 --- a/components/ILIAS/Test/src/ExportImport/Envelopes/RandomTestQuestion.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/RandomTestQuestion.php @@ -18,11 +18,11 @@ declare(strict_types=1); -namespace ILIAS\Test\ExportImport\Envelopes; +namespace ILIAS\Test\ExportImport\Normalize\Envelopes; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Envelope; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelope; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; class RandomTestQuestion implements Envelope { diff --git a/components/ILIAS/Test/src/ExportImport/Envelopes/WorkingTime.php b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/WorkingTime.php similarity index 86% rename from components/ILIAS/Test/src/ExportImport/Envelopes/WorkingTime.php rename to components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/WorkingTime.php index 743d69191698..b2d6464502fb 100644 --- a/components/ILIAS/Test/src/ExportImport/Envelopes/WorkingTime.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/WorkingTime.php @@ -18,11 +18,11 @@ declare(strict_types=1); -namespace ILIAS\Test\ExportImport\Envelopes; +namespace ILIAS\Test\ExportImport\Normalize\Envelopes; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Envelope; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelope; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; class WorkingTime implements Envelope { diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/AttemptResultNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/AttemptResultNormalizer.php similarity index 88% rename from components/ILIAS/Test/src/ExportImport/Normalizer/AttemptResultNormalizer.php rename to components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/AttemptResultNormalizer.php index e8d3641e4036..a9a0d54b0d66 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/AttemptResultNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/AttemptResultNormalizer.php @@ -18,13 +18,13 @@ declare(strict_types=1); -namespace ILIAS\Test\ExportImport\Normalizer; +namespace ILIAS\Test\ExportImport\Normalize\Normalizer; use ILIAS\Test\Results\Data\AttemptResult; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** * @implements Normalizer diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ExportableNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ExportableNormalizer.php similarity index 86% rename from components/ILIAS/Test/src/ExportImport/Normalizer/ExportableNormalizer.php rename to components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ExportableNormalizer.php index 549f13f7cb93..73e2fd01fa9e 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/ExportableNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ExportableNormalizer.php @@ -18,11 +18,11 @@ declare(strict_types=1); -namespace ILIAS\Test\ExportImport\Normalizer; +namespace ILIAS\Test\ExportImport\Normalize\Normalizer; use ILIAS\Test\ExportImport\Exportable; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** * @implements Normalizer diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ParticipantNormalizer.php similarity index 92% rename from components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php rename to components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ParticipantNormalizer.php index 35c450307daa..b727e378f26b 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ParticipantNormalizer.php @@ -18,14 +18,14 @@ declare(strict_types=1); -namespace ILIAS\Test\ExportImport\Normalizer; +namespace ILIAS\Test\ExportImport\Normalize\Normalizer; use DateTimeImmutable; use ILIAS\Test\Participants\Participant; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** * @implements Normalizer diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantResultNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ParticipantResultNormalizer.php similarity index 86% rename from components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantResultNormalizer.php rename to components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ParticipantResultNormalizer.php index 28335e5c769e..6a2267483e3e 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/ParticipantResultNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ParticipantResultNormalizer.php @@ -18,14 +18,14 @@ declare(strict_types=1); -namespace ILIAS\Test\ExportImport\Normalizer; +namespace ILIAS\Test\ExportImport\Normalize\Normalizer; use ILIAS\Test\Results\Data\ParticipantResult; use ILIAS\Test\Scoring\Marks\Mark; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** * @implements Normalizer diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/QuestionSetConfigNormalizer.php similarity index 94% rename from components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php rename to components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/QuestionSetConfigNormalizer.php index 1522aada28cb..71018f4f32aa 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/QuestionSetConfigNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/QuestionSetConfigNormalizer.php @@ -18,16 +18,16 @@ declare(strict_types=1); -namespace ILIAS\Test\ExportImport\Normalizer; +namespace ILIAS\Test\ExportImport\Normalize\Normalizer; use ilComponentRepository; use ilDBInterface; -use ILIAS\Test\ExportImport\Envelopes\QuestionSetConfig; +use ILIAS\Test\ExportImport\Normalize\Envelopes\QuestionSetConfig; use ILIAS\Test\Logging\TestLogger; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ilLanguage; use ilObjTest; diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ilObjTestNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilObjTestNormalizer.php similarity index 81% rename from components/ILIAS/Test/src/ExportImport/Normalizer/ilObjTestNormalizer.php rename to components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilObjTestNormalizer.php index 1783cb80b698..e91ada1388be 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/ilObjTestNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilObjTestNormalizer.php @@ -18,13 +18,13 @@ declare(strict_types=1); -namespace ILIAS\Test\ExportImport\Normalizer; +namespace ILIAS\Test\ExportImport\Normalize\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; use ilObjTest; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer\IlObjectNormalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer\IlObjectNormalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** * @implements Normalizer diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSequenceNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilTestSequenceNormalizer.php similarity index 91% rename from components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSequenceNormalizer.php rename to components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilTestSequenceNormalizer.php index 963661d932f1..f9e7036cebf4 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSequenceNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilTestSequenceNormalizer.php @@ -18,13 +18,13 @@ declare(strict_types=1); -namespace ILIAS\Test\ExportImport\Normalizer; +namespace ILIAS\Test\ExportImport\Normalize\Normalizer; use ilDBInterface; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ilTestSequence; diff --git a/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSkillLevelThresholdNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilTestSkillLevelThresholdNormalizer.php similarity index 87% rename from components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSkillLevelThresholdNormalizer.php rename to components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilTestSkillLevelThresholdNormalizer.php index 2b44b04296ca..3254dd6e1688 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalizer/ilTestSkillLevelThresholdNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilTestSkillLevelThresholdNormalizer.php @@ -18,14 +18,14 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport\Normalizer; +namespace ILIAS\TestQuestionPool\ExportImport\Normalize\Normalizer; use ilTestSkillLevelThreshold; use ilDBInterface; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** * @implements Normalizer diff --git a/components/ILIAS/Test/src/ExportImport/Pipes/CollectUserIds.php b/components/ILIAS/Test/src/ExportImport/Normalize/Processors/CollectUserIds.php similarity index 86% rename from components/ILIAS/Test/src/ExportImport/Pipes/CollectUserIds.php rename to components/ILIAS/Test/src/ExportImport/Normalize/Processors/CollectUserIds.php index e670a7f6ae93..ce0972f394f0 100644 --- a/components/ILIAS/Test/src/ExportImport/Pipes/CollectUserIds.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Processors/CollectUserIds.php @@ -18,10 +18,10 @@ declare(strict_types=1); -namespace ILIAS\Test\ExportImport\Pipes; +namespace ILIAS\Test\ExportImport\Normalize\Processors; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\NormalizeCarry; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\NormalizeCarry; use ILIAS\TestQuestionPool\ExportImport\Foundation\Queue\Processor; /** diff --git a/components/ILIAS/Test/src/ExportImport/TestCollector.php b/components/ILIAS/Test/src/ExportImport/TestCollector.php index 4a5062ce00eb..912267f2141e 100644 --- a/components/ILIAS/Test/src/ExportImport/TestCollector.php +++ b/components/ILIAS/Test/src/ExportImport/TestCollector.php @@ -26,13 +26,13 @@ use ilDBInterface; use ILIAS\Data\ObjectId; use ILIAS\Language\Language; -use ILIAS\Test\ExportImport\Envelopes\AdditionalWorkingTime; -use ILIAS\Test\ExportImport\Envelopes\ManualFeedback; -use ILIAS\Test\ExportImport\Envelopes\QuestionResult; -use ILIAS\Test\ExportImport\Envelopes\QuestionSetConfig; -use ILIAS\Test\ExportImport\Envelopes\RandomTestQuestion; -use ILIAS\Test\ExportImport\Envelopes\Solution; -use ILIAS\Test\ExportImport\Envelopes\WorkingTime; +use ILIAS\Test\ExportImport\Normalize\Envelopes\AdditionalWorkingTime; +use ILIAS\Test\ExportImport\Normalize\Envelopes\ManualFeedback; +use ILIAS\Test\ExportImport\Normalize\Envelopes\QuestionResult; +use ILIAS\Test\ExportImport\Normalize\Envelopes\QuestionSetConfig; +use ILIAS\Test\ExportImport\Normalize\Envelopes\RandomTestQuestion; +use ILIAS\Test\ExportImport\Normalize\Envelopes\Attempt; +use ILIAS\Test\ExportImport\Normalize\Envelopes\WorkingTime; use ILIAS\Test\Logging\TestLogger; use ILIAS\Test\Participants\ParticipantRepository; use ILIAS\Test\Questions\Properties\Properties; @@ -41,7 +41,7 @@ use ILIAS\Test\Settings\GlobalSettings\UserIdentifiers; use ILIAS\Test\TestManScoringDoneHelper; use ILIAS\TestQuestionPool\ExportImport\Export\CollectsQuestions; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ilObjTest; use ilTestQuestionSetConfigFactory; @@ -300,7 +300,7 @@ public function getResults(int $participant_id): array $attempt_results = $this->results_repository->getTestAttemptResults($participant_id); $set = [ 'sequences' => $this->getSequences($participant_id, array_keys($attempt_results)), - 'solutions' => $this->getSolutions($participant_id), + 'question_attempts' => $this->getQuestionAttempts($participant_id), 'results' => $this->getQuestionResults($participant_id), 'attempts' => $attempt_results, 'test_result' => $this->results_repository->getTestResult($participant_id), @@ -319,9 +319,9 @@ public function getResults(int $participant_id): array } /** - * @return list + * @return list */ - public function getSolutions(int $participant_id): array + public function getQuestionAttempts(int $participant_id): array { $query = $this->db->queryF( "SELECT * FROM tst_solutions WHERE active_fi = %s", @@ -330,7 +330,7 @@ public function getSolutions(int $participant_id): array ); return array_map( - fn(array $row): Solution => Solution::fromRow($row), + fn(array $row): Attempt => Attempt::fromRow($row), $this->db->fetchAll($query) ); } diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index a91573a3c101..071a555cf58d 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -30,19 +30,19 @@ use ILIAS\Language\Language; use ILIAS\ResourceStorage\Services as IRSS; use ILIAS\Taxonomy\DomainService as Taxonomy; -use ILIAS\Test\ExportImport\Pipes\CollectUserIds; +use ILIAS\Test\ExportImport\Normalize\Processors\CollectUserIds; use ILIAS\Test\Logging\TestLogger; use ILIAS\Test\Participants\ParticipantRepository; use ILIAS\Test\Questions\Properties\Repository as QuestionsRepository; use ILIAS\Test\Results\Data\Repository as ResultsRepository; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportStep; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\ExportState; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Exporter; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Serializer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\CollectResources; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlSerializer; -use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportStep; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportState; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\Exporter; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\Serializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\CollectResources; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\XmlSerializer; +use TestQuestionPool\ExportImport\Normalize\Processors\CollectQuestionImages; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; use ilTree; diff --git a/components/ILIAS/Test/src/ExportImport/TestNormalizerRegistration.php b/components/ILIAS/Test/src/ExportImport/TestNormalizerRegistration.php index 5596dd1f11c5..a6ce9bc18722 100644 --- a/components/ILIAS/Test/src/ExportImport/TestNormalizerRegistration.php +++ b/components/ILIAS/Test/src/ExportImport/TestNormalizerRegistration.php @@ -21,21 +21,21 @@ namespace ILIAS\Test\ExportImport; use ILIAS\DI\Container; -use ILIAS\Test\ExportImport\Envelopes\QuestionSetConfig; -use ILIAS\Test\ExportImport\Normalizer\AttemptResultNormalizer; -use ILIAS\Test\ExportImport\Normalizer\ExportableNormalizer; -use ILIAS\Test\ExportImport\Normalizer\ParticipantNormalizer; -use ILIAS\Test\ExportImport\Normalizer\ParticipantResultNormalizer; -use ILIAS\Test\ExportImport\Normalizer\QuestionSetConfigNormalizer; -use ILIAS\Test\ExportImport\Normalizer\ilObjTestNormalizer; -use ILIAS\Test\ExportImport\Normalizer\ilTestSequenceNormalizer; +use ILIAS\Test\ExportImport\Normalize\Envelopes\QuestionSetConfig; +use ILIAS\Test\ExportImport\Normalize\Normalizer\AttemptResultNormalizer; +use ILIAS\Test\ExportImport\Normalize\Normalizer\ExportableNormalizer; +use ILIAS\Test\ExportImport\Normalize\Normalizer\ParticipantNormalizer; +use ILIAS\Test\ExportImport\Normalize\Normalizer\ParticipantResultNormalizer; +use ILIAS\Test\ExportImport\Normalize\Normalizer\QuestionSetConfigNormalizer; +use ILIAS\Test\ExportImport\Normalize\Normalizer\ilObjTestNormalizer; +use ILIAS\Test\ExportImport\Normalize\Normalizer\ilTestSequenceNormalizer; use ILIAS\Test\Participants\Participant; use ILIAS\Test\Results\Data\AttemptResult; use ILIAS\Test\Results\Data\ParticipantResult; use ILIAS\Test\TestDIC; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer\Registry; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Normalizer\ilTestSkillLevelThresholdNormalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer\Registry; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Normalize\Normalizer\ilTestSkillLevelThresholdNormalizer; final class TestNormalizerRegistration { diff --git a/components/ILIAS/Test/src/ExportImport/TransformationsBuilder.php b/components/ILIAS/Test/src/ExportImport/TransformationsBuilder.php index 16f2ef85cd30..e73db3cd5aab 100644 --- a/components/ILIAS/Test/src/ExportImport/TransformationsBuilder.php +++ b/components/ILIAS/Test/src/ExportImport/TransformationsBuilder.php @@ -22,12 +22,12 @@ use ILIAS\DI\Container; use ILIAS\Test\TestDIC; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\FoundationNormalizerRegistration; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer\Registry; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\DenormalizingProcessor; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\FinalizeNormalizing; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\NormalizingProcessor; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FoundationNormalizerRegistration; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer\Registry; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\DenormalizingProcessor; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\FinalizeNormalizing; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\NormalizingProcessor; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Queue\Processor; use ILIAS\TestQuestionPool\ExportImport\Foundation\Queue\Queue; use ILIAS\TestQuestionPool\ExportImport\QuestionPoolNormalizerRegistration; diff --git a/components/ILIAS/Test/src/TestDIC.php b/components/ILIAS/Test/src/TestDIC.php index 347887b8d674..7bc482c5ea78 100755 --- a/components/ILIAS/Test/src/TestDIC.php +++ b/components/ILIAS/Test/src/TestDIC.php @@ -53,8 +53,8 @@ use ILIAS\Test\Questions\Properties\DatabaseRepository as TestQuestionsDatabaseRepository; use ILIAS\Test\Results\Data\Factory as ResultsDataFactory; use ILIAS\Test\Results\Presentation\Factory as ResultsPresentationFactory; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\StateHolder; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\StateHolder; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionsImporter; use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter; use ILIAS\Test\ExportImport\Import\TestImporter; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryState.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryState.php index 15b3c1fbad3f..0c25c73792ca 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryState.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryState.php @@ -16,7 +16,7 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; /** * Class for true/false or yes/no answers diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php index 0482ab8938fb..6105a734c084 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php @@ -18,8 +18,8 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Normalize\Envelopes\QuestionImage; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; /** * Class for answers with a binary state indicator diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerCloze.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerCloze.php index cdb8bb6ba6cc..8e6f39e76660 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerCloze.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerCloze.php @@ -16,7 +16,7 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; /** * Class for cloze question numeric answers diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerErrorText.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerErrorText.php index 6cb9897d64d7..e338bcb3ffac 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerErrorText.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerErrorText.php @@ -16,9 +16,9 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; /** * Class for error text answers diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerImagemap.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerImagemap.php index b8e5a84bbd44..3294f8f0385f 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerImagemap.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerImagemap.php @@ -16,7 +16,7 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; /** * Class for true/false or yes/no answers diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatching.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatching.php index f385fa01b1e3..45b7e3c70d1f 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatching.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatching.php @@ -16,9 +16,9 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; /** * Class for matching question answers diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingPair.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingPair.php index 7b68eab9d425..0d9042831f93 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingPair.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingPair.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; /** * Class for matching question pairs diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php index c3d00fc9014f..e9cfb1e3f7ae 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php @@ -18,10 +18,10 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Normalize\Envelopes\QuestionImage; /** * Class for matching question terms diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponse.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponse.php index 6827a5a9dbce..979c30c60652 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponse.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponse.php @@ -16,7 +16,7 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; /** * Class for true/false or yes/no answers diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php index b2e2c592b756..4999f7aa9f2f 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php @@ -18,8 +18,8 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Normalize\Envelopes\QuestionImage; /** * ASS_AnswerBinaryStateImage is a class for answers with a binary state diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php index bc0376c79a09..120d5402ff6b 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php @@ -16,10 +16,10 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; /** * Class for simple answers diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerTrueFalse.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerTrueFalse.php index 3d07d5664b4f..4fa4a758ecac 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerTrueFalse.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerTrueFalse.php @@ -16,7 +16,7 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; /** * Class for true/false or yes/no answers diff --git a/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php b/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php index b46806894cdc..5380625dc0f1 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php @@ -16,9 +16,9 @@ * *********************************************************************/ -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\Refinery\Transformation; /** diff --git a/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php b/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php index 423e4f44e540..548525a07856 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\Questions\QuestionPartiallySaveable; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php b/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php index 91c63fa86a1d..ec3c40d1c9ba 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php b/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php index 389ee090e78c..ce27319f68fa 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\Test\Participants\ParticipantRepository; use ILIAS\Test\Logging\AdditionalInformationGenerator; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php index b6ba5a4fede5..9ac4fce42a38 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php index 1d0193b0e305..2097eeaf9e68 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php @@ -17,9 +17,9 @@ *********************************************************************/ use ILIAS\Refinery\Factory as Refinery; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; /** * Formula Question Result diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php index a0f57031c4fd..f0158abea8b2 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php @@ -18,10 +18,10 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; /** * Formula Question Unit diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php index 97dd73b41bd0..2ba80deb41aa 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php @@ -18,10 +18,10 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; /** * Formula Question Unit Category diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionVariable.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionVariable.php index 528327929bf0..0216c4e229f9 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionVariable.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionVariable.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; /** * Formula Question Variable diff --git a/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php index 7c78bc8a319d..f449fc96151e 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php @@ -18,10 +18,10 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Normalize\Envelopes\QuestionImage; use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\TestQuestionPool\RequestDataCollector; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php index da0b620eb286..7b657b47004e 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\ManipulateImagesInChoiceQuestionsTrait; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php b/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php index 8bf5b7bcfcb1..ccf0dc5a191c 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php index 6ab6ce1e4101..a1990c5a7b45 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php index 6b66e5cccfb9..e9f99dbede2e 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\ManipulateImagesInChoiceQuestionsTrait; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php b/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php index 99e6bde05fc7..ffe2dbdd6d95 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php b/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php index a435a4c49f0b..b8e14c9c3c60 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\Test\Logging\AdditionalInformationGenerator; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php index 6ac3f13f6e9b..4e4ef3c158e0 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php @@ -19,9 +19,9 @@ declare(strict_types=1); use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\Questions\Ordering\OrderingQuestionDatabaseRepository as OQRepository; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php index b11ba7d41b7e..0e99545495ef 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php @@ -36,8 +36,8 @@ use ILIAS\Test\Logging\TestQuestionAdministrationInteractionTypes; use ILIAS\Test\Results\Data\Repository as TestResultRepository; use ILIAS\Test\TestDIC; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; use ILIAS\TestQuestionPool\QuestionPoolDIC; use ILIAS\TestQuestionPool\Questions\Files\QuestionFiles; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php index 4fd9ef2864ea..6f65316d8ddf 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php @@ -18,9 +18,9 @@ declare(strict_types=1); -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\ManipulateImagesInChoiceQuestionsTrait; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php index 5d5d83384d28..fc6f97d89684 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php @@ -19,9 +19,9 @@ declare(strict_types=1); use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php b/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php index aa3790fff8fe..17c5a7e1ed88 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php @@ -19,9 +19,9 @@ declare(strict_types=1); use ILIAS\Test\Logging\AdditionalInformationGenerator; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\FromNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\ToNormalized; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FromNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\ToNormalized; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\Questions\QuestionAutosaveable; use ILIAS\TestQuestionPool\Questions\QuestionLMExportable; diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php b/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php index a1b1ef8bb988..18c7d24cf4f0 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php @@ -1,9 +1,9 @@ diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/EnvelopeNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/EnvelopeNormalizer.php similarity index 82% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/EnvelopeNormalizer.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/EnvelopeNormalizer.php index 4a5319c890d5..a82d9c166d58 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/EnvelopeNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/EnvelopeNormalizer.php @@ -18,12 +18,12 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer; +namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Envelope; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelope; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** * @implements Normalizer diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/IlObjectNormalizer.php similarity index 94% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/IlObjectNormalizer.php index d01650b9c9f0..8c7900d4a9b0 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/IlObjectNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/IlObjectNormalizer.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer; +namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; use ILIAS\ILIASObject\Properties\CoreProperties\Online; use ILIAS\ILIASObject\Properties\CoreProperties\TitleAndDescription; @@ -29,10 +29,10 @@ use ILIAS\ILIASObject\Properties\Translations\Translations; use ILIAS\ILIASObject\Properties\Properties; use ILIAS\ILIASObject\Properties\Property; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; use ilObject; use ilObjectFactory; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Legacy11UUIDNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/Legacy11UUIDNormalizer.php similarity index 94% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Legacy11UUIDNormalizer.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/Legacy11UUIDNormalizer.php index d1c4cca5f127..f3a06a197794 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Legacy11UUIDNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/Legacy11UUIDNormalizer.php @@ -18,10 +18,10 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer; +namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; use ILIAS\Data\UUID\Uuid; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** * Example class to demonstrate the usage of legacy normalizers. diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Registry.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/Registry.php similarity index 96% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Registry.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/Registry.php index 471bac140a72..cecb071dd2c2 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/Registry.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/Registry.php @@ -18,10 +18,10 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer; +namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** * Registry for normalizers. It is used to register and lookup normalizers for specific class types. diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/ResourceNormalizer.php similarity index 94% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/ResourceNormalizer.php index 012e4d1c75bd..f57976c1c62f 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/ResourceNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/ResourceNormalizer.php @@ -18,15 +18,15 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer; +namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; use ILIAS\ResourceStorage\Identification\ResourceIdentification; use ILIAS\ResourceStorage\Resource\Repository\ResourceRepository; use ILIAS\ResourceStorage\Resource\ResourceType; use ILIAS\ResourceStorage\Resource\StorableResource; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** * @implements Normalizer diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/TransformationNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/TransformationNormalizer.php similarity index 89% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/TransformationNormalizer.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/TransformationNormalizer.php index d2684442d37d..42b36d53c377 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/TransformationNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/TransformationNormalizer.php @@ -18,10 +18,10 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer; +namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; use ILIAS\Refinery\Factory as Refinery; use ILIAS\Refinery\Transformation; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/UUIDNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/UUIDNormalizer.php similarity index 89% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/UUIDNormalizer.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/UUIDNormalizer.php index 10d28c2779d4..03b2bd3780a7 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Normalizer/UUIDNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/UUIDNormalizer.php @@ -18,12 +18,12 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer; +namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; use ILIAS\Data\UUID\Factory; use ILIAS\Data\UUID\Uuid; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** * @implements Normalizer diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/NormalizingException.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/NormalizingException.php similarity index 99% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/NormalizingException.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/NormalizingException.php index ebf6c5344d12..88a37f434e40 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/NormalizingException.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/NormalizingException.php @@ -18,7 +18,7 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing; +namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize; use InvalidArgumentException; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Processors/CollectResources.php similarity index 94% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Processors/CollectResources.php index 7f0bb7fd866c..00133ee926b3 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/CollectResources.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Processors/CollectResources.php @@ -18,13 +18,13 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes; +namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors; use ILIAS\ResourceStorage\Identification\ResourceIdentification; use ILIAS\ResourceStorage\Resource\StorableResource; use ILIAS\ResourceStorage\Services as IRSS; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\NormalizeCarry; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\DenormalizeCarry; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\NormalizeCarry; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\DenormalizeCarry; use ILIAS\TestQuestionPool\ExportImport\Foundation\Queue\Processor; use Psr\Log\LoggerInterface; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizeCarry.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Processors/DenormalizeCarry.php similarity index 90% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizeCarry.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Processors/DenormalizeCarry.php index 29746a869411..80adf035cd9f 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalizing/Pipes/DenormalizeCarry.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Processors/DenormalizeCarry.php @@ -1,9 +1,9 @@ */ class SuggestedSolutionNormalizer implements Normalizer { diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php similarity index 92% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php index 4ffe639984b6..e7fdd59e3092 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php @@ -18,17 +18,18 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport\Normalizer; +namespace ILIAS\TestQuestionPool\ExportImport\Normalize\Normalizer; use ilAssQuestionSkillAssignment; use ilAssQuestionSolutionComparisonExpression; use ilDBInterface; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Envelopes\Id; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** + * @implements Normalizer */ class ilAssQuestionSkillAssignmentNormalizer implements Normalizer { diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilObjQuestionPoolNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/ilObjQuestionPoolNormalizer.php similarity index 82% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilObjQuestionPoolNormalizer.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/ilObjQuestionPoolNormalizer.php index d00e9f483250..67d046f98e30 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalizer/ilObjQuestionPoolNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/ilObjQuestionPoolNormalizer.php @@ -18,14 +18,15 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport\Normalizer; +namespace ILIAS\TestQuestionPool\ExportImport\Normalize\Normalizer; use ilObjQuestionPool; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Normalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer\IlObjectNormalizer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer\IlObjectNormalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** + * @implements Normalizer */ class ilObjQuestionPoolNormalizer extends IlObjectNormalizer implements Normalizer { diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Processors/CollectQuestionImages.php similarity index 89% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Processors/CollectQuestionImages.php index 4284f01e5355..b41e622c3b23 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Pipes/CollectQuestionImages.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Processors/CollectQuestionImages.php @@ -18,15 +18,15 @@ declare(strict_types=1); -namespace ILIAS\TestQuestionPool\ExportImport\Pipes; +namespace TestQuestionPool\ExportImport\Normalize\Processors; use ILIAS\Data\ObjectId; use ILIAS\Data\UUID\Factory; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\NormalizingException; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\DenormalizeCarry; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\NormalizeCarry; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\DenormalizeCarry; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\NormalizeCarry; use ILIAS\TestQuestionPool\ExportImport\Foundation\Queue\Processor; -use ILIAS\TestQuestionPool\ExportImport\Envelopes\QuestionImage; +use ILIAS\TestQuestionPool\ExportImport\Normalize\Envelopes\QuestionImage; use ILIAS\TestQuestionPool\Questions\Files\QuestionFiles; /** diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolNormalizerRegistration.php b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolNormalizerRegistration.php index 73ac1889b447..14656ec22d73 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolNormalizerRegistration.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/QuestionPoolNormalizerRegistration.php @@ -21,11 +21,11 @@ namespace ILIAS\TestQuestionPool\ExportImport; use ILIAS\DI\Container; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer\Registry; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Normalizer\ilAssQuestionSkillAssignmentNormalizer; -use ILIAS\TestQuestionPool\ExportImport\Normalizer\ilObjQuestionPoolNormalizer; -use ILIAS\TestQuestionPool\ExportImport\Normalizer\SuggestedSolutionNormalizer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer\Registry; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Normalize\Normalizer\ilAssQuestionSkillAssignmentNormalizer; +use ILIAS\TestQuestionPool\ExportImport\Normalize\Normalizer\ilObjQuestionPoolNormalizer; +use ILIAS\TestQuestionPool\ExportImport\Normalize\Normalizer\SuggestedSolutionNormalizer; use ILIAS\TestQuestionPool\Questions\SuggestedSolution\SuggestedSolution; final class QuestionPoolNormalizerRegistration diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/TransformationsBuilder.php b/components/ILIAS/TestQuestionPool/src/ExportImport/TransformationsBuilder.php index 7a449e30c781..73bae7963af5 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/TransformationsBuilder.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/TransformationsBuilder.php @@ -21,12 +21,12 @@ namespace ILIAS\TestQuestionPool\ExportImport; use ILIAS\DI\Container; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\FoundationNormalizerRegistration; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Normalizer\Registry; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\DenormalizingProcessor; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\FinalizeNormalizing; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\NormalizingProcessor; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Transformations; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FoundationNormalizerRegistration; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer\Registry; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\DenormalizingProcessor; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\FinalizeNormalizing; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\NormalizingProcessor; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Queue\Processor; use ILIAS\TestQuestionPool\ExportImport\Foundation\Queue\Queue; diff --git a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php index 3621a66c3a3c..50c2cffa74d8 100755 --- a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php +++ b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php @@ -21,8 +21,8 @@ namespace ILIAS\TestQuestionPool; use ILIAS\Data\Factory as DataFactory; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\StateHolder; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\StateHolder; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Export\QuestionPoolExporter; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionPoolImporter; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionsImporter; diff --git a/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php b/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php index 8d0a92e3b857..41da880394a9 100644 --- a/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php +++ b/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php @@ -24,16 +24,16 @@ use ILIAS\Refinery\Factory as RefineryFactory; use ILIAS\Test\ExportImport\TransformationsBuilder as TestTransformationsBuilder; use ILIAS\Test\TestDIC; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Contracts\Deserializer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportContext; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Importing\ImportSessionRepository; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\Deserializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Queue\Processor; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\DenormalizeCarry; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalizing\Pipes\NormalizeCarry; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlDeserializer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Serializing\XmlSerializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\DenormalizeCarry; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\NormalizeCarry; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\XmlDeserializer; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\XmlSerializer; use ILIAS\TestQuestionPool\ExportImport\TransformationsBuilder; -use ILIAS\TestQuestionPool\ExportImport\Pipes\CollectQuestionImages; +use TestQuestionPool\ExportImport\Normalize\Processors\CollectQuestionImages; use ILIAS\TestQuestionPool\QuestionPoolDIC; class ExportImportCharacterizationTest extends assBaseTestCase @@ -240,6 +240,36 @@ public function testXmlStringAndFileDeserializersProduceTheSameData(): void $this->assertSame($expected, $from_file); } + public function testResultXmlOnlyRenamesSolutionsToQuestionAttempts(): void + { + $serializer = XmlSerializer::inMemory(); + $serializer->createDocument('Result terminology characterization'); + $serializer->group('results', function () use ($serializer): void { + $serializer->append('set', [ + 'sequences' => [], + 'question_attempts' => [['attempt' => 1]], + 'results' => [], + 'attempts' => [['attempt' => 1]], + ]); + }); + + $sets = $this->deserializeGroup( + XmlDeserializer::fromString($serializer->write()), + 'results' + ); + + $this->assertSame( + [[ + 'sequences' => [], + 'question_attempts' => [['attempt' => '1']], + 'results' => [], + 'attempts' => [['attempt' => '1']], + ]], + $sets + ); + $this->assertArrayNotHasKey('solutions', $sets[0]); + } + public function testXmlMemoryDeserializerProcessesDocumentFragments(): void { $groups = []; diff --git a/components/ILIAS/TestQuestionPool/tests/ExportImport/StateHolderTest.php b/components/ILIAS/TestQuestionPool/tests/ExportImport/StateHolderTest.php index 706598c7b030..f071a70a8cc1 100644 --- a/components/ILIAS/TestQuestionPool/tests/ExportImport/StateHolderTest.php +++ b/components/ILIAS/TestQuestionPool/tests/ExportImport/StateHolderTest.php @@ -20,7 +20,7 @@ use ILIAS\Export\ExportHandler\I\Consumer\ExportConfig\CollectionInterface as ExportConfig; use ILIAS\Export\ExportHandler\I\Target\HandlerInterface as ExportTarget; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Bridge\StateHolder; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\StateHolder; class StateHolderTest extends assBaseTestCase { From 36a1d00f3435b0cb0c831016c22805f7b92fa5e8 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Tue, 22 Sep 2026 11:23:07 +0200 Subject: [PATCH 57/66] refactor: code style improvements --- ...tRandomQuestionSetSourcePoolDefinition.php | 10 +- .../ExportOptions/XMLWithResultsOption.php | 3 +- .../src/ExportImport/Import/PersistStage.php | 8 +- .../Import/RandomTestConfigImporter.php | 33 ++-- .../Import/SkillLevelThresholdsImporter.php | 21 +-- .../src/ExportImport/Import/TestImporter.php | 88 +++++----- .../Import/TestResultsImporter.php | 153 +++++++++--------- .../Envelopes/AdditionalWorkingTime.php | 12 +- .../Normalize/Envelopes/Attempt.php | 12 +- .../Normalize/Envelopes/ManualFeedback.php | 12 +- .../Normalize/Envelopes/QuestionResult.php | 12 +- .../Normalize/Envelopes/QuestionSetConfig.php | 15 +- .../Envelopes/RandomTestQuestion.php | 12 +- .../Normalize/Envelopes/WorkingTime.php | 12 +- .../Normalizer/AttemptResultNormalizer.php | 10 +- .../Normalizer/ExportableNormalizer.php | 10 +- .../Normalizer/ParticipantNormalizer.php | 15 +- .../ParticipantResultNormalizer.php | 10 +- .../QuestionSetConfigNormalizer.php | 62 +++---- .../Normalizer/ilObjTestNormalizer.php | 21 +-- .../Normalizer/ilTestSequenceNormalizer.php | 22 +-- .../ilTestSkillLevelThresholdNormalizer.php | 22 +-- .../Normalize/Processors/CollectUserIds.php | 4 +- .../Test/src/ExportImport/TestCollector.php | 76 ++++----- .../Test/src/ExportImport/TestExporter.php | 27 +--- .../classes/class.assAnswerBinaryState.php | 8 +- .../class.assAnswerBinaryStateImage.php | 8 +- .../classes/class.assAnswerCloze.php | 8 +- .../classes/class.assAnswerErrorText.php | 8 +- .../classes/class.assAnswerImagemap.php | 8 +- .../classes/class.assAnswerMatching.php | 8 +- .../classes/class.assAnswerMatchingPair.php | 8 +- .../classes/class.assAnswerMatchingTerm.php | 8 +- .../class.assAnswerMultipleResponse.php | 8 +- .../class.assAnswerMultipleResponseImage.php | 8 +- .../classes/class.assAnswerSimple.php | 8 +- .../classes/class.assAnswerTrueFalse.php | 8 +- .../classes/class.assClozeGap.php | 8 +- .../classes/class.assClozeTest.php | 8 +- .../classes/class.assErrorText.php | 8 +- .../classes/class.assFileUpload.php | 8 +- .../classes/class.assFormulaQuestion.php | 8 +- .../class.assFormulaQuestionResult.php | 8 +- .../classes/class.assFormulaQuestionUnit.php | 8 +- .../class.assFormulaQuestionUnitCategory.php | 8 +- .../class.assFormulaQuestionVariable.php | 8 +- .../classes/class.assImagemapQuestion.php | 8 +- .../classes/class.assKprimChoice.php | 8 +- .../classes/class.assLongMenu.php | 8 +- .../classes/class.assMatchingQuestion.php | 8 +- .../classes/class.assMultipleChoice.php | 8 +- .../classes/class.assNumeric.php | 8 +- .../classes/class.assOrderingHorizontal.php | 8 +- .../classes/class.assOrderingQuestion.php | 8 +- .../classes/class.assSingleChoice.php | 8 +- .../classes/class.assTextQuestion.php | 8 +- .../classes/class.assTextSubset.php | 8 +- .../questions/class.ilAssOrderingElement.php | 8 +- .../ExportImport/Export/CollectsQuestions.php | 40 ++--- .../Export/QuestionPoolCollector.php | 13 +- .../Export/QuestionPoolExporter.php | 6 +- .../Foundation/Export/ExportState.php | 5 +- .../Foundation/Export/StateHolder.php | 3 +- .../Foundation/Normalize/Envelope.php | 4 +- .../Foundation/Normalize/Envelopes/Id.php | 12 +- .../FoundationNormalizerRegistration.php | 5 +- .../Normalizer/DateTimeNormalizer.php | 20 +-- .../Normalizer/EnvelopeNormalizer.php | 14 +- .../Normalizer/IlObjectNormalizer.php | 20 +-- .../Normalizer/Legacy11UUIDNormalizer.php | 8 +- .../Normalizer/ResourceNormalizer.php | 10 +- .../Normalizer/TransformationNormalizer.php | 8 +- .../Normalize/Normalizer/UUIDNormalizer.php | 8 +- .../Normalize/NormalizingException.php | 3 +- .../Normalize/Processors/CollectResources.php | 3 - .../Foundation/Normalize/Transformations.php | 3 +- .../src/ExportImport/Import/PersistStage.php | 5 +- .../Import/QuestionPoolImporter.php | 12 +- .../Import/QuestionSelectionStage.php | 3 +- .../ExportImport/Import/QuestionsImporter.php | 47 +++--- .../Import/SkillAssignmentsImporter.php | 13 +- .../Import/UploadValidationStage.php | 3 +- .../Normalize/Envelopes/Feedback.php | 12 +- .../Normalize/Envelopes/QuestionImage.php | 12 +- .../SuggestedSolutionNormalizer.php | 13 +- ...ilAssQuestionSkillAssignmentNormalizer.php | 39 ++--- .../ilObjQuestionPoolNormalizer.php | 21 +-- .../Processors/CollectQuestionImages.php | 6 +- .../ExportImportCharacterizationTest.php | 1 + .../tests/ilAssQuestionLifecycleTest.php | 26 ++- 90 files changed, 500 insertions(+), 844 deletions(-) diff --git a/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php b/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php index ddc5fbce1abf..c3f74c639632 100755 --- a/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php +++ b/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php @@ -475,9 +475,7 @@ public function getPoolInfoLabel(ilLanguage $lng): string // ----------------------------------------------------------------------------------------------------------------- - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -520,9 +518,7 @@ public function toNormalized( return $normalized; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations @@ -561,4 +557,4 @@ public function fromNormalized( return $clone; } -} +} \ No newline at end of file diff --git a/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php b/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php index 669164749c1a..487e0d1b06ef 100644 --- a/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php +++ b/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php @@ -20,7 +20,6 @@ namespace ILIAS\Test\ExportImport\ExportOptions; -use ilExportGUI; use ILIAS\Export\ExportHandler\Consumer\ExportOption\BasicLegacyHandler as BasicLegacyExportOption; use ILIAS\Export\ExportHandler\Factory as ExportHandlerLocator; use ILIAS\Export\ExportHandler\I\Consumer\Context\HandlerInterface as ConsumerContext; @@ -88,6 +87,6 @@ public function onExportOptionSelected(ConsumerContext $context): void // Delegate the export to the manager which will call ilTestExporter $manager->createExport(1, $export_info, ''); - $this->ctrl->redirectByClass(ilExportGUI::class, ilExportGUI::CMD_LIST_EXPORT_FILES); + $this->ctrl->redirectByClass(\ilExportGUI::class, \ilExportGUI::CMD_LIST_EXPORT_FILES); } } diff --git a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php index 730f5bce56e6..8f14fd279b53 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php +++ b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php @@ -26,12 +26,11 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\StageResult; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\XmlDeserializer; -use ilImport; use Psr\Log\LoggerInterface; /** * Final stage of the test import process. Imports the head dependencies (user and resource mappings) and then - * imports the test object and all its dependencies using `ilImport`. It will delegate the import to the + * imports the test object and all its dependencies using `\ilImport`. It will delegate the import to the * `ilTestImporter` class. */ class PersistStage implements ImportStage @@ -62,12 +61,13 @@ public function getDescription(): ?string public function process(ImportContext $context): StageResult { if (!$context->isLegacyImport()) { - if ($result = $this->importMappingsFile($context)) { + $result = $this->importMappingsFile($context); + if ($result !== null) { return $result; } } - (new ilImport($this->requested_ref_id))->importObject( + (new \ilImport($this->requested_ref_id))->importObject( null, $context->fileToImport(), basename($context->fileToImport()), diff --git a/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php b/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php index 084d7a433f17..ca8ad9393383 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php @@ -20,15 +20,8 @@ namespace ILIAS\Test\ExportImport\Import; -use ilDBInterface; use ILIAS\Data\Factory as DataFactory; use ILIAS\Test\ExportImport\Normalize\Envelopes\QuestionSetConfig; -use ilImportMapping; -use ilObjTest; -use ilTestRandomQuestionSetSourcePoolDefinition; -use ilTestRandomQuestionSetSourcePoolDefinitionFactory; -use ilTestRandomQuestionSetSourcePoolDefinitionList; -use ilTestRandomQuestionSetStagingPoolQuestion; use Psr\Log\LoggerInterface; /** @@ -38,7 +31,7 @@ class RandomTestConfigImporter { public function __construct( - private readonly ilDBInterface $database, + private readonly \ilDBInterface $database, private readonly LoggerInterface $log, private readonly DataFactory $data_factory ) { @@ -50,8 +43,8 @@ public function __construct( */ public function import( QuestionSetConfig $config, - ilImportMapping $mapping, - ilObjTest $test_object + \ilImportMapping $mapping, + \ilObjTest $test_object ): void { if (!$config->isRandom()) { throw new \InvalidArgumentException('Expected random question set config'); @@ -72,8 +65,8 @@ public function import( private function importRandomQuestionStagingPool( int $old_pool_id, array $questions, - ilImportMapping $mapping, - ilObjTest $test_object + \ilImportMapping $mapping, + \ilObjTest $test_object ): void { $new_pool_id = $this->database->nextId('object_data'); $mapping->addMapping( @@ -86,7 +79,7 @@ private function importRandomQuestionStagingPool( // QuestionID was mapped during question set config denormalization foreach ($questions as $question_id) { - $question = new ilTestRandomQuestionSetStagingPoolQuestion($this->database); + $question = new \ilTestRandomQuestionSetStagingPoolQuestion($this->database); $question->setTestId($test_object->getTestId()); $question->setPoolId($new_pool_id); $question->setQuestionId($question_id); @@ -96,8 +89,8 @@ private function importRandomQuestionStagingPool( } private function importSourcePoolDefinition( - ilTestRandomQuestionSetSourcePoolDefinition $definition, - ilImportMapping $mapping, + \ilTestRandomQuestionSetSourcePoolDefinition $definition, + \ilImportMapping $mapping, ): void { // New PoolID was not available during denormalization, so we have to map it here $old_pool_id = $definition->getPoolId(); @@ -130,7 +123,7 @@ private function importSourcePoolDefinition( * Taxonomy mappings are only available after the Taxonomy component has finished its import, so this must run * during finalProcessing(). */ - public function finalizeTaxonomyFilters(ilImportMapping $mapping): void + public function finalizeTaxonomyFilters(\ilImportMapping $mapping): void { $tst_mappings = $mapping->getMappingsOfEntity('components/ILIAS/Test', 'tst'); @@ -139,13 +132,13 @@ public function finalizeTaxonomyFilters(ilImportMapping $mapping): void continue; } - $test_obj = new ilObjTest(0, false); + $test_obj = new \ilObjTest(0, false); $test_obj->setTestId((int) $new_test_id); - $definition_list = new ilTestRandomQuestionSetSourcePoolDefinitionList( + $definition_list = new \ilTestRandomQuestionSetSourcePoolDefinitionList( $this->database, $test_obj, - new ilTestRandomQuestionSetSourcePoolDefinitionFactory($this->database, $test_obj) + new \ilTestRandomQuestionSetSourcePoolDefinitionFactory($this->database, $test_obj) ); $definition_list->loadDefinitions(); @@ -162,7 +155,7 @@ public function finalizeTaxonomyFilters(ilImportMapping $mapping): void } } - private function remapTaxonomyFilter(ilImportMapping $mapping, array $filter): array + private function remapTaxonomyFilter(\ilImportMapping $mapping, array $filter): array { $remapped = []; foreach ($filter as $tax_id => $node_ids) { diff --git a/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php b/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php index 8f86ff0fe030..432494bb7b84 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php @@ -20,11 +20,6 @@ namespace ILIAS\Test\ExportImport\Import; -use ilDBInterface; -use ilImportMapping; -use ilSkillTreeRepository; -use ilTestSkillLevelThreshold; -use ilTestSkillLevelThresholdList; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use Psr\Log\LoggerInterface; @@ -33,7 +28,7 @@ * the source installation id. If a skill level cannot be mapped, the threshold is added to the failed list. * * Depends on SkillAssignmentsImporter having run first, so that skill_base and skill_tref id mappings are already - * registered in ilImportMapping before thresholds are denormalized. + * registered in \ilImportMapping before thresholds are denormalized. * * @phpstan-type ImportResultData array{skill_base_id: int, skill_tref_id: int, skill_level_id: int, threshold: int} */ @@ -41,8 +36,8 @@ class SkillLevelThresholdsImporter { public function __construct( private readonly LoggerInterface $log, - private readonly ilDBInterface $db, - private readonly ilSkillTreeRepository $skill_repo, + private readonly \ilDBInterface $db, + private readonly \ilSkillTreeRepository $skill_repo, private readonly string $component, private readonly int $local_install_id ) { @@ -59,14 +54,14 @@ public function import( array $normalized_thresholds, int $import_install_id, Transformations $transformations, - ilImportMapping $mapping, + \ilImportMapping $mapping, ): array { $result = ['failed' => [], 'success' => []]; - $threshold_list = new ilTestSkillLevelThresholdList($this->db); + $threshold_list = new \ilTestSkillLevelThresholdList($this->db); foreach ($normalized_thresholds as $item) { // The mapping processor replaces TestID and Skill BaseID/TRefID - $threshold = $transformations->denormalize($item, ilTestSkillLevelThreshold::class); + $threshold = $transformations->denormalize($item, \ilTestSkillLevelThreshold::class); $local_level_id = $this->getLevelIdMapping($import_install_id, $threshold->getSkillLevelId()); if ($local_level_id === null) { @@ -94,7 +89,7 @@ public function import( return $result; } - protected function getLevelIdMapping(int $import_install_id, int $import_level_id): ?int + private function getLevelIdMapping(int $import_install_id, int $import_level_id): ?int { if ($import_install_id === $this->local_install_id) { return $import_level_id; @@ -112,7 +107,7 @@ protected function getLevelIdMapping(int $import_install_id, int $import_level_i /** * @return ImportResultData */ - private function buildResultData(ilTestSkillLevelThreshold $threshold): array + private function buildResultData(\ilTestSkillLevelThreshold $threshold): array { return [ 'skill_base_id' => $threshold->getSkillBaseId() ?? 0, diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php index 1b354795923a..0cba30b22be0 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php @@ -20,9 +20,6 @@ namespace ILIAS\Test\ExportImport\Import; -use assFileUploadStakeholder; -use ilDBConstants; -use ilDBInterface; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\ReferenceId; use ILIAS\Data\UUID\Factory as UUIDFactory; @@ -45,9 +42,6 @@ use ILIAS\TestQuestionPool\ExportImport\Import\QuestionsImporter; use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter; use TestQuestionPool\ExportImport\Normalize\Processors\CollectQuestionImages; -use ilImportMapping; -use ilObjTest; -use ilTestPage; use Psr\Log\LoggerInterface; /** @@ -57,7 +51,7 @@ class TestImporter { public function __construct( private readonly TransformationsBuilder $builder, - private readonly ilDBInterface $database, + private readonly \ilDBInterface $database, private readonly LoggerInterface $log, private readonly IRSS $irss, private readonly DataFactory $data_factory, @@ -77,7 +71,7 @@ public function __construct( */ public function import( Deserializer $deserializer, - ilImportMapping $mapping, + \ilImportMapping $mapping, ReferenceId $parent_id, ImportContext $context ): ImportContext { @@ -91,7 +85,7 @@ public function import( $resource_collector ); - /** @var ilObjTest|null $test_object */ + /** @var \ilObjTest|null $test_object */ $test_object = null; $deserializer->addHandler( @@ -226,7 +220,7 @@ function (array $times) use ($transformations): void { * It will replace the old question ids with the new question ids in the test pages and remap taxonomy IDs in random * question set source pool definitions. */ - public function finalize(ilImportMapping $mapping): void + public function finalize(\ilImportMapping $mapping): void { $this->log->info('Finalizing test import...'); $this->questions_importer->finalizeQuestionPages($mapping); @@ -236,7 +230,7 @@ public function finalize(ilImportMapping $mapping): void private function importMappings( - ilImportMapping $mapping, + \ilImportMapping $mapping, CollectResources $resource_collector, ImportContext $context ): void { @@ -263,7 +257,7 @@ private function importMappings( $new_id = $this->irss->manage()->stream( Streams::ofResource(fopen($resource_path, 'rb')), - new assFileUploadStakeholder(), + new \assFileUploadStakeholder(), $resource['title'] ); $resource_collector->storeMapping($resource['id'], $new_id); @@ -275,10 +269,10 @@ private function importMappings( private function importTest( array $normalized, Transformations $transformations, - ilImportMapping $mapping, + \ilImportMapping $mapping, ReferenceId $parent_id - ): ilObjTest { - $test_object = $transformations->denormalize($normalized, ilObjTest::class); + ): \ilObjTest { + $test_object = $transformations->denormalize($normalized, \ilObjTest::class); $old_obj_id = $test_object->getId(); $old_test_id = $test_object->getTestId(); @@ -303,8 +297,8 @@ private function importTest( private function importSettings( array $list, Transformations $transformations, - ilImportMapping $mapping, - ilObjTest $test_object + \ilImportMapping $mapping, + \ilObjTest $test_object ): void { $settings_id = $test_object->getMainSettings()->getId(); @@ -312,7 +306,8 @@ private function importSettings( $scoring_settings = $transformations->denormalize($list[1], ScoreSettings::class)->withId($settings_id); $mark_schema = $transformations->denormalize($list[2], MarkSchema::class)->withTestId($test_object->getTestId()); - if ($intro_page_id = $main_settings->getIntroductionSettings()->getIntroductionPageId()) { + $intro_page_id = $main_settings->getIntroductionSettings()->getIntroductionPageId(); + if ($intro_page_id !== null && $intro_page_id !== 0) { $new_page_id = $this->createPage($intro_page_id, $test_object->getId(), $mapping); $main_settings = $main_settings->withIntroductionSettings( $main_settings->getIntroductionSettings()->withIntroductionPageId($new_page_id) @@ -320,7 +315,8 @@ private function importSettings( $this->log->debug("Imported introduction page: {$intro_page_id} -> {$new_page_id}"); } - if ($concluding_page_id = $main_settings->getFinishingSettings()->getConcludingRemarksPageId()) { + $concluding_page_id = $main_settings->getFinishingSettings()->getConcludingRemarksPageId(); + if ($concluding_page_id !== null && $concluding_page_id !== 0) { $new_page_id = $this->createPage($concluding_page_id, $test_object->getId(), $mapping); $main_settings = $main_settings->withFinishingSettings( $main_settings->getFinishingSettings()->withConcludingRemarksPageId($new_page_id) @@ -334,9 +330,9 @@ private function importSettings( $this->log->debug("Imported test settings and mark schema: {$settings_id} (Settings ID)"); } - private function createPage(int $imported_page_id, int $parent_id, ilImportMapping $mapping): int + private function createPage(int $imported_page_id, int $parent_id, \ilImportMapping $mapping): int { - $page = new ilTestPage(); + $page = new \ilTestPage(); $page->setParentId($parent_id); $page->createPageWithNextId(); @@ -353,9 +349,9 @@ private function createPage(int $imported_page_id, int $parent_id, ilImportMappi private function importQuestions( array $list, Transformations $transformations, - ilImportMapping $mapping, + \ilImportMapping $mapping, ImportContext $context, - ilObjTest $test_object + \ilObjTest $test_object ): void { foreach ($list as $normalized) { $question = $this->questions_importer->importQuestion( @@ -364,7 +360,7 @@ private function importQuestions( $mapping, $context->selectedQuestionIds() ); - if (!$question instanceof \assQuestion || $normalized['sequence'] === null) { + if (!($question instanceof \assQuestion) || $normalized['sequence'] === null) { continue; } @@ -380,8 +376,8 @@ private function importQuestions( private function importQuestionSetConfig( array $normalized, Transformations $transformations, - ilImportMapping $mapping, - ilObjTest $test_object + \ilImportMapping $mapping, + \ilObjTest $test_object ): void { $config = $transformations->denormalize($normalized, QuestionSetConfig::class); @@ -390,7 +386,7 @@ private function importQuestionSetConfig( } } - private function importParticipants(array $list, Transformations $transformations, ilImportMapping $mapping): void + private function importParticipants(array $list, Transformations $transformations, \ilImportMapping $mapping): void { foreach ($list as $normalized) { if ($normalized['active_id'] === null) { @@ -409,20 +405,20 @@ private function importParticipants(array $list, Transformations $transformation $this->database->insert( 'tst_active', [ - 'active_id' => [ilDBConstants::T_INTEGER, $new_active_id], - 'user_fi' => [ilDBConstants::T_INTEGER, $participant->getUserId()], - 'test_fi' => [ilDBConstants::T_INTEGER, $participant->getTestId()], - 'anonymous_id' => [ilDBConstants::T_TEXT, $participant->getAnonymousId()], - 'tries' => [ilDBConstants::T_INTEGER, $participant->getAttempts()], - 'submitted' => [ilDBConstants::T_INTEGER, $participant->getSubmitted() ? 1 : 0], - 'last_finished_pass' => [ilDBConstants::T_INTEGER, $participant->getLastFinishedAttempt()], - 'last_started_pass' => [ilDBConstants::T_INTEGER, $participant->getLastStartedAttempt()], - 'importname' => [ilDBConstants::T_TEXT, "{$participant->getFirstname()} {$participant->getLastname()}"], - 'tstamp' => [ilDBConstants::T_INTEGER, time()], - 'submittimestamp' => [ilDBConstants::T_TIMESTAMP, $transformations->nullableString($normalized['submittimestamp'])], - 'lastindex' => [ilDBConstants::T_INTEGER, $transformations->nullableInt($normalized['lastindex'])], - 'objective_container' => [ilDBConstants::T_INTEGER, $transformations->nullableInt($normalized['objective_container'])], - 'start_lock' => [ilDBConstants::T_TEXT, $transformations->nullableString($normalized['start_lock'])], + 'active_id' => [\ilDBConstants::T_INTEGER, $new_active_id], + 'user_fi' => [\ilDBConstants::T_INTEGER, $participant->getUserId()], + 'test_fi' => [\ilDBConstants::T_INTEGER, $participant->getTestId()], + 'anonymous_id' => [\ilDBConstants::T_TEXT, $participant->getAnonymousId()], + 'tries' => [\ilDBConstants::T_INTEGER, $participant->getAttempts()], + 'submitted' => [\ilDBConstants::T_INTEGER, $participant->getSubmitted() === true ? 1 : 0], + 'last_finished_pass' => [\ilDBConstants::T_INTEGER, $participant->getLastFinishedAttempt()], + 'last_started_pass' => [\ilDBConstants::T_INTEGER, $participant->getLastStartedAttempt()], + 'importname' => [\ilDBConstants::T_TEXT, "{$participant->getFirstname()} {$participant->getLastname()}"], + 'tstamp' => [\ilDBConstants::T_INTEGER, time()], + 'submittimestamp' => [\ilDBConstants::T_TIMESTAMP, $transformations->nullableString($normalized['submittimestamp'])], + 'lastindex' => [\ilDBConstants::T_INTEGER, $transformations->nullableInt($normalized['lastindex'])], + 'objective_container' => [\ilDBConstants::T_INTEGER, $transformations->nullableInt($normalized['objective_container'])], + 'start_lock' => [\ilDBConstants::T_TEXT, $transformations->nullableString($normalized['start_lock'])], ] ); $this->log->debug("Stored test session in database: {$new_active_id} (Active ID)"); @@ -437,11 +433,11 @@ private function importInvitedParticipant(array $normalized, Transformations $tr $this->database->insert( 'tst_invited_user', [ - 'test_fi' => [ilDBConstants::T_INTEGER, $participant->getTestId()], - 'user_fi' => [ilDBConstants::T_INTEGER, $participant->getUserId()], - 'ip_range_from' => [ilDBConstants::T_TEXT, $participant->getClientIpFrom()], - 'ip_range_to' => [ilDBConstants::T_TEXT, $participant->getClientIpTo()], - 'tstamp' => [ilDBConstants::T_INTEGER, $participant->getInvitationDate()], + 'test_fi' => [\ilDBConstants::T_INTEGER, $participant->getTestId()], + 'user_fi' => [\ilDBConstants::T_INTEGER, $participant->getUserId()], + 'ip_range_from' => [\ilDBConstants::T_TEXT, $participant->getClientIpFrom()], + 'ip_range_to' => [\ilDBConstants::T_TEXT, $participant->getClientIpTo()], + 'tstamp' => [\ilDBConstants::T_INTEGER, $participant->getInvitationDate()], ] ); $this->log->debug("Stored invited participant in database: {$participant->getUserId()} (User ID), {$participant->getTestId()} (Test ID)"); diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php index c11be7cce412..0170e452da9d 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php @@ -20,8 +20,6 @@ namespace ILIAS\Test\ExportImport\Import; -use ilDBConstants; -use ilDBInterface; use ILIAS\Test\ExportImport\Normalize\Envelopes\AdditionalWorkingTime; use ILIAS\Test\ExportImport\Normalize\Envelopes\ManualFeedback; use ILIAS\Test\ExportImport\Normalize\Envelopes\QuestionResult; @@ -33,7 +31,6 @@ use ILIAS\Test\TestManScoringDoneHelper; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; -use ilTestSequence; use Psr\Log\LoggerInterface; /** @@ -42,7 +39,7 @@ class TestResultsImporter { public function __construct( - private readonly ilDBInterface $database, + private readonly \ilDBInterface $database, private readonly LoggerInterface $log, ) { } @@ -71,17 +68,17 @@ public function import(array $list, Transformations $tt): void } } - public function importTestSequences(array $list, Transformations $tt): void + private function importTestSequences(array $list, Transformations $tt): void { foreach ($list as $normalized) { // The mapping processor replaces ActiveID and QuestionIDs - $sequence = $tt->denormalize($normalized, ilTestSequence::class); + $sequence = $tt->denormalize($normalized, \ilTestSequence::class); $sequence->saveToDb(); $this->log->debug("Stored test sequence in database: {$sequence->getActiveId()} (Active ID), {$sequence->getPass()} (Pass)"); } } - public function importQuestionAttempts(array $list, Transformations $tt): void + private function importQuestionAttempts(array $list, Transformations $tt): void { foreach ($list as $normalized) { // The mapping processor replaces ActiveID and QuestionID @@ -91,23 +88,23 @@ public function importQuestionAttempts(array $list, Transformations $tt): void $this->database->insert( 'tst_solutions', [ - 'solution_id' => [ilDBConstants::T_INTEGER, $next_id], - 'active_fi' => [ilDBConstants::T_INTEGER, $attempt->active_id->getId()], - 'question_fi' => [ilDBConstants::T_INTEGER, $attempt->question_id->getId()], - 'pass' => [ilDBConstants::T_INTEGER, $attempt->attempt], - 'value1' => [ilDBConstants::T_TEXT, $attempt->value1 !== null ? (string) $attempt->value1 : null], - 'value2' => [ilDBConstants::T_TEXT, $attempt->value2], - 'points' => [ilDBConstants::T_FLOAT, $attempt->points], - 'step' => [ilDBConstants::T_INTEGER, $attempt->step], - 'authorized' => [ilDBConstants::T_INTEGER, $attempt->authorized ? 1 : 0], - 'tstamp' => [ilDBConstants::T_INTEGER, time()], + 'solution_id' => [\ilDBConstants::T_INTEGER, $next_id], + 'active_fi' => [\ilDBConstants::T_INTEGER, $attempt->active_id->getId()], + 'question_fi' => [\ilDBConstants::T_INTEGER, $attempt->question_id->getId()], + 'pass' => [\ilDBConstants::T_INTEGER, $attempt->attempt], + 'value1' => [\ilDBConstants::T_TEXT, $attempt->value1 !== null ? (string) $attempt->value1 : null], + 'value2' => [\ilDBConstants::T_TEXT, $attempt->value2], + 'points' => [\ilDBConstants::T_FLOAT, $attempt->points], + 'step' => [\ilDBConstants::T_INTEGER, $attempt->step], + 'authorized' => [\ilDBConstants::T_INTEGER, $attempt->authorized === true ? 1 : 0], + 'tstamp' => [\ilDBConstants::T_INTEGER, time()], ] ); $this->log->debug("Stored attempt in database: {$next_id}"); } } - public function importQuestionResults(array $list, Transformations $tt): void + private function importQuestionResults(array $list, Transformations $tt): void { foreach ($list as $normalized) { // The mapping processor replaces ActiveID and QuestionID @@ -117,21 +114,21 @@ public function importQuestionResults(array $list, Transformations $tt): void $this->database->insert( 'tst_test_result', [ - 'test_result_id' => [ilDBConstants::T_INTEGER, $next_id], - 'active_fi' => [ilDBConstants::T_INTEGER, $result->active_id->getId()], - 'question_fi' => [ilDBConstants::T_INTEGER, $result->question_id->getId()], - 'pass' => [ilDBConstants::T_INTEGER, $result->attempt], - 'points' => [ilDBConstants::T_FLOAT, $result->points], - 'manual' => [ilDBConstants::T_INTEGER, $result->manual ? 1 : 0], - 'tstamp' => [ilDBConstants::T_INTEGER, time()], - 'answered' => [ilDBConstants::T_INTEGER, $result->answered ? 1 : 0], - 'step' => [ilDBConstants::T_INTEGER, $result->step], + 'test_result_id' => [\ilDBConstants::T_INTEGER, $next_id], + 'active_fi' => [\ilDBConstants::T_INTEGER, $result->active_id->getId()], + 'question_fi' => [\ilDBConstants::T_INTEGER, $result->question_id->getId()], + 'pass' => [\ilDBConstants::T_INTEGER, $result->attempt], + 'points' => [\ilDBConstants::T_FLOAT, $result->points], + 'manual' => [\ilDBConstants::T_INTEGER, $result->manual === true ? 1 : 0], + 'tstamp' => [\ilDBConstants::T_INTEGER, time()], + 'answered' => [\ilDBConstants::T_INTEGER, $result->answered === true ? 1 : 0], + 'step' => [\ilDBConstants::T_INTEGER, $result->step], ] ); $this->log->debug("Stored question result in database: {$next_id}"); } } - public function importAttemptResults(array $list, Transformations $tt): void + private function importAttemptResults(array $list, Transformations $tt): void { foreach ($list as $normalized) { // The mapping processor replaces ActiveID @@ -140,23 +137,23 @@ public function importAttemptResults(array $list, Transformations $tt): void $this->database->insert( 'tst_pass_result', [ - 'active_fi' => [ilDBConstants::T_INTEGER, $attempt->getActiveId()], - 'pass' => [ilDBConstants::T_INTEGER, $attempt->getAttempt()], - 'maxpoints' => [ilDBConstants::T_FLOAT, $attempt->getMaxPoints()], - 'points' => [ilDBConstants::T_FLOAT, $attempt->getReachedPoints()], - 'questioncount' => [ilDBConstants::T_INTEGER, $attempt->getQuestionCount()], - 'answeredquestions' => [ilDBConstants::T_INTEGER, $attempt->getAnsweredQuestions()], - 'workingtime' => [ilDBConstants::T_INTEGER, $attempt->getWorkingTime()], - 'exam_id' => [ilDBConstants::T_TEXT, $attempt->getExamId()], - 'finalized_by' => [ilDBConstants::T_TEXT, $attempt->getFinalizedBy()], - 'tstamp' => [ilDBConstants::T_INTEGER, time()], + 'active_fi' => [\ilDBConstants::T_INTEGER, $attempt->getActiveId()], + 'pass' => [\ilDBConstants::T_INTEGER, $attempt->getAttempt()], + 'maxpoints' => [\ilDBConstants::T_FLOAT, $attempt->getMaxPoints()], + 'points' => [\ilDBConstants::T_FLOAT, $attempt->getReachedPoints()], + 'questioncount' => [\ilDBConstants::T_INTEGER, $attempt->getQuestionCount()], + 'answeredquestions' => [\ilDBConstants::T_INTEGER, $attempt->getAnsweredQuestions()], + 'workingtime' => [\ilDBConstants::T_INTEGER, $attempt->getWorkingTime()], + 'exam_id' => [\ilDBConstants::T_TEXT, $attempt->getExamId()], + 'finalized_by' => [\ilDBConstants::T_TEXT, $attempt->getFinalizedBy()], + 'tstamp' => [\ilDBConstants::T_INTEGER, time()], ] ); $this->log->debug("Stored attempt result in database: {$attempt->getActiveId()} (Active ID), {$attempt->getAttempt()} (Pass)"); } } - public function importTestResult(?array $normalized, Transformations $tt): void + private function importTestResult(?array $normalized, Transformations $tt): void { if ($normalized === null) { $this->log->warning("Missing test result, skipping"); @@ -169,21 +166,21 @@ public function importTestResult(?array $normalized, Transformations $tt): void $this->database->insert( 'tst_result_cache', [ - 'active_fi' => [ilDBConstants::T_INTEGER, $result->getActiveId()], - 'pass' => [ilDBConstants::T_INTEGER, $result->getAttempt()], - 'max_points' => [ilDBConstants::T_FLOAT, $result->getMaxPoints()], - 'reached_points' => [ilDBConstants::T_FLOAT, $result->getReachedPoints()], - 'mark_short' => [ilDBConstants::T_TEXT, $result->getMark()->getShortName()], - 'mark_official' => [ilDBConstants::T_TEXT, $result->getMark()->getOfficialName()], - 'passed' => [ilDBConstants::T_INTEGER, $result->isPassed() ? 1 : 0], - 'failed' => [ilDBConstants::T_INTEGER, $result->isFailed() ? 1 : 0], - 'tstamp' => [ilDBConstants::T_INTEGER, time()], + 'active_fi' => [\ilDBConstants::T_INTEGER, $result->getActiveId()], + 'pass' => [\ilDBConstants::T_INTEGER, $result->getAttempt()], + 'max_points' => [\ilDBConstants::T_FLOAT, $result->getMaxPoints()], + 'reached_points' => [\ilDBConstants::T_FLOAT, $result->getReachedPoints()], + 'mark_short' => [\ilDBConstants::T_TEXT, $result->getMark()->getShortName()], + 'mark_official' => [\ilDBConstants::T_TEXT, $result->getMark()->getOfficialName()], + 'passed' => [\ilDBConstants::T_INTEGER, $result->isPassed() === true ? 1 : 0], + 'failed' => [\ilDBConstants::T_INTEGER, $result->isFailed() === true ? 1 : 0], + 'tstamp' => [\ilDBConstants::T_INTEGER, time()], ] ); $this->log->debug("Stored test result in database: {$result->getActiveId()} (Active ID), {$result->getAttempt()} (Pass)"); } - public function importWorkingTimes(array $list, Transformations $tt): void + private function importWorkingTimes(array $list, Transformations $tt): void { foreach ($list as $normalized) { // The mapping processor replaces ActiveID @@ -193,19 +190,19 @@ public function importWorkingTimes(array $list, Transformations $tt): void $this->database->insert( 'tst_times', [ - 'times_id' => [ilDBConstants::T_INTEGER, $next_id], - 'active_fi' => [ilDBConstants::T_INTEGER, $working_time->active_id->getId()], - 'pass' => [ilDBConstants::T_INTEGER, $working_time->attempt], - 'started' => [ilDBConstants::T_TIMESTAMP, $working_time->started], - 'finished' => [ilDBConstants::T_TIMESTAMP, $working_time->finished], - 'tstamp' => [ilDBConstants::T_INTEGER, time()], + 'times_id' => [\ilDBConstants::T_INTEGER, $next_id], + 'active_fi' => [\ilDBConstants::T_INTEGER, $working_time->active_id->getId()], + 'pass' => [\ilDBConstants::T_INTEGER, $working_time->attempt], + 'started' => [\ilDBConstants::T_TIMESTAMP, $working_time->started], + 'finished' => [\ilDBConstants::T_TIMESTAMP, $working_time->finished], + 'tstamp' => [\ilDBConstants::T_INTEGER, time()], ] ); $this->log->debug("Stored working time in database: {$next_id}"); } } - public function importManualFeedback(array $list, Transformations $tt): void + private function importManualFeedback(array $list, Transformations $tt): void { foreach ($list as $normalized) { // The mapping processor replaces ActiveID, QuestionID and UserID @@ -215,22 +212,22 @@ public function importManualFeedback(array $list, Transformations $tt): void $this->database->insert( 'tst_manual_fb', [ - 'manual_feedback_id' => [ilDBConstants::T_INTEGER, $next_id], - 'active_fi' => [ilDBConstants::T_INTEGER, $manual_feedback->active_id->getId()], - 'question_fi' => [ilDBConstants::T_INTEGER, $manual_feedback->question_id->getId()], - 'pass' => [ilDBConstants::T_INTEGER, $manual_feedback->attempt], - 'feedback' => [ilDBConstants::T_TEXT, $manual_feedback->feedback], - 'finalized_evaluation' => [ilDBConstants::T_INTEGER, $manual_feedback->finalized_evaluation ? 1 : 0], - 'finalized_timestamp' => [ilDBConstants::T_INTEGER, $manual_feedback->finalized_timestamp], - 'finalized_by_usr_id' => [ilDBConstants::T_INTEGER, $manual_feedback->finalized_by->getId()], - 'tstamp' => [ilDBConstants::T_INTEGER, time()], + 'manual_feedback_id' => [\ilDBConstants::T_INTEGER, $next_id], + 'active_fi' => [\ilDBConstants::T_INTEGER, $manual_feedback->active_id->getId()], + 'question_fi' => [\ilDBConstants::T_INTEGER, $manual_feedback->question_id->getId()], + 'pass' => [\ilDBConstants::T_INTEGER, $manual_feedback->attempt], + 'feedback' => [\ilDBConstants::T_TEXT, $manual_feedback->feedback], + 'finalized_evaluation' => [\ilDBConstants::T_INTEGER, $manual_feedback->finalized_evaluation === true ? 1 : 0], + 'finalized_timestamp' => [\ilDBConstants::T_INTEGER, $manual_feedback->finalized_timestamp], + 'finalized_by_usr_id' => [\ilDBConstants::T_INTEGER, $manual_feedback->finalized_by->getId()], + 'tstamp' => [\ilDBConstants::T_INTEGER, time()], ] ); $this->log->debug("Stored manual feedback in database: {$next_id}"); } } - public function importManualScoring(array $normalized, Transformations $tt): void + private function importManualScoring(array $normalized, Transformations $tt): void { // The mapping processor replaces ActiveID, QuestionID and UserID $active_id = $tt->denormalize($normalized['active_id'], Id::class)->getId(); @@ -248,17 +245,17 @@ public function importAdditionalWorkingTimes(array $list, Transformations $tt): $this->database->insert( 'tst_addtime', [ - 'additionaltime' => [ilDBConstants::T_INTEGER, $time->time], - 'user_fi' => [ilDBConstants::T_INTEGER, $time->user_id->getId()], - 'test_fi' => [ilDBConstants::T_INTEGER, $time->test_id->getId()], - 'tstamp' => [ilDBConstants::T_TIMESTAMP, $time->timestamp], + 'additionaltime' => [\ilDBConstants::T_INTEGER, $time->time], + 'user_fi' => [\ilDBConstants::T_INTEGER, $time->user_id->getId()], + 'test_fi' => [\ilDBConstants::T_INTEGER, $time->test_id->getId()], + 'tstamp' => [\ilDBConstants::T_TIMESTAMP, $time->timestamp], ] ); $this->log->debug("Stored additional working time in database: {$time->user_id->getId()} (User ID)"); } } - public function importRandomTestQuestions(array $list, Transformations $tt): void + private function importRandomTestQuestions(array $list, Transformations $tt): void { foreach ($list as $normalized) { // The mapping processor replaces ActiveID, QuestionID and SourcePoolDefinitionID @@ -268,13 +265,13 @@ public function importRandomTestQuestions(array $list, Transformations $tt): voi $this->database->insert( 'tst_test_rnd_qst', [ - 'test_random_question_id' => [ilDBConstants::T_INTEGER, $next_id], - 'active_fi' => [ilDBConstants::T_INTEGER, $question->active_id->getId()], - 'question_fi' => [ilDBConstants::T_INTEGER, $question->question_id->getId()], - 'sequence' => [ilDBConstants::T_INTEGER, $question->sequence], - 'pass' => [ilDBConstants::T_INTEGER, $question->pass], - 'tstamp' => [ilDBConstants::T_INTEGER, time()], - 'src_pool_def_fi' => [ilDBConstants::T_INTEGER, $question->src_pool_def_id->getId()], + 'test_random_question_id' => [\ilDBConstants::T_INTEGER, $next_id], + 'active_fi' => [\ilDBConstants::T_INTEGER, $question->active_id->getId()], + 'question_fi' => [\ilDBConstants::T_INTEGER, $question->question_id->getId()], + 'sequence' => [\ilDBConstants::T_INTEGER, $question->sequence], + 'pass' => [\ilDBConstants::T_INTEGER, $question->pass], + 'tstamp' => [\ilDBConstants::T_INTEGER, time()], + 'src_pool_def_fi' => [\ilDBConstants::T_INTEGER, $question->src_pool_def_id->getId()], ] ); $this->log->debug("Stored random test question in database: {$next_id}"); diff --git a/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/AdditionalWorkingTime.php b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/AdditionalWorkingTime.php index e7d49f4b3b64..33f59c7326cc 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/AdditionalWorkingTime.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/AdditionalWorkingTime.php @@ -34,10 +34,8 @@ public function __construct( ) { } - /** - * @inheritDoc - */ - public function toArray(Transformations $tt): array + #[\Override] + public function unpack(Transformations $tt): array { return [ 'user_id' => $tt->normalize($this->user_id), @@ -47,10 +45,8 @@ public function toArray(Transformations $tt): array ]; } - /** - * @inheritDoc - */ - public static function fromArray(array $value, Transformations $tt): static + #[\Override] + public static function pack(array $value, Transformations $tt): static { return new self( $tt->denormalize($value['user_id'], Id::class), diff --git a/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/Attempt.php b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/Attempt.php index 536a00bb55e6..dbe07b57516e 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/Attempt.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/Attempt.php @@ -41,10 +41,8 @@ public function __construct( ) { } - /** - * @inheritDoc - */ - public function toArray(Transformations $tt): array + #[\Override] + public function unpack(Transformations $tt): array { return [ 'active_id' => $tt->normalize($this->active_id), @@ -59,10 +57,8 @@ public function toArray(Transformations $tt): array ]; } - /** - * @inheritDoc - */ - public static function fromArray(array $value, Transformations $tt): static + #[\Override] + public static function pack(array $value, Transformations $tt): static { return new self( $tt->denormalize($value['active_id'], Id::class), diff --git a/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/ManualFeedback.php b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/ManualFeedback.php index 572b28e83319..a0e6292a2e7c 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/ManualFeedback.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/ManualFeedback.php @@ -37,10 +37,8 @@ public function __construct( ) { } - /** - * @inheritDoc - */ - public function toArray(Transformations $tt): array + #[\Override] + public function unpack(Transformations $tt): array { return [ 'active_id' => $tt->normalize($this->active_id), @@ -53,10 +51,8 @@ public function toArray(Transformations $tt): array ]; } - /** - * @inheritDoc - */ - public static function fromArray(array $value, Transformations $tt): static + #[\Override] + public static function pack(array $value, Transformations $tt): static { return new self( $tt->denormalize($value['active_id'], Id::class), diff --git a/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/QuestionResult.php b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/QuestionResult.php index 43ceaa4cb74a..bceec6f6cd46 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/QuestionResult.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/QuestionResult.php @@ -38,10 +38,8 @@ public function __construct( ) { } - /** - * @inheritDoc - */ - public function toArray(Transformations $tt): array + #[\Override] + public function unpack(Transformations $tt): array { return [ 'active_id' => $tt->normalize($this->active_id), @@ -55,10 +53,8 @@ public function toArray(Transformations $tt): array ]; } - /** - * @inheritDoc - */ - public static function fromArray(array $value, Transformations $tt): static + #[\Override] + public static function pack(array $value, Transformations $tt): static { return new self( $tt->denormalize($value['active_id'], Id::class), diff --git a/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/QuestionSetConfig.php b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/QuestionSetConfig.php index 43dc8be80ef8..4be75fa345e3 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/QuestionSetConfig.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/QuestionSetConfig.php @@ -20,33 +20,30 @@ namespace ILIAS\Test\ExportImport\Normalize\Envelopes; -use ilTestQuestionSetConfig; -use ilTestRandomQuestionSetConfig; -use ilTestRandomQuestionSetSourcePoolDefinition; class QuestionSetConfig { public function __construct( - private readonly ilTestQuestionSetConfig $config, - /** @var list */ + private readonly \ilTestQuestionSetConfig $config, + /** @var list<\ilTestRandomQuestionSetSourcePoolDefinition> */ private array $definitions = [], /** @var array> */ private array $staging_pools = [], ) { } - public function getConfig(): ilTestQuestionSetConfig + public function getConfig(): \ilTestQuestionSetConfig { return $this->config; } public function isRandom(): bool { - return $this->config instanceof ilTestRandomQuestionSetConfig; + return $this->config instanceof \ilTestRandomQuestionSetConfig; } /** - * @return list + * @return list<\ilTestRandomQuestionSetSourcePoolDefinition> */ public function getDefinitions(): array { @@ -54,7 +51,7 @@ public function getDefinitions(): array } /** - * @param list $definitions + * @param list<\ilTestRandomQuestionSetSourcePoolDefinition> $definitions */ public function setDefinitions(array $definitions): void { diff --git a/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/RandomTestQuestion.php b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/RandomTestQuestion.php index 50cc9f2ccbae..08f816574784 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/RandomTestQuestion.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/RandomTestQuestion.php @@ -36,10 +36,8 @@ public function __construct( ) { } - /** - * @inheritDoc - */ - public function toArray(Transformations $tt): array + #[\Override] + public function unpack(Transformations $tt): array { return [ 'active_id' => $tt->normalize($this->active_id), @@ -51,10 +49,8 @@ public function toArray(Transformations $tt): array ]; } - /** - * @inheritDoc - */ - public static function fromArray(array $value, Transformations $tt): static + #[\Override] + public static function pack(array $value, Transformations $tt): static { return new self( $tt->denormalize($value['active_id'], Id::class), diff --git a/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/WorkingTime.php b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/WorkingTime.php index b2d6464502fb..11dbd0e27537 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/WorkingTime.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Envelopes/WorkingTime.php @@ -34,10 +34,8 @@ public function __construct( ) { } - /** - * @inheritDoc - */ - public function toArray(Transformations $tt): array + #[\Override] + public function unpack(Transformations $tt): array { return [ 'active_id' => $tt->normalize($this->active_id), @@ -47,10 +45,8 @@ public function toArray(Transformations $tt): array ]; } - /** - * @inheritDoc - */ - public static function fromArray(array $value, Transformations $tt): static + #[\Override] + public static function pack(array $value, Transformations $tt): static { return new self( $tt->denormalize($value['active_id'], Id::class), diff --git a/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/AttemptResultNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/AttemptResultNormalizer.php index a9a0d54b0d66..1d1599113647 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/AttemptResultNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/AttemptResultNormalizer.php @@ -34,12 +34,10 @@ class AttemptResultNormalizer implements Normalizer public function __construct(private readonly Transformations $transformations) { } - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { - if (!$value instanceof AttemptResult) { + if (!($value instanceof AttemptResult)) { throw new NormalizingException('Invalid value', $value); } @@ -57,9 +55,7 @@ public function normalize($value): array|float|bool|int|string|null ]; } - /** - * @inheritDoc - */ + #[\Override] public function denormalize(array|float|bool|int|string|null $value, string $type): AttemptResult { if ($type !== AttemptResult::class) { diff --git a/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ExportableNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ExportableNormalizer.php index 73e2fd01fa9e..f94f76aed8db 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ExportableNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ExportableNormalizer.php @@ -29,21 +29,17 @@ */ class ExportableNormalizer implements Normalizer { - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { - if (!$value instanceof Exportable) { + if (!($value instanceof Exportable)) { throw new NormalizingException('Invalid exportable value', $value); } return $value->toExport(); } - /** - * @inheritDoc - */ + #[\Override] public function denormalize(array|float|bool|int|string|null $value, string $type): Exportable { if (!in_array(Exportable::class, class_implements($type))) { diff --git a/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ParticipantNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ParticipantNormalizer.php index b727e378f26b..d7ea0e8b1a7f 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ParticipantNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ParticipantNormalizer.php @@ -20,7 +20,6 @@ namespace ILIAS\Test\ExportImport\Normalize\Normalizer; -use DateTimeImmutable; use ILIAS\Test\Participants\Participant; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; @@ -37,12 +36,10 @@ public function __construct( ) { } - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { - if (!$value instanceof Participant) { + if (!($value instanceof Participant)) { throw new NormalizingException('Invalid value', $value); } @@ -73,9 +70,7 @@ public function normalize($value): array|float|bool|int|string|null ]; } - /** - * @inheritDoc - */ + #[\Override] public function denormalize(array|float|bool|int|string|null $value, string $type): Participant { if ($type !== Participant::class) { @@ -103,8 +98,8 @@ public function denormalize(array|float|bool|int|string|null $value, string $typ $this->tt->nullableInt($value['last_started_attempt']), $this->tt->nullableInt($value['last_finished_attempt']), $this->tt->bool($value['unfinished_attempts']), - $this->tt->denormalize($value['first_access'], DateTimeImmutable::class), - $this->tt->denormalize($value['last_access'], DateTimeImmutable::class), + $this->tt->denormalize($value['first_access'], \DateTimeImmutable::class), + $this->tt->denormalize($value['last_access'], \DateTimeImmutable::class), $this->tt->bool($value['scoring_finalized']), ); } diff --git a/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ParticipantResultNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ParticipantResultNormalizer.php index 6a2267483e3e..f1b675a6a018 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ParticipantResultNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ParticipantResultNormalizer.php @@ -37,12 +37,10 @@ public function __construct( ) { } - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { - if (!$value instanceof ParticipantResult) { + if (!($value instanceof ParticipantResult)) { throw new NormalizingException('Invalid value', $value); } @@ -57,9 +55,7 @@ public function normalize($value): array|float|bool|int|string|null ]; } - /** - * @inheritDoc - */ + #[\Override] public function denormalize(array|float|bool|int|string|null $value, string $type): ParticipantResult { if ($type !== ParticipantResult::class) { diff --git a/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/QuestionSetConfigNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/QuestionSetConfigNormalizer.php index 71018f4f32aa..e2f67f3ee6c0 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/QuestionSetConfigNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/QuestionSetConfigNormalizer.php @@ -20,8 +20,6 @@ namespace ILIAS\Test\ExportImport\Normalize\Normalizer; -use ilComponentRepository; -use ilDBInterface; use ILIAS\Test\ExportImport\Normalize\Envelopes\QuestionSetConfig; use ILIAS\Test\Logging\TestLogger; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; @@ -29,14 +27,6 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; -use ilLanguage; -use ilObjTest; -use ilTestException; -use ilTestFixedQuestionSetConfig; -use ilTestRandomQuestionSetConfig; -use ilTestQuestionSetConfig; -use ilTestRandomQuestionSetSourcePoolDefinition; -use ilTree; /** * @implements Normalizer @@ -45,21 +35,19 @@ class QuestionSetConfigNormalizer implements Normalizer { public function __construct( private readonly Transformations $tt, - private readonly ilDBInterface $db, - private readonly ilLanguage $lng, - private readonly ilTree $tree, - private readonly ilComponentRepository $component_repository, + private readonly \ilDBInterface $db, + private readonly \ilLanguage $lng, + private readonly \ilTree $tree, + private readonly \ilComponentRepository $component_repository, private readonly TestLogger $logger, private readonly GeneralQuestionPropertiesRepository $repository, ) { } - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { - if (!$value instanceof QuestionSetConfig) { + if (!($value instanceof QuestionSetConfig)) { throw new NormalizingException('Invalid value', $value); } @@ -76,17 +64,17 @@ public function normalize($value): array|float|bool|int|string|null return $normalized; } - private function normalizeQuestionSetConfig(ilTestQuestionSetConfig $config): array + private function normalizeQuestionSetConfig(\ilTestQuestionSetConfig $config): array { - if ($config instanceof ilTestFixedQuestionSetConfig) { + if ($config instanceof \ilTestFixedQuestionSetConfig) { return [ - 'type' => ilObjTest::QUESTION_SET_TYPE_FIXED, + 'type' => \ilObjTest::QUESTION_SET_TYPE_FIXED, ]; } - if ($config instanceof ilTestRandomQuestionSetConfig) { + if ($config instanceof \ilTestRandomQuestionSetConfig) { return [ - 'type' => ilObjTest::QUESTION_SET_TYPE_RANDOM, + 'type' => \ilObjTest::QUESTION_SET_TYPE_RANDOM, 'homogeneous' => $config->arePoolsWithHomogeneousScoredQuestionsRequired(), 'amount_mode' => $config->getQuestionAmountConfigurationMode(), 'amount' => $config->getQuestionAmountPerTest(), @@ -114,7 +102,7 @@ private function normalizeStagingPools(array $staging_pools): array return $normalized; } - private function normalizeTestObj(ilTestQuestionSetConfig $config): array + private function normalizeTestObj(\ilTestQuestionSetConfig $config): array { $test_obj = $config->getTestObject(); @@ -124,9 +112,7 @@ private function normalizeTestObj(ilTestQuestionSetConfig $config): array ]; } - /** - * @inheritDoc - */ + #[\Override] public function denormalize(array|float|bool|int|string|null $value, string $type): mixed { if ($type !== QuestionSetConfig::class) { @@ -136,7 +122,7 @@ public function denormalize(array|float|bool|int|string|null $value, string $typ $test_obj = $this->denormalizeTestObj($value['test_obj']); $config = $this->denormalizeQuestionSetConfig($value['config'], $test_obj); - if (!$config instanceof ilTestRandomQuestionSetConfig) { + if (!($config instanceof \ilTestRandomQuestionSetConfig)) { return new QuestionSetConfig($config); } @@ -149,10 +135,10 @@ public function denormalize(array|float|bool|int|string|null $value, string $typ return new QuestionSetConfig($config, $definitions, $staging_pools); } - private function denormalizeQuestionSetConfig(array $normalized, ilObjTest $test_obj): ilTestQuestionSetConfig + private function denormalizeQuestionSetConfig(array $normalized, \ilObjTest $test_obj): \ilTestQuestionSetConfig { - if($normalized['type'] === ilObjTest::QUESTION_SET_TYPE_FIXED) { - return new ilTestFixedQuestionSetConfig( + if($normalized['type'] === \ilObjTest::QUESTION_SET_TYPE_FIXED) { + return new \ilTestFixedQuestionSetConfig( $this->tree, $this->db, $this->lng, @@ -163,7 +149,7 @@ private function denormalizeQuestionSetConfig(array $normalized, ilObjTest $test ); } - $config = new ilTestRandomQuestionSetConfig( + $config = new \ilTestRandomQuestionSetConfig( $this->tree, $this->db, $this->lng, @@ -174,8 +160,8 @@ private function denormalizeQuestionSetConfig(array $normalized, ilObjTest $test ); $amount_mode = $this->tt->string($normalized['amount_mode']); - if (!$config->isValidQuestionAmountConfigurationMode($amount_mode)) { - throw new ilTestException("Invalid random test question set config amount mode given: {$amount_mode}"); + if ($config->isValidQuestionAmountConfigurationMode($amount_mode) === false) { + throw new \ilTestException("Invalid random test question set config amount mode given: {$amount_mode}"); } $config->setQuestionAmountConfigurationMode($amount_mode); @@ -186,9 +172,9 @@ private function denormalizeQuestionSetConfig(array $normalized, ilObjTest $test return $config; } - private function denormalizeSourcePoolDefinition(array $normalized, ilObjTest $test_obj): ilTestRandomQuestionSetSourcePoolDefinition + private function denormalizeSourcePoolDefinition(array $normalized, \ilObjTest $test_obj): \ilTestRandomQuestionSetSourcePoolDefinition { - $definition = new ilTestRandomQuestionSetSourcePoolDefinition( + $definition = new \ilTestRandomQuestionSetSourcePoolDefinition( $this->db, $test_obj ); @@ -210,9 +196,9 @@ private function denormalizeStagingPools(array $normalized): array return $staging_pools; } - private function denormalizeTestObj(array $normalized): ilObjTest + private function denormalizeTestObj(array $normalized): \ilObjTest { - $test_obj = new ilObjTest(0, false); + $test_obj = new \ilObjTest(0, false); $test_obj->setTestId($this->tt->denormalize($normalized['test_id'], Id::class)->getId()); $test_obj->setId($this->tt->denormalize($normalized['obj_id'], Id::class)->getId()); diff --git a/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilObjTestNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilObjTestNormalizer.php index e91ada1388be..03d9080774d6 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilObjTestNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilObjTestNormalizer.php @@ -21,22 +21,19 @@ namespace ILIAS\Test\ExportImport\Normalize\Normalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; -use ilObjTest; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer\IlObjectNormalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** - * @implements Normalizer + * @implements Normalizer<\ilObjTest, array> */ class ilObjTestNormalizer extends IlObjectNormalizer implements Normalizer { - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { - if (!$value instanceof ilObjTest) { + if (!($value instanceof \ilObjTest)) { throw new NormalizingException('Invalid value', $value); } @@ -46,17 +43,15 @@ public function normalize($value): array|float|bool|int|string|null return $normalized; } - /** - * @inheritDoc - */ - public function denormalize(array|float|bool|int|string|null $value, string $type): ilObjTest + #[\Override] + public function denormalize(array|float|bool|int|string|null $value, string $type): \ilObjTest { - if ($type !== ilObjTest::class) { + if ($type !== \ilObjTest::class) { throw new NormalizingException("Invalid type for ilObjTest: {$type}"); } - /** @var ilObjTest $object */ - $object = parent::denormalize($value, ilObjTest::class); + /** @var \ilObjTest $object */ + $object = parent::denormalize($value, \ilObjTest::class); $object->setTestId( $this->tt->denormalize($value['test_id'], Id::class)->getId() ); diff --git a/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilTestSequenceNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilTestSequenceNormalizer.php index f9e7036cebf4..dc72b80b0e5d 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilTestSequenceNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilTestSequenceNormalizer.php @@ -20,32 +20,28 @@ namespace ILIAS\Test\ExportImport\Normalize\Normalizer; -use ilDBInterface; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; -use ilTestSequence; /** - * @implements Normalizer + * @implements Normalizer<\ilTestSequence, array> */ class ilTestSequenceNormalizer implements Normalizer { public function __construct( private readonly Transformations $tt, - private readonly ilDBInterface $db, + private readonly \ilDBInterface $db, private readonly GeneralQuestionPropertiesRepository $repository, ) { } - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { - if (!$value instanceof ilTestSequence) { + if (!($value instanceof \ilTestSequence)) { throw new NormalizingException('Invalid value', $value); } @@ -68,19 +64,17 @@ private function normalizeQuestions(array $questions): array ); } - /** - * @inheritDoc - */ - public function denormalize(array|float|bool|int|string|null $value, string $type): ilTestSequence + #[\Override] + public function denormalize(array|float|bool|int|string|null $value, string $type): \ilTestSequence { - if ($type !== ilTestSequence::class) { + if ($type !== \ilTestSequence::class) { throw new NormalizingException("Invalid type for ilTestSequence: {$type}"); } $active_id = $this->tt->denormalize($value['active_id'], Id::class)->getId(); $attempt = $this->tt->int($value['attempt']); - $sequence = new ilTestSequence($this->db, $active_id, $attempt, $this->repository); + $sequence = new \ilTestSequence($this->db, $active_id, $attempt, $this->repository); $sequence->setAnsweringOptionalQuestionsConfirmed($this->tt->bool($value['ans_opt_confirmed'])); $sequence->sequencedata['sequence'] = $this->denormalizeSequence($value['sequence']); diff --git a/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilTestSkillLevelThresholdNormalizer.php b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilTestSkillLevelThresholdNormalizer.php index 3254dd6e1688..1f574d317028 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilTestSkillLevelThresholdNormalizer.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Normalizer/ilTestSkillLevelThresholdNormalizer.php @@ -20,30 +20,26 @@ namespace ILIAS\TestQuestionPool\ExportImport\Normalize\Normalizer; -use ilTestSkillLevelThreshold; -use ilDBInterface; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** - * @implements Normalizer + * @implements Normalizer<\ilTestSkillLevelThreshold, array> */ class ilTestSkillLevelThresholdNormalizer implements Normalizer { public function __construct( private readonly Transformations $tt, - private readonly ilDBInterface $db, + private readonly \ilDBInterface $db, ) { } - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { - if (!$value instanceof ilTestSkillLevelThreshold) { + if (!($value instanceof \ilTestSkillLevelThreshold)) { throw new NormalizingException('Invalid value', $value); } @@ -56,16 +52,14 @@ public function normalize($value): array|float|bool|int|string|null ]; } - /** - * @inheritDoc - */ - public function denormalize(array|float|bool|int|string|null $value, string $type): ilTestSkillLevelThreshold + #[\Override] + public function denormalize(array|float|bool|int|string|null $value, string $type): \ilTestSkillLevelThreshold { - if ($type !== ilTestSkillLevelThreshold::class) { + if ($type !== \ilTestSkillLevelThreshold::class) { throw new NormalizingException("Invalid type for ilTestSkillLevelThreshold: {$type}"); } - $threshold = new ilTestSkillLevelThreshold($this->db); + $threshold = new \ilTestSkillLevelThreshold($this->db); $threshold->setSkillLevelId($this->tt->denormalize($value['id'], Id::class)->getId()); $threshold->setTestId($this->tt->denormalize($value['test_id'], Id::class)->getId()); $threshold->setSkillBaseId($this->tt->denormalize($value['skill_base_id'], Id::class)->getId()); diff --git a/components/ILIAS/Test/src/ExportImport/Normalize/Processors/CollectUserIds.php b/components/ILIAS/Test/src/ExportImport/Normalize/Processors/CollectUserIds.php index ce0972f394f0..ebaf141045e6 100644 --- a/components/ILIAS/Test/src/ExportImport/Normalize/Processors/CollectUserIds.php +++ b/components/ILIAS/Test/src/ExportImport/Normalize/Processors/CollectUserIds.php @@ -35,9 +35,7 @@ class CollectUserIds implements Processor */ private array $ids = []; - /** - * @inheritDoc - */ + #[\Override] public function process(object $carry): void { if ( diff --git a/components/ILIAS/Test/src/ExportImport/TestCollector.php b/components/ILIAS/Test/src/ExportImport/TestCollector.php index 912267f2141e..6de54744003f 100644 --- a/components/ILIAS/Test/src/ExportImport/TestCollector.php +++ b/components/ILIAS/Test/src/ExportImport/TestCollector.php @@ -20,10 +20,6 @@ namespace ILIAS\Test\ExportImport; -use Generator; -use ilComponentRepository; -use ilDBConstants; -use ilDBInterface; use ILIAS\Data\ObjectId; use ILIAS\Language\Language; use ILIAS\Test\ExportImport\Normalize\Envelopes\AdditionalWorkingTime; @@ -43,15 +39,6 @@ use ILIAS\TestQuestionPool\ExportImport\Export\CollectsQuestions; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; -use ilObjTest; -use ilTestQuestionSetConfigFactory; -use ilTestRandomQuestionSetSourcePoolDefinitionFactory; -use ilTestRandomQuestionSetSourcePoolDefinitionList; -use ilTestRandomQuestionSetStagingPoolQuestionList; -use ilTestSequence; -use ilTestSkillLevelThreshold; -use ilTestSkillLevelThresholdList; -use ilTree; /** * Collector to aggregate data from the test object for export. @@ -64,7 +51,7 @@ class TestCollector /** @var array $questions */ private ?array $questions = null; - private ?ilObjTest $test = null; + private ?\ilObjTest $test = null; private ?array $participants = null; @@ -73,17 +60,17 @@ public function __construct( private readonly ResultsRepository $results_repository, private readonly QuestionsRepository $questions_repository, private readonly GeneralQuestionPropertiesRepository $general_questions_repository, - private readonly ilDBInterface $db, - private readonly ilTree $tree, + private readonly \ilDBInterface $db, + private readonly \ilTree $tree, private readonly Language $lng, private readonly TestLogger $logger, - private readonly ilComponentRepository $component_repository, + private readonly \ilComponentRepository $component_repository, private readonly ObjectId $object_id ) { $this->manual_scoring = new TestManScoringDoneHelper($this->db); } - private function database(): ilDBInterface + private function database(): \ilDBInterface { return $this->db; } @@ -98,9 +85,9 @@ public function getTestId(): int return $this->getObject()->getTestId(); } - public function getObject(): ilObjTest + public function getObject(): \ilObjTest { - return $this->test ??= new ilObjTest($this->object_id->toInt(), false); + return $this->test ??= new \ilObjTest($this->object_id->toInt(), false); } public function getSettings(): array @@ -126,7 +113,7 @@ public function getUserMapping(array $user_ids): array if ($export_identifier === UserIdentifiers::USER_ID) { $mapping = array_combine($user_ids, $user_ids); } else { - $in_clause = $this->db->in('usr_id', $user_ids, false, ilDBConstants::T_INTEGER); + $in_clause = $this->db->in('usr_id', $user_ids, false, \ilDBConstants::T_INTEGER); $query = $this->db->query("SELECT usr_id, {$export_identifier->value} FROM usr_data WHERE {$in_clause}"); foreach ($this->db->fetchAll($query) as $row) { @@ -144,9 +131,6 @@ public function getUserMapping(array $user_ids): array Questions */ - /** - * @inheritDoc - */ public function getQuestionProperties(): array { return array_map( @@ -164,11 +148,11 @@ public function getTestQuestionProperties(): array } /** - * @return list + * @return list<\ilTestSkillLevelThreshold> */ public function getSkillLevelThresholds(): array { - $threshold_list = new ilTestSkillLevelThresholdList($this->database()); + $threshold_list = new \ilTestSkillLevelThresholdList($this->database()); $threshold_list->setTestId($this->getTestId()); $threshold_list->loadFromDb(); @@ -189,7 +173,7 @@ public function getSkillLevelThresholds(): array */ public function getQuestionSetConfig(): QuestionSetConfig { - $factory = new ilTestQuestionSetConfigFactory( + $factory = new \ilTestQuestionSetConfigFactory( $this->tree, $this->db, $this->lng, @@ -200,16 +184,16 @@ public function getQuestionSetConfig(): QuestionSetConfig ); $config = new QuestionSetConfig($factory->getQuestionSetConfig()); - if (!$config->isRandom()) { + if ($config->isRandom() === false) { return $config; } - $definition_factory = new ilTestRandomQuestionSetSourcePoolDefinitionFactory( + $definition_factory = new \ilTestRandomQuestionSetSourcePoolDefinitionFactory( $this->db, $this->getObject() ); - $definition_list = new ilTestRandomQuestionSetSourcePoolDefinitionList( + $definition_list = new \ilTestRandomQuestionSetSourcePoolDefinitionList( $this->db, $this->getObject(), $definition_factory @@ -230,7 +214,7 @@ public function getQuestionSetConfig(): QuestionSetConfig */ private function getStagingPoolQuestions(int $pool_id): array { - $question_list = new ilTestRandomQuestionSetStagingPoolQuestionList( + $question_list = new \ilTestRandomQuestionSetStagingPoolQuestionList( $this->db, $this->component_repository ); @@ -246,9 +230,9 @@ private function getStagingPoolQuestions(int $pool_id): array */ /** - * @return Generator + * @return \Generator */ - public function getParticipants(): Generator + public function getParticipants(): \Generator { return $this->participant_repository->getParticipants($this->getTestId()); } @@ -280,7 +264,7 @@ public function getParticipantsIds(): array */ public function getAdditionalParticipantData(array $participant_ids): array { - $in_clause = $this->db->in('active_id', $participant_ids, false, ilDBConstants::T_INTEGER); + $in_clause = $this->db->in('active_id', $participant_ids, false, \ilDBConstants::T_INTEGER); $query = $this->db->query("SELECT active_id AS mapping_id, submittimestamp, lastindex, objective_container, start_lock FROM tst_active WHERE {$in_clause}"); $data = []; @@ -321,11 +305,11 @@ public function getResults(int $participant_id): array /** * @return list */ - public function getQuestionAttempts(int $participant_id): array + private function getQuestionAttempts(int $participant_id): array { $query = $this->db->queryF( "SELECT * FROM tst_solutions WHERE active_fi = %s", - [ilDBConstants::T_INTEGER], + [\ilDBConstants::T_INTEGER], [$participant_id] ); @@ -342,7 +326,7 @@ public function getQuestionResults(int $participant_id): array { $query = $this->db->queryF( "SELECT * FROM tst_test_result WHERE active_fi = %s", - [ilDBConstants::T_INTEGER], + [\ilDBConstants::T_INTEGER], [$participant_id] ); @@ -355,11 +339,11 @@ public function getQuestionResults(int $participant_id): array /** * @return list */ - public function getWorkingTimes(int $participant_id): array + private function getWorkingTimes(int $participant_id): array { $query = $this->db->queryF( "SELECT * FROM tst_times WHERE active_fi = %s", - [ilDBConstants::T_INTEGER], + [\ilDBConstants::T_INTEGER], [$participant_id] ); @@ -376,7 +360,7 @@ public function getManualFeedback(int $participant_id): array { $query = $this->db->queryF( "SELECT * FROM tst_manual_fb WHERE active_fi = %s", - [ilDBConstants::T_INTEGER], + [\ilDBConstants::T_INTEGER], [$participant_id] ); @@ -388,12 +372,12 @@ public function getManualFeedback(int $participant_id): array /** * @param list $attempts - * @return list + * @return list<\ilTestSequence> */ - public function getSequences(int $participant_id, array $attempts): array + private function getSequences(int $participant_id, array $attempts): array { foreach ($attempts as $attempt) { - $test_sequence = new ilTestSequence($this->db, $participant_id, $attempt, $this->general_questions_repository); + $test_sequence = new \ilTestSequence($this->db, $participant_id, $attempt, $this->general_questions_repository); $test_sequence->loadFromDb(); $sequences[] = $test_sequence; } @@ -409,7 +393,7 @@ public function getAdditionalWorkingTimes(): array { $query = $this->db->queryF( "SELECT * FROM tst_addtime WHERE test_fi = %s", - [ilDBConstants::T_INTEGER], + [\ilDBConstants::T_INTEGER], [$this->getTestId()] ); @@ -422,11 +406,11 @@ public function getAdditionalWorkingTimes(): array /** * @return list */ - public function getRandomTestQuestions(int $participant_id): array + private function getRandomTestQuestions(int $participant_id): array { $query = $this->db->queryF( "SELECT * FROM tst_test_rnd_qst WHERE active_fi = %s", - [ilDBConstants::T_INTEGER], + [\ilDBConstants::T_INTEGER], [$participant_id] ); diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index 071a555cf58d..ae9c6ed49bd5 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -20,9 +20,6 @@ namespace ILIAS\Test\ExportImport; -use assFormulaQuestion; -use ilComponentRepository; -use ilDBInterface; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\ObjectId; use ILIAS\Data\UUID\Factory as UUIDFactory; @@ -44,18 +41,17 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\XmlSerializer; use TestQuestionPool\ExportImport\Normalize\Processors\CollectQuestionImages; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; -use ilTree; class TestExporter implements Exporter { public function __construct( private readonly TransformationsBuilder $builder, private readonly DataFactory $data_factory, - private readonly ilDBInterface $db, - private readonly ilTree $tree, + private readonly \ilDBInterface $db, + private readonly \ilTree $tree, private readonly Language $lng, private readonly TestLogger $logger, - private readonly ilComponentRepository $component_repository, + private readonly \ilComponentRepository $component_repository, private readonly IRSS $irss, private readonly ParticipantRepository $participant_repository, private readonly ResultsRepository $results_repository, @@ -65,9 +61,6 @@ public function __construct( ) { } - /** - * @inheritDoc - */ public function prepare(ExportState $state): void { $state->logger()->info('Preparing test export (1/3)...'); @@ -137,9 +130,6 @@ private function collector(ExportState $state): TestCollector return $collector; } - /** - * @inheritDoc - */ public function process(ExportState $state): void { $state->logger()->info('Processing test export (2/3)...'); @@ -235,9 +225,6 @@ private function processResults(ExportState $state): void ); } - /** - * @inheritDoc - */ public function write(ExportState $state): void { $state->logger()->info('Writing test export (3/3)...'); @@ -323,10 +310,12 @@ private function exportSettings( $serializer->append('scoring', $transformations->normalize($test->getScoreSettings())); $serializer->append('marks', $transformations->normalize($test->getMarkSchema())); - if ($intro_page_id = $main_settings->getIntroductionSettings()->getIntroductionPageId()) { + $intro_page_id = $main_settings->getIntroductionSettings()->getIntroductionPageId(); + if ($intro_page_id !== null && $intro_page_id !== 0) { $state->addDependency('components/ILIAS/COPage', 'pg', ["tst:{$intro_page_id}"]); } - if ($concluding_page_id = $main_settings->getFinishingSettings()->getConcludingRemarksPageId()) { + $concluding_page_id = $main_settings->getFinishingSettings()->getConcludingRemarksPageId(); + if ($concluding_page_id !== null && $concluding_page_id !== 0) { $state->addDependency('components/ILIAS/COPage', 'pg', ["tst:{$concluding_page_id}"]); } } @@ -348,7 +337,7 @@ private function exportQuestions( 'sequence' => $question_properties[$question->getId()]->getSequenceInformation()?->getPlaceInSequence(), ]; - if ($question instanceof assFormulaQuestion) { + if ($question instanceof \assFormulaQuestion) { $data = $collector->getUnitsAndCategories($question->getId()); $normalized['formula_data'] = $transformations->normalize($data); } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryState.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryState.php index 0c25c73792ca..89e2cf3495c3 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryState.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryState.php @@ -183,9 +183,7 @@ public function setUnchecked(): void $this->checked = false; } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -197,9 +195,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php index 6105a734c084..1f61d09d4a2f 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerBinaryStateImage.php @@ -80,9 +80,7 @@ public function hasImage(): bool return $this->image !== null; } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -96,9 +94,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerCloze.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerCloze.php index 8e6f39e76660..6f79f373c262 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerCloze.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerCloze.php @@ -153,9 +153,7 @@ public function getGapSize(): int return $this->gap_size; } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -169,9 +167,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerErrorText.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerErrorText.php index e338bcb3ffac..7a2acb8f3ea8 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerErrorText.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerErrorText.php @@ -99,9 +99,7 @@ public function getLength(): int return $this->length; } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -115,9 +113,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerImagemap.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerImagemap.php index 3294f8f0385f..a0b548d1a0f1 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerImagemap.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerImagemap.php @@ -145,9 +145,7 @@ public function setPointsUnchecked($points_unchecked): void } } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -161,9 +159,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatching.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatching.php index 45b7e3c70d1f..b1909cd5bbdf 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatching.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatching.php @@ -241,9 +241,7 @@ public function setPoints(float $points = 0.0): void $this->points = $points; } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -257,9 +255,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingPair.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingPair.php index 0d9042831f93..214170dcdf69 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingPair.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingPair.php @@ -77,9 +77,7 @@ public function withPoints(float $points): self return $clone; } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -92,9 +90,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php index e9cfb1e3f7ae..cf0daa2072a6 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMatchingTerm.php @@ -82,9 +82,7 @@ public function withIdentifier(int $identifier): self return $clone; } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -99,9 +97,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponse.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponse.php index 979c30c60652..0ebdba2a48e7 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponse.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponse.php @@ -100,9 +100,7 @@ public function getPointsChecked(): float return $this->getPoints(); } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -114,9 +112,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php index 4999f7aa9f2f..0ad319ff3147 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php @@ -78,9 +78,7 @@ public function hasImage(): bool return $this->image !== null; } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -94,9 +92,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php index 120d5402ff6b..55611a44010e 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerSimple.php @@ -217,9 +217,7 @@ public function setPoints($points = 0.0): void } } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -233,9 +231,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assAnswerTrueFalse.php b/components/ILIAS/TestQuestionPool/classes/class.assAnswerTrueFalse.php index 4fa4a758ecac..5539b04da677 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assAnswerTrueFalse.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assAnswerTrueFalse.php @@ -150,9 +150,7 @@ public function setFalse(): void $this->correctness = "0"; } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -164,9 +162,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php b/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php index 5380625dc0f1..33c78f129c13 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assClozeGap.php @@ -457,9 +457,7 @@ public function numericRangeExists(): bool return false; } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -473,9 +471,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php b/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php index 548525a07856..71d21ef35e55 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php @@ -1771,9 +1771,7 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra return $answers; } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -1792,9 +1790,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php b/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php index ec3c40d1c9ba..ccdd669c9354 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php @@ -1004,9 +1004,7 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): stri return $this->createErrorTextExport($this->getBestSelection()); } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -1021,9 +1019,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php b/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php index ce27319f68fa..5ada3cf53b4b 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php @@ -954,9 +954,7 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): stri return ''; } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -970,9 +968,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php index 9ac4fce42a38..01a601537ad7 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php @@ -1490,9 +1490,7 @@ function (array $c, array $v): array { ); } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -1505,9 +1503,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php index 2097eeaf9e68..5367f1fec155 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionResult.php @@ -866,9 +866,7 @@ public function getAvailableResultUnits($question_id): array return $this->available_units; } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -894,9 +892,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php index f0158abea8b2..1fe121022463 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnit.php @@ -165,9 +165,7 @@ private function sanitizeString(string $string): string return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'utf-8'); } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -184,9 +182,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php index 2ba80deb41aa..bc58fa2f99eb 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionUnitCategory.php @@ -92,9 +92,7 @@ private function sanitizeString(string $string): string return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'utf-8'); } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -107,9 +105,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionVariable.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionVariable.php index 0216c4e229f9..8322037bd12b 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionVariable.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionVariable.php @@ -202,9 +202,7 @@ public function getRangeMinTxt(): string return $this->range_min_txt; } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -222,9 +220,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php index f449fc96151e..54ef538e5027 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php @@ -932,9 +932,7 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra ); } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -948,9 +946,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php index 7b657b47004e..29a9dac0dd0b 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php @@ -924,9 +924,7 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra ); } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -945,9 +943,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php b/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php index ccf0dc5a191c..f9fa182c29e0 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php @@ -837,9 +837,7 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra return $correct_answers; } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -856,9 +854,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php index a1990c5a7b45..6d336d348c6d 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php @@ -1427,9 +1427,7 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra ); } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -1445,9 +1443,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php index e9f99dbede2e..11e00d070d52 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php @@ -960,9 +960,7 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra ); } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -976,9 +974,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php b/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php index ffe2dbdd6d95..82588b65e218 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php @@ -467,9 +467,7 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): stri return "{$this->getLowerLimit()}-{$this->getUpperLimit()}"; } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -483,9 +481,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php b/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php index b8e14c9c3c60..e35a0ef8e7cc 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php @@ -586,9 +586,7 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): stri return $this->getOrderText(); } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -603,9 +601,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php index 4e4ef3c158e0..d01a9d786935 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php @@ -1370,9 +1370,7 @@ function (ilAssOrderingElement $v): string { ); } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -1388,9 +1386,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php index 6f65316d8ddf..540e57410634 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php @@ -948,9 +948,7 @@ function (array $c, ASS_AnswerBinaryStateImage $v): array { ); } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -963,9 +961,7 @@ public function toNormalized( return $normalized; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php index fc6f97d89684..f07009e857b1 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php @@ -859,9 +859,7 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra } } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -878,9 +876,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php b/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php index 17c5a7e1ed88..e7fdeeecea4a 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php @@ -759,9 +759,7 @@ public function getCorrectSolutionForTextOutput(int $active_id, int $pass): arra return $this->getAvailableAnswers(); } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -775,9 +773,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php index 837de62508c8..737386041afa 100755 --- a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php +++ b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php @@ -447,9 +447,7 @@ public function withContent(string $content): self return $clone; } - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -467,9 +465,7 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/CollectsQuestions.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/CollectsQuestions.php index 6ec6c63b155e..3d5d16a1eb6a 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/CollectsQuestions.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/CollectsQuestions.php @@ -20,20 +20,10 @@ namespace ILIAS\TestQuestionPool\ExportImport\Export; -use assFormulaQuestionUnit; -use assFormulaQuestionUnitCategory; -use assQuestion; -use Generator; -use ilAssClozeTestFeedback; -use ilAssMultiOptionQuestionFeedback; -use ilAssQuestionSkillAssignmentList; -use ilAssSpecificFeedbackIdentifierList; -use ilDBInterface; use ILIAS\Data\ObjectId; use ILIAS\TestQuestionPool\ExportImport\Normalize\Envelopes\Feedback; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; use ILIAS\TestQuestionPool\Questions\GeneralQuestionProperties; -use ilUnitConfigurationRepository; /** * Trait to collect questions and related data from a question pool or test object. @@ -55,20 +45,20 @@ abstract public function getObjectId(): ObjectId; /** * Get the database interface. */ - abstract private function database(): ilDBInterface; + abstract private function database(): \ilDBInterface; - private ?ilAssQuestionSkillAssignmentList $skill_assignments = null; + private ?\ilAssQuestionSkillAssignmentList $skill_assignments = null; /** * Collect the question objects by instantiating the question objects. * - * @return Generator + * @return \Generator<\assQuestion> */ - public function getQuestionObjects(): Generator + public function getQuestionObjects(): \Generator { foreach ($this->getQuestionProperties() as $question) { - yield assQuestion::instantiateQuestion($question->getQuestionId()); + yield \assQuestion::instantiateQuestion($question->getQuestionId()); } } @@ -79,11 +69,11 @@ public function getQuestionObjects(): Generator /** * Get all unit categories and units for a formula question. * - * @return array{categories: list, base_units: list, units: list} + * @return array{categories: list<\assFormulaQuestionUnitCategory>, base_units: list<\assFormulaQuestionUnit>, units: list<\assFormulaQuestionUnit>} */ public function getUnitsAndCategories(int $question_id): array { - $repository = new ilUnitConfigurationRepository($question_id); + $repository = new \ilUnitConfigurationRepository($question_id); $data = [ 'categories' => [], 'base_units' => [], @@ -91,11 +81,11 @@ public function getUnitsAndCategories(int $question_id): array ]; foreach ($repository->getCategorizedUnits() as $item) { - if ($item instanceof assFormulaQuestionUnitCategory) { + if ($item instanceof \assFormulaQuestionUnitCategory) { $data['categories'][] = $item; } - if (!$item instanceof assFormulaQuestionUnit) { + if (!($item instanceof \assFormulaQuestionUnit)) { continue; } @@ -116,7 +106,7 @@ public function getUnitsAndCategories(int $question_id): array /** * Collect the feedback content for a question and return it as a Feedback transfer object. */ - public function getFeedback(assQuestion $question): Feedback + public function getFeedback(\assQuestion $question): Feedback { return new Feedback( new Id($question->getId(), 'question'), @@ -126,11 +116,11 @@ public function getFeedback(assQuestion $question): Feedback ); } - private function loadSpecificFeedback(assQuestion $question): array + private function loadSpecificFeedback(\assQuestion $question): array { // Skip if specific feedback is not available or supported by the question type. if ( - !$question->feedbackOBJ instanceof ilAssMultiOptionQuestionFeedback || + !($question->feedbackOBJ instanceof \ilAssMultiOptionQuestionFeedback) || !$question->feedbackOBJ->isSpecificAnswerFeedbackAvailable($question->getId()) ) { return []; @@ -139,8 +129,8 @@ private function loadSpecificFeedback(assQuestion $question): array $feedback = []; // Cloze question type specific feedback uses the identifier list to load the answer-specific feedback. - if ($question->feedbackOBJ instanceof ilAssClozeTestFeedback) { - $feedback_list = new ilAssSpecificFeedbackIdentifierList(); + if ($question->feedbackOBJ instanceof \ilAssClozeTestFeedback) { + $feedback_list = new \ilAssSpecificFeedbackIdentifierList(); $feedback_list->load($question->getId()); foreach ($feedback_list as $identifier) { @@ -183,7 +173,7 @@ private function loadSpecificFeedback(assQuestion $question): array public function getSkillAssignments(): array { if ($this->skill_assignments === null) { - $this->skill_assignments = new ilAssQuestionSkillAssignmentList($this->database()); + $this->skill_assignments = new \ilAssQuestionSkillAssignmentList($this->database()); $this->skill_assignments->setParentObjId($this->getObjectId()->toInt()); $this->skill_assignments->loadFromDb(); $this->skill_assignments->loadAdditionalSkillData(); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolCollector.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolCollector.php index 00125011c7d6..5ac77acc357b 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolCollector.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolCollector.php @@ -20,11 +20,9 @@ namespace ILIAS\TestQuestionPool\ExportImport\Export; -use ilDBInterface; use ILIAS\Data\ObjectId; use ILIAS\TestQuestionPool\Questions\GeneralQuestionProperties; use ILIAS\TestQuestionPool\Questions\GeneralQuestionPropertiesRepository; -use ilObjQuestionPool; /** * Collector to aggregate data from the question pool for export. @@ -35,11 +33,11 @@ class QuestionPoolCollector /** @var array $questions */ private ?array $questions = null; - private ?ilObjQuestionPool $pool_object = null; + private ?\ilObjQuestionPool $pool_object = null; public function __construct( private readonly GeneralQuestionPropertiesRepository $question_repository, - private readonly ilDBInterface $db, + private readonly \ilDBInterface $db, private readonly ObjectId $pool_id ) { } @@ -49,6 +47,7 @@ public function __construct( * * @return ObjectId */ + #[\Override] public function getObjectId(): ObjectId { return $this->pool_id; @@ -57,10 +56,10 @@ public function getObjectId(): ObjectId /** * Get the object of the question pool. It will be loaded from the database if not already loaded. */ - public function getObject(): ilObjQuestionPool + public function getObject(): \ilObjQuestionPool { if ($this->pool_object === null) { - $this->pool_object = new ilObjQuestionPool($this->pool_id->toInt(), false); + $this->pool_object = new \ilObjQuestionPool($this->pool_id->toInt(), false); $this->pool_object->read(); } @@ -77,7 +76,7 @@ public function getQuestionProperties(): array return $this->questions ??= $this->question_repository->getForParentObjectId($this->pool_id->toInt()); } - private function database(): ilDBInterface + private function database(): \ilDBInterface { return $this->db; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php index 762058ffbcd9..afa3be86773c 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php @@ -20,8 +20,6 @@ namespace ILIAS\TestQuestionPool\ExportImport\Export; -use assFormulaQuestion; -use ilDBInterface; use ILIAS\Data\Factory as DataFactory; use ILIAS\Data\ObjectId; use ILIAS\Data\UUID\Factory as UUIDFactory; @@ -44,7 +42,7 @@ public function __construct( private readonly TransformationsBuilder $builder, private readonly DataFactory $data_factory, private readonly GeneralQuestionPropertiesRepository $question_repository, - private readonly ilDBInterface $db, + private readonly \ilDBInterface $db, private readonly Taxonomy $taxonomy ) { } @@ -207,7 +205,7 @@ private function exportQuestions( 'feedback' => $transformations->normalize($collector->getFeedback($question)) ]; - if ($question instanceof assFormulaQuestion) { + if ($question instanceof \assFormulaQuestion) { $normalized['formula_data'] = $transformations->normalize( $collector->getUnitsAndCategories($question->getId()) ); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportState.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportState.php index bed1f3ad061a..8b61d05612ba 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportState.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportState.php @@ -27,7 +27,6 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\Serializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use Psr\Log\LoggerInterface as Logger; -use RuntimeException; class ExportState { @@ -79,7 +78,7 @@ public function setStep(ExportStep $step): void public function assertStep(ExportStep $step): void { if ($this->step->value < $step->value) { - throw new RuntimeException("Expected step {$step->name}, but got {$this->step->name} instead"); + throw new \RuntimeException("Expected step {$step->name}, but got {$this->step->name} instead"); } $this->step = $step; @@ -184,7 +183,7 @@ public function getContent(): string private function assertNotNull(mixed $value, string $property): void { if ($value === null) { - throw new RuntimeException( + throw new \RuntimeException( "{$property} not set. This may happen if the exporter steps are not executed in the correct order." ); } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/StateHolder.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/StateHolder.php index ad0417a2be2e..282662a24bd1 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/StateHolder.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/StateHolder.php @@ -22,7 +22,6 @@ use ILIAS\Export\ExportHandler\I\Target\HandlerInterface as ExportTarget; use ILIAS\Export\ExportHandler\I\Consumer\ExportConfig\CollectionInterface as ExportConfig; -use RuntimeException; /** * State management for the export process. It is used to pass the export target, config and tools between the ILIAS @@ -49,7 +48,7 @@ public function exists(): bool public function get(): ExportState { if ($this->export_state === null) { - throw new RuntimeException('Export state not found. You need to create the state first.'); + throw new \RuntimeException('Export state not found. You need to create the state first.'); } return $this->export_state; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Envelope.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Envelope.php index ae38c053f7d1..9a4c6a34734e 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Envelope.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Envelope.php @@ -33,7 +33,7 @@ interface Envelope * @param Transformations $tt Transformation helpers used for value casting and mapping. * @return array Language-neutral normalized representation. */ - public function toArray(Transformations $tt): array; + public function unpack(Transformations $tt): array; /** * Reconstruct an envelope instance from its normalized array representation. @@ -42,5 +42,5 @@ public function toArray(Transformations $tt): array; * @param Transformations $tt Transformation helpers used for value casting and mapping. * @return static Reconstructed envelope instance. */ - public static function fromArray(array $value, Transformations $tt): static; + public static function pack(array $value, Transformations $tt): static; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Envelopes/Id.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Envelopes/Id.php index 0a95b4f92bdd..299cd5ad856f 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Envelopes/Id.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Envelopes/Id.php @@ -54,10 +54,8 @@ public function getObject(): string return $this->object; } - /** - * @inheritDoc - */ - public function toArray(Transformations $tt): array + #[\Override] + public function unpack(Transformations $tt): array { if (is_object($this->id)) { return [ @@ -74,10 +72,8 @@ public function toArray(Transformations $tt): array ]; } - /** - * @inheritDoc - */ - public static function fromArray(array $value, Transformations $tt): static + #[\Override] + public static function pack(array $value, Transformations $tt): static { $raw_id = $value['id']; $type = $value['type']; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/FoundationNormalizerRegistration.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/FoundationNormalizerRegistration.php index ed40963b6102..6b1156015d37 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/FoundationNormalizerRegistration.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/FoundationNormalizerRegistration.php @@ -34,7 +34,6 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer\ResourceNormalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer\TransformationNormalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer\UUIDNormalizer; -use InitResourceStorage; final class FoundationNormalizerRegistration { @@ -54,10 +53,10 @@ public function register(Registry $registry, Transformations $transformations): $registry->register(Envelope::class, new EnvelopeNormalizer($transformations)); $registry->register(Transformation::class, new TransformationNormalizer($this->dic->refinery())); - if (isset($this->dic[InitResourceStorage::D_REPOSITORIES])) { + if (isset($this->dic[\InitResourceStorage::D_REPOSITORIES])) { $resource = new ResourceNormalizer( $transformations, - $this->dic[InitResourceStorage::D_REPOSITORIES]->getResourceRepository() + $this->dic[\InitResourceStorage::D_REPOSITORIES]->getResourceRepository() ); $registry->register(ResourceIdentification::class, $resource); $registry->register(StorableResource::class, $resource); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/DateTimeNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/DateTimeNormalizer.php index f7364a629f2e..0b89cad23119 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/DateTimeNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/DateTimeNormalizer.php @@ -20,36 +20,30 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; -use DateTime; -use DateTimeImmutable; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** - * @implements Normalizer + * @implements Normalizer<\DateTime|\DateTimeImmutable, string> */ class DateTimeNormalizer implements Normalizer { - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { - if ($value instanceof DateTimeImmutable || $value instanceof DateTime) { + if ($value instanceof \DateTimeImmutable || $value instanceof \DateTime) { return $value->format(DATE_ATOM); } throw new NormalizingException('Invalid datetime value', $value); } - /** - * @inheritDoc - */ - public function denormalize(array|float|bool|int|string|null $value, string $type): DateTime|DateTimeImmutable + #[\Override] + public function denormalize(array|float|bool|int|string|null $value, string $type): \DateTime|\DateTimeImmutable { return match ($type) { - DateTimeImmutable::class => DateTimeImmutable::createFromFormat(DATE_ATOM, $value), - DateTime::class => DateTime::createFromFormat(DATE_ATOM, $value), + \DateTimeImmutable::class => \DateTimeImmutable::createFromFormat(DATE_ATOM, $value), + \DateTime::class => \DateTime::createFromFormat(DATE_ATOM, $value), default => throw new NormalizingException("Invalid type for datetime: {$type}") }; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/EnvelopeNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/EnvelopeNormalizer.php index a82d9c166d58..16f3c20cdb3e 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/EnvelopeNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/EnvelopeNormalizer.php @@ -35,27 +35,23 @@ public function __construct( ) { } - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { - if (!$value instanceof Envelope) { + if (!($value instanceof Envelope)) { throw new NormalizingException('Invalid envelope value', $value); } - return $value->toArray($this->tt); + return $value->unpack($this->tt); } - /** - * @inheritDoc - */ + #[\Override] public function denormalize(array|float|bool|int|string|null $value, string $type): Envelope { if (!in_array(Envelope::class, class_implements($type))) { throw new NormalizingException('Invalid envelope type', $type); } - return $type::fromArray($value, $this->tt); + return $type::pack($value, $this->tt); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/IlObjectNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/IlObjectNormalizer.php index 8c7900d4a9b0..027bd93de0e2 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/IlObjectNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/IlObjectNormalizer.php @@ -33,11 +33,9 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; -use ilObject; -use ilObjectFactory; /** - * @implements Normalizer + * @implements Normalizer<\ilObject, array> */ class IlObjectNormalizer implements Normalizer { @@ -46,12 +44,10 @@ public function __construct( ) { } - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { - if (!$value instanceof ilObject) { + if (!($value instanceof \ilObject)) { throw new NormalizingException('Invalid value', $value); } @@ -121,18 +117,16 @@ private function normalizeTranslations(Translations $translations): array ]; } - /** - * @inheritDoc - */ - public function denormalize(array|float|bool|int|string|null $value, string $type): ilObject + #[\Override] + public function denormalize(array|float|bool|int|string|null $value, string $type): \ilObject { - if ($type !== ilObject::class && !in_array(ilObject::class, class_parents($type))) { + if ($type !== \ilObject::class && !in_array(\ilObject::class, class_parents($type))) { throw new NormalizingException("Invalid type for ilObject: {$type}"); } // Validate the class of the object by its type field $object_type = $this->tt->string($value['type']); - $object_class = ilObjectFactory::getClassByType($object_type); + $object_class = \ilObjectFactory::getClassByType($object_type); if ($object_class !== $type) { throw new NormalizingException("Expected {$type}, got object of type {$object_type} ({$object_class})"); } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/Legacy11UUIDNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/Legacy11UUIDNormalizer.php index f3a06a197794..ab5f548c8023 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/Legacy11UUIDNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/Legacy11UUIDNormalizer.php @@ -35,17 +35,13 @@ */ class Legacy11UUIDNormalizer extends UUIDNormalizer { - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { throw new NormalizingException('Normalizing of legacy data is not supported'); } - /** - * @inheritDoc - */ + #[\Override] public function denormalize(array|float|bool|int|string|null $value, string $type): Uuid { return parent::denormalize(str_replace('_', '-', (string) $value), $type); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/ResourceNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/ResourceNormalizer.php index f57976c1c62f..6251c8fc44d5 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/ResourceNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/ResourceNormalizer.php @@ -43,9 +43,7 @@ public function __construct( ) { } - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { if ($value instanceof ResourceIdentification) { @@ -81,9 +79,7 @@ private function normalizeResource(StorableResource $resource): array ]; } - /** - * @inheritDoc - */ + #[\Override] public function denormalize(array|float|bool|int|string|null $value, string $type): ResourceIdentification|StorableResource { if ($type === ResourceIdentification::class) { @@ -129,7 +125,7 @@ public static function isResourceIdentification(mixed $value): bool /** * Returns true if the value is a normalized storable resource. */ - public static function isStorableResource(mixed $value): bool + private static function isStorableResource(mixed $value): bool { return ($value[self::KEY_TYPE] ?? false) === self::TYPE_RESOURCE; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/TransformationNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/TransformationNormalizer.php index 42b36d53c377..5ee713845cb2 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/TransformationNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/TransformationNormalizer.php @@ -33,9 +33,7 @@ class TransformationNormalizer implements Normalizer public function __construct(private readonly Refinery $refinery) { } - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { if ($value instanceof Transformation) { @@ -45,9 +43,7 @@ public function normalize($value): array|float|bool|int|string|null throw new NormalizingException('Invalid transformation value', $value); } - /** - * @inheritDoc - */ + #[\Override] public function denormalize(array|float|bool|int|string|null $value, string $type): Transformation { return $this->refinery->custom()->transformation(static fn() => $value); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/UUIDNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/UUIDNormalizer.php index 03b2bd3780a7..0dee319572de 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/UUIDNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/UUIDNormalizer.php @@ -37,9 +37,7 @@ public function __construct() $this->factory = new Factory(); } - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { if ($value instanceof Uuid) { @@ -49,9 +47,7 @@ public function normalize($value): array|float|bool|int|string|null throw new NormalizingException('Invalid UUID value', $value); } - /** - * @inheritDoc - */ + #[\Override] public function denormalize(array|float|bool|int|string|null $value, string $type): Uuid { return $this->factory->fromString((string) $value); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/NormalizingException.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/NormalizingException.php index 88a37f434e40..49188b529eb8 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/NormalizingException.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/NormalizingException.php @@ -20,12 +20,11 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize; -use InvalidArgumentException; /** * Exception thrown when a value cannot be normalized or denormalized. */ -class NormalizingException extends InvalidArgumentException +class NormalizingException extends \InvalidArgumentException { public function __construct(string $message, mixed $value = null) { diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Processors/CollectResources.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Processors/CollectResources.php index 00133ee926b3..abb8bd407b89 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Processors/CollectResources.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Processors/CollectResources.php @@ -69,9 +69,6 @@ public function storeMapping(string $old_id, ResourceIdentification $new_id): vo $this->import_mapping[$old_id] = $new_id; } - /** - * @inheritDoc - */ public function process(object $carry): void { if ($carry instanceof NormalizeCarry && $carry->value() instanceof ResourceIdentification) { diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Transformations.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Transformations.php index 31d4228a71ef..1f4ae0dfa799 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Transformations.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Transformations.php @@ -2,7 +2,6 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize; -use Generator; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\DenormalizeCarry; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\NormalizeCarry; use ILIAS\TestQuestionPool\ExportImport\Foundation\Queue\Processor; @@ -33,7 +32,7 @@ public function __construct( */ public function normalize(mixed $value, array $context = []): array|float|bool|int|string|null { - if (is_object($value) && $value instanceof Generator) { + if (is_object($value) && $value instanceof \Generator) { $value = iterator_to_array($value); } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php index 667120076452..4436a35cdad2 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php @@ -26,11 +26,10 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\StageResult; use ILIAS\TestQuestionPool\RequestDataCollector; -use ilImport; /** * Final stage of the question pool import process. Imports the question pool object and all its questions and other - * dependencies using `ilImport`. It will delegate the import to the `ilTestQuestionPoolImporter` class. + * dependencies using `\ilImport`. It will delegate the import to the `ilTestQuestionPoolImporter` class. */ class PersistStage implements ImportStage { @@ -58,7 +57,7 @@ public function getDescription(): ?string public function process(ImportContext $context): StageResult { - $importer = new ilImport($this->request_data_collector->getRefId()); + $importer = new \ilImport($this->request_data_collector->getRefId()); $importer->importObject( null, $context->fileToImport(), diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php index c1ae4366d8e1..224cc9852b3f 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php @@ -29,8 +29,6 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\IdMappingProcessor; use ILIAS\TestQuestionPool\ExportImport\TransformationsBuilder; use TestQuestionPool\ExportImport\Normalize\Processors\CollectQuestionImages; -use ilImportMapping; -use ilObjQuestionPool; use Psr\Log\LoggerInterface; /** @@ -53,7 +51,7 @@ public function __construct( */ public function import( Deserializer $deserializer, - ilImportMapping $mapping, + \ilImportMapping $mapping, ReferenceId $parent_id, ImportContext $context ): ImportContext { @@ -125,20 +123,20 @@ function (array $assignments) use ($transformations, $mapping, &$context): void * Finalize the import after all dependencies have been imported. * It will replace the old question ids with the new question ids in the question pages. */ - public function finalize(ilImportMapping $mapping): void + public function finalize(\ilImportMapping $mapping): void { $this->log->info('Finalizing question pool import...'); $this->questions_importer->finalizeQuestionPages($mapping); $this->log->info('...Finished finalizing question pool'); } - protected function importQuestionPool( + private function importQuestionPool( array $normalized, Transformations $transformations, - ilImportMapping $mapping, + \ilImportMapping $mapping, ReferenceId $parent_id ): int { - $pool_object = $transformations->denormalize($normalized, ilObjQuestionPool::class); + $pool_object = $transformations->denormalize($normalized, \ilObjQuestionPool::class); $old_pool_id = $pool_object->getId(); $pool_object->setTitle($pool_object->getTitle()); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php index 6d961e8760bf..c7702dca3b4a 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php @@ -20,7 +20,6 @@ namespace ILIAS\TestQuestionPool\ExportImport\Import; -use ilComponentFactory; use ILIAS\Language\Language; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportStage; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; @@ -54,7 +53,7 @@ class QuestionSelectionStage implements ImportStage public function __construct( private readonly Language $lng, private readonly LoggerInterface $log, - private readonly ilComponentFactory $component_factory, + private readonly \ilComponentFactory $component_factory, private readonly UIFactory $ui_factory, private readonly ServerRequestInterface $request, private readonly string $form_action, diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php index e37b2d7ab560..2e290c3b7d12 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php @@ -20,13 +20,6 @@ namespace ILIAS\TestQuestionPool\ExportImport\Import; -use assFormulaQuestion; -use assFormulaQuestionUnit; -use assFormulaQuestionUnitCategory; -use assQuestion; -use ilAssQuestionPage; -use ilCtrl; -use ilDBInterface; use ILIAS\Filesystem\Filesystem; use ILIAS\Filesystem\Filesystems; use ILIAS\Filesystem\Stream\FileStream; @@ -40,8 +33,6 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Normalize\Envelopes\Feedback; use TestQuestionPool\ExportImport\Normalize\Processors\CollectQuestionImages; -use ilImportMapping; -use ilUnitConfigurationRepository; use Psr\Log\LoggerInterface; class QuestionsImporter @@ -51,8 +42,8 @@ class QuestionsImporter public function __construct( private readonly string $component, private readonly string $parent_type, - private readonly ilCtrl $ctrl, - private readonly ilDBInterface $database, + private readonly \ilCtrl $ctrl, + private readonly \ilDBInterface $database, private readonly Language $language, private readonly LoggerInterface $log, private readonly Images $image_converter, @@ -64,15 +55,15 @@ public function __construct( public function importQuestion( array $normalized, Transformations $transformations, - ilImportMapping $mapping, + \ilImportMapping $mapping, array $selected_questions - ): ?assQuestion { + ): ?\assQuestion { $question_class = $normalized['type']; if (!class_exists($question_class)) { throw new \InvalidArgumentException("Question class {$question_class} does not exist"); } - /** @var assQuestion $question */ + /** @var \assQuestion $question */ $question = $transformations->denormalize($normalized, new $question_class()); $old_question_id = $question->getId(); if (!in_array($old_question_id, $selected_questions)) { @@ -89,7 +80,7 @@ public function importQuestion( $this->log->debug("Created new question: {$old_question_id} -> {$new_question_id}"); $this->storeQuestionMappings($mapping, $old_question_id, $new_question_id, $question->getObjId()); - if ($question instanceof assFormulaQuestion) { + if ($question instanceof \assFormulaQuestion) { $this->importFormulaQuestion($normalized, $question, $transformations, $mapping, ); } @@ -106,7 +97,7 @@ public function importQuestion( public function importQuestionImages( int $parent_obj_id, - ilImportMapping $mapping, + \ilImportMapping $mapping, ImportContext $context, CollectQuestionImages $collector, ): void { @@ -131,7 +122,7 @@ public function importQuestionImages( $this->log->debug("Imported question image: {$source_path} -> {$image_path}"); $thumbnail = $this->generateThumbnail($input_stream); - if (!$thumbnail instanceof FileStream) { + if (!($thumbnail instanceof FileStream)) { continue; } @@ -144,10 +135,10 @@ public function importQuestionImages( } } - private function buildImageBasePath(int $parent_obj_id, QuestionImage $envelope, ilImportMapping $mapping): ?string + private function buildImageBasePath(int $parent_obj_id, QuestionImage $envelope, \ilImportMapping $mapping): ?string { $question_id = $mapping->getMapping($this->component, 'question', (string) $envelope->getQuestionId()); - if (!$question_id) { + if ($question_id === null || $question_id === '') { $this->log->error("Question ID mapping not found for {$envelope->getQuestionId()}"); return null; } @@ -176,7 +167,7 @@ private function generateThumbnail(FileStream $image_stream): ?FileStream /** * Finalize the imported question pages by replacing the old question ids with the new question ids. */ - public function finalizeQuestionPages(ilImportMapping $mapping): void + public function finalizeQuestionPages(\ilImportMapping $mapping): void { $page_mappings = $mapping->getMappingsOfEntity('components/ILIAS/COPage', 'pg'); @@ -192,7 +183,7 @@ public function finalizeQuestionPages(ilImportMapping $mapping): void $new_question_id = $new_matches[1]; $this->log->debug("Finalizing question page: {$old_question_id} -> {$new_question_id}"); - $page = new ilAssQuestionPage((int) $new_question_id); + $page = new \ilAssQuestionPage((int) $new_question_id); $xml = preg_replace( '/il_\d+_qst_' . preg_quote($old_question_id, '/') . '\b/', "il__qst_{$new_question_id}", @@ -219,7 +210,7 @@ public function finalizeQuestionPages(ilImportMapping $mapping): void } private function storeQuestionMappings( - ilImportMapping $mapping, + \ilImportMapping $mapping, int $old_question_id, int $new_question_id, int $parent_obj_id, @@ -256,7 +247,7 @@ private function storeQuestionMappings( ); } - private function importFeedback(Feedback $feedback, assQuestion $question): void + private function importFeedback(Feedback $feedback, \assQuestion $question): void { $question_id = $question->getId(); $question->feedbackOBJ->importGenericFeedback($question_id, false, $feedback->getGenericUncompleted()); @@ -276,16 +267,16 @@ private function importFeedback(Feedback $feedback, assQuestion $question): void private function importFormulaQuestion( array $normalized, - assFormulaQuestion $question, + \assFormulaQuestion $question, Transformations $transformations, - ilImportMapping $mapping, + \ilImportMapping $mapping, ): void { $formula = $normalized['formula_data']; - $repository = new ilUnitConfigurationRepository($question->getId()); + $repository = new \ilUnitConfigurationRepository($question->getId()); // First, import the unit categories which are referenced by the units foreach ($formula['categories'] as $normalized_category) { - $category = $transformations->denormalize($normalized_category, new assFormulaQuestionUnitCategory()); + $category = $transformations->denormalize($normalized_category, new \assFormulaQuestionUnitCategory()); $old_category_id = $category->getId(); $repository->saveNewUnitCategory($category); @@ -299,7 +290,7 @@ private function importFormulaQuestion( foreach ($normalized_units as $normalized_unit) { $old_unit_id = $transformations->denormalize($normalized_unit['id'], Id::class)->getId(); - $unit = new assFormulaQuestionUnit(); + $unit = new \assFormulaQuestionUnit(); $repository->createNewUnit($unit); $mapping->addMapping($this->component, 'unit', (string) $old_unit_id, (string) $unit->getId()); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php index 59109c4c8130..4f120d324c20 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php @@ -20,11 +20,8 @@ namespace ILIAS\TestQuestionPool\ExportImport\Import; -use ilAssQuestionSkillAssignment; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\Skill\Service\SkillUsageService; -use ilImportMapping; -use ilSkillTreeRepository; use Psr\Log\LoggerInterface; /** @@ -37,7 +34,7 @@ class SkillAssignmentsImporter { public function __construct( private readonly LoggerInterface $log, - private readonly ilSkillTreeRepository $skill_repo, + private readonly \ilSkillTreeRepository $skill_repo, private readonly SkillUsageService $skill_usage_service, private readonly string $component, private readonly int $local_install_id @@ -55,13 +52,13 @@ public function import( array $normalized_assignments, int $import_install_id, Transformations $transformations, - ilImportMapping $mapping, + \ilImportMapping $mapping, ): array { $result = ['failed' => [], 'success' => []]; foreach ($normalized_assignments as $item) { // The mapping processor replaces ParentObjID and QuestionID. - $assignment = $transformations->denormalize($item, ilAssQuestionSkillAssignment::class); + $assignment = $transformations->denormalize($item, \ilAssQuestionSkillAssignment::class); $skill_data = $this->getSkillIdMapping( $assignment->getSkillBaseId(), @@ -112,7 +109,7 @@ public function import( return $result; } - protected function getSkillIdMapping(int $skill_base_id, int $skill_tref_id, int $import_install_id): ?array + private function getSkillIdMapping(int $skill_base_id, int $skill_tref_id, int $import_install_id): ?array { if ($import_install_id === $this->local_install_id) { return [ @@ -138,7 +135,7 @@ protected function getSkillIdMapping(int $skill_base_id, int $skill_tref_id, int /** * @return ImportResultData */ - protected function buildResultData(ilAssQuestionSkillAssignment $assignment): array + private function buildResultData(\ilAssQuestionSkillAssignment $assignment): array { return [ 'skill_id' => $assignment->getSkillBaseId(), diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php index 1e3090b55b27..5b5d8413595c 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php @@ -27,7 +27,6 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportStage; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\StageResult; -use ilManifestParser; use Psr\Log\LoggerInterface; /** @@ -81,7 +80,7 @@ public function process(ImportContext $context): StageResult $unzip->extract(); $this->log->info("Extracted import file: {$context->fileToImport()} -> {$import_base_dir}"); - $manifest = new ilManifestParser($import_base_dir . DIRECTORY_SEPARATOR . 'manifest.xml'); + $manifest = new \ilManifestParser($import_base_dir . DIRECTORY_SEPARATOR . 'manifest.xml'); $export_file = array_find( $manifest->getExportFiles(), fn(array $file): bool => $file['component'] === $this->component diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Envelopes/Feedback.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Envelopes/Feedback.php index 70bf9d3f8a14..72d64025d5eb 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Envelopes/Feedback.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Envelopes/Feedback.php @@ -56,10 +56,8 @@ public function getSpecificFeedback(): array return $this->specific_feedback; } - /** - * @inheritDoc - */ - public function toArray(Transformations $tt): array + #[\Override] + public function unpack(Transformations $tt): array { return [ 'question_id' => $tt->normalize($this->question_id), @@ -69,10 +67,8 @@ public function toArray(Transformations $tt): array ]; } - /** - * @inheritDoc - */ - public static function fromArray(array $value, Transformations $tt): static + #[\Override] + public static function pack(array $value, Transformations $tt): static { return new self( $tt->denormalize($value['question_id'], Id::class), diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Envelopes/QuestionImage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Envelopes/QuestionImage.php index 4369618e0187..8efae0f1e76b 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Envelopes/QuestionImage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Envelopes/QuestionImage.php @@ -65,10 +65,8 @@ public function setId(string $id): void $this->id = $id; } - /** - * @inheritDoc - */ - public function toArray(Transformations $tt): array + #[\Override] + public function unpack(Transformations $tt): array { return [ 'filename' => $this->filename, @@ -78,10 +76,8 @@ public function toArray(Transformations $tt): array ]; } - /** - * @inheritDoc - */ - public static function fromArray(array $value, Transformations $tt): static + #[\Override] + public static function pack(array $value, Transformations $tt): static { return new self( $value['filename'], diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/SuggestedSolutionNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/SuggestedSolutionNormalizer.php index b04df5bc2c26..61750788c616 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/SuggestedSolutionNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/SuggestedSolutionNormalizer.php @@ -20,7 +20,6 @@ namespace ILIAS\TestQuestionPool\ExportImport\Normalize\Normalizer; -use DateTimeImmutable; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; @@ -40,12 +39,10 @@ public function __construct( ) { } - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { - if (!$value instanceof SuggestedSolution) { + if (!($value instanceof SuggestedSolution)) { throw new NormalizingException('Invalid value', $value); } @@ -77,9 +74,7 @@ public function normalize($value): array|float|bool|int|string|null return $normalized; } - /** - * @inheritDoc - */ + #[\Override] public function denormalize(array|float|bool|int|string|null $value, string $type): SuggestedSolution { if ($type !== SuggestedSolution::class && !in_array(SuggestedSolution::class, class_parents($type))) { @@ -103,7 +98,7 @@ public function denormalize(array|float|bool|int|string|null $value, string $typ $question_id = $this->tt->denormalize($value['question_id'], Id::class)->getId(); $subquestion_index = $this->tt->int($value['subquestion_index']); $import_id = $this->tt->string($value['import_id']); - $last_update = $this->tt->denormalize($value['last_update'], DateTimeImmutable::class); + $last_update = $this->tt->denormalize($value['last_update'], \DateTimeImmutable::class); switch ($type) { case SuggestedSolutionFile::class: diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php index e7fdd59e3092..c8a87fdc6889 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/ilAssQuestionSkillAssignmentNormalizer.php @@ -20,31 +20,26 @@ namespace ILIAS\TestQuestionPool\ExportImport\Normalize\Normalizer; -use ilAssQuestionSkillAssignment; -use ilAssQuestionSolutionComparisonExpression; -use ilDBInterface; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** - * @implements Normalizer + * @implements Normalizer<\ilAssQuestionSkillAssignment, array> */ class ilAssQuestionSkillAssignmentNormalizer implements Normalizer { public function __construct( private readonly Transformations $tt, - private readonly ilDBInterface $db + private readonly \ilDBInterface $db ) { } - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { - if (!$value instanceof ilAssQuestionSkillAssignment) { + if (!($value instanceof \ilAssQuestionSkillAssignment)) { throw new NormalizingException('Invalid value', $value); } @@ -59,18 +54,18 @@ public function normalize($value): array|float|bool|int|string|null ]; switch ($value->getEvalMode()) { - case ilAssQuestionSkillAssignment::EVAL_MODE_BY_QUESTION_RESULT: + case \ilAssQuestionSkillAssignment::EVAL_MODE_BY_QUESTION_RESULT: $normalized['points'] = $value->getSkillPoints(); break; - case ilAssQuestionSkillAssignment::EVAL_MODE_BY_QUESTION_SOLUTION: + case \ilAssQuestionSkillAssignment::EVAL_MODE_BY_QUESTION_SOLUTION: $normalized['solution_comparison_expressions'] = $this->normalizeExpressionList($value); break; } return $normalized; } - private function normalizeExpressionList(ilAssQuestionSkillAssignment $value): array + private function normalizeExpressionList(\ilAssQuestionSkillAssignment $value): array { $value->initSolutionComparisonExpressionList(); @@ -86,16 +81,14 @@ private function normalizeExpressionList(ilAssQuestionSkillAssignment $value): a return $list; } - /** - * @inheritDoc - */ - public function denormalize(array|float|bool|int|string|null $value, string $type): ilAssQuestionSkillAssignment + #[\Override] + public function denormalize(array|float|bool|int|string|null $value, string $type): \ilAssQuestionSkillAssignment { - if ($type !== ilAssQuestionSkillAssignment::class) { + if ($type !== \ilAssQuestionSkillAssignment::class) { throw new NormalizingException("Invalid type for ilAssQuestionSkillAssignment: {$type}"); } - $assignment = new ilAssQuestionSkillAssignment($this->db); + $assignment = new \ilAssQuestionSkillAssignment($this->db); $assignment->setParentObjId($this->tt->denormalize($value['parent_id'], Id::class)->getId()); $assignment->setQuestionId($this->tt->denormalize($value['question_id'], Id::class)->getId()); $assignment->setSkillBaseId($this->tt->denormalize($value['base_id'], Id::class)->getId()); @@ -106,11 +99,11 @@ public function denormalize(array|float|bool|int|string|null $value, string $typ $assignment->initSolutionComparisonExpressionList(); switch ($assignment->getEvalMode()) { - case ilAssQuestionSkillAssignment::EVAL_MODE_BY_QUESTION_RESULT: + case \ilAssQuestionSkillAssignment::EVAL_MODE_BY_QUESTION_RESULT: $assignment->setSkillPoints($this->tt->int($value['points'])); break; - case ilAssQuestionSkillAssignment::EVAL_MODE_BY_QUESTION_SOLUTION: + case \ilAssQuestionSkillAssignment::EVAL_MODE_BY_QUESTION_SOLUTION: $list = $assignment->getSolutionComparisonExpressionList(); foreach ($value['solution_comparison_expressions'] as $normalized) { $list->add($this->denormalizeExpression($normalized, $assignment)); @@ -123,9 +116,9 @@ public function denormalize(array|float|bool|int|string|null $value, string $typ private function denormalizeExpression( array $normalized, - ilAssQuestionSkillAssignment $assignment - ): ilAssQuestionSolutionComparisonExpression { - $expression = new ilAssQuestionSolutionComparisonExpression(); + \ilAssQuestionSkillAssignment $assignment + ): \ilAssQuestionSolutionComparisonExpression { + $expression = new \ilAssQuestionSolutionComparisonExpression(); $expression->setQuestionId($assignment->getQuestionId()); $expression->setSkillBaseId($assignment->getSkillBaseId()); $expression->setSkillTrefId($assignment->getSkillTrefId()); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/ilObjQuestionPoolNormalizer.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/ilObjQuestionPoolNormalizer.php index 67d046f98e30..afb5fa39b9ee 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/ilObjQuestionPoolNormalizer.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Normalizer/ilObjQuestionPoolNormalizer.php @@ -20,22 +20,19 @@ namespace ILIAS\TestQuestionPool\ExportImport\Normalize\Normalizer; -use ilObjQuestionPool; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer\IlObjectNormalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** - * @implements Normalizer + * @implements Normalizer<\ilObjQuestionPool, array> */ class ilObjQuestionPoolNormalizer extends IlObjectNormalizer implements Normalizer { - /** - * @inheritDoc - */ + #[\Override] public function normalize($value): array|float|bool|int|string|null { - if (!$value instanceof ilObjQuestionPool) { + if (!($value instanceof \ilObjQuestionPool)) { throw new NormalizingException('Invalid value', $value); } @@ -45,17 +42,15 @@ public function normalize($value): array|float|bool|int|string|null return $normalized; } - /** - * @inheritDoc - */ - public function denormalize(array|float|bool|int|string|null $value, string $type): ilObjQuestionPool + #[\Override] + public function denormalize(array|float|bool|int|string|null $value, string $type): \ilObjQuestionPool { - if ($type !== ilObjQuestionPool::class) { + if ($type !== \ilObjQuestionPool::class) { throw new NormalizingException("Invalid type for ilObjQuestionPool: {$type}"); } - /** @var ilObjQuestionPool $object */ - $object = parent::denormalize($value, ilObjQuestionPool::class); + /** @var \ilObjQuestionPool $object */ + $object = parent::denormalize($value, \ilObjQuestionPool::class); $object->setSkillServiceEnabled($this->tt->bool($value['skill_service_enabled'])); return $object; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Processors/CollectQuestionImages.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Processors/CollectQuestionImages.php index b41e622c3b23..d7674b2126c5 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Processors/CollectQuestionImages.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Normalize/Processors/CollectQuestionImages.php @@ -53,9 +53,7 @@ public function __construct( $this->question_files = new QuestionFiles(); } - /** - * @inheritDoc - */ + #[\Override] public function process(object $carry): void { if ($carry instanceof NormalizeCarry && $carry->value() instanceof QuestionImage) { @@ -97,7 +95,7 @@ private function handleDenormalization(DenormalizeCarry $passable): void if ($envelope === null) { return; } - if (!$envelope instanceof QuestionImage) { + if (!($envelope instanceof QuestionImage)) { throw new NormalizingException('Expected question image envelope, got ' . get_debug_type($envelope)); } diff --git a/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php b/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php index 41da880394a9..1359d14fe21e 100644 --- a/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php +++ b/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php @@ -61,6 +61,7 @@ protected function setUp(): void (new ReflectionClass(TestDIC::class))->getProperty('dic')->setValue(null, null); } + #[\Override] protected function tearDown(): void { $_SESSION = $this->session; diff --git a/components/ILIAS/TestQuestionPool/tests/ilAssQuestionLifecycleTest.php b/components/ILIAS/TestQuestionPool/tests/ilAssQuestionLifecycleTest.php index 6551b560389a..14a34bb76923 100644 --- a/components/ILIAS/TestQuestionPool/tests/ilAssQuestionLifecycleTest.php +++ b/components/ILIAS/TestQuestionPool/tests/ilAssQuestionLifecycleTest.php @@ -35,15 +35,31 @@ protected function setUp(): void { parent::setUp(); - $this->object = new class () extends ilAssQuestionLifecycle { - public function __construct() - { - } - }; + $this->object = new ilAssQuestionLifecycle(); } public function testConstruct(): void { $this->assertInstanceOf(ilAssQuestionLifecycle::class, $this->object); + $this->assertSame(ilAssQuestionLifecycle::DRAFT, $this->object->getIdentifier()); + } + + public function testConstructAcceptsIdentifier(): void + { + $lifecycle = new ilAssQuestionLifecycle(ilAssQuestionLifecycle::FINAL); + + $this->assertSame(ilAssQuestionLifecycle::FINAL, $lifecycle->getIdentifier()); + } + + public function testFactoryRemainsWrapper(): void + { + $this->assertSame( + ilAssQuestionLifecycle::REVIEW, + ilAssQuestionLifecycle::getInstance(ilAssQuestionLifecycle::REVIEW)->getIdentifier() + ); + $this->assertSame( + ilAssQuestionLifecycle::DRAFT, + ilAssQuestionLifecycle::getDraftInstance()->getIdentifier() + ); } } From 86d95fa5ab32cd546c19111a2b356d6d04f93d89 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Tue, 22 Sep 2026 12:01:42 +0200 Subject: [PATCH 58/66] refactor: replace ilAssQuestionLifecycle static methods with instance creation --- ...RandomQuestionSetPoolDefinitionFormGUI.php | 2 +- ...ss.ilTestQuestionFilterLabelTranslator.php | 2 +- .../Presentation/QuestionsBrowserFilter.php | 2 +- .../Presentation/QuestionsBrowserTable.php | 2 +- .../src/Questions/Properties/Properties.php | 2 +- .../classes/class.assClozeTest.php | 4 +- .../classes/class.assErrorText.php | 4 +- .../classes/class.assFileUpload.php | 4 +- .../classes/class.assFormulaQuestion.php | 4 +- .../classes/class.assFormulaQuestionGUI.php | 2 +- .../classes/class.assImagemapQuestion.php | 4 +- .../classes/class.assKprimChoice.php | 4 +- .../classes/class.assLongMenu.php | 4 +- .../classes/class.assMatchingQuestion.php | 4 +- .../classes/class.assMultipleChoice.php | 4 +- .../classes/class.assNumeric.php | 4 +- .../classes/class.assOrderingHorizontal.php | 4 +- .../classes/class.assOrderingQuestion.php | 4 +- .../classes/class.assQuestion.php | 2 +- .../classes/class.assQuestionGUI.php | 2 +- .../classes/class.assSingleChoice.php | 4 +- .../classes/class.assTextQuestion.php | 4 +- .../classes/class.assTextSubset.php | 4 +- .../classes/class.ilBulkEditQuestionsGUI.php | 4 +- .../import/qti12/class.assQuestionImport.php | 6 +-- .../class.ilAssQuestionLifecycle.php | 39 ++++--------------- .../class.ilQuestionBrowserTableGUI.php | 4 +- .../Questions/Presentation/QuestionTable.php | 4 +- .../tests/ilAssQuestionLifecycleTest.php | 12 ------ 29 files changed, 54 insertions(+), 91 deletions(-) diff --git a/components/ILIAS/Test/classes/Forms/class.ilTestRandomQuestionSetPoolDefinitionFormGUI.php b/components/ILIAS/Test/classes/Forms/class.ilTestRandomQuestionSetPoolDefinitionFormGUI.php index 3ef3cd1a7035..d29881d84d16 100755 --- a/components/ILIAS/Test/classes/Forms/class.ilTestRandomQuestionSetPoolDefinitionFormGUI.php +++ b/components/ILIAS/Test/classes/Forms/class.ilTestRandomQuestionSetPoolDefinitionFormGUI.php @@ -154,7 +154,7 @@ public function build(ilTestRandomQuestionSetSourcePoolDefinition $sourcePool, $ $lifecycleFilter = new ilSelectInputGUI($this->lng->txt('qst_lifecycle'), 'filter_lifecycle'); $lifecycleFilter->setRequired(true); $lifecycleFilter->setMulti(true); - $lifecycleFilter->setOptions(ilAssQuestionLifecycle::getDraftInstance()->getSelectOptions($this->lng)); + $lifecycleFilter->setOptions(new ilAssQuestionLifecycle()->getSelectOptions($this->lng)); $lifecycleFilter->setValue($lifecycleFilterValues); $lifecycleCheckbox->addSubItem($lifecycleFilter); $this->addItem($lifecycleCheckbox); diff --git a/components/ILIAS/Test/classes/class.ilTestQuestionFilterLabelTranslator.php b/components/ILIAS/Test/classes/class.ilTestQuestionFilterLabelTranslator.php index a164996e7271..de2ba588a036 100755 --- a/components/ILIAS/Test/classes/class.ilTestQuestionFilterLabelTranslator.php +++ b/components/ILIAS/Test/classes/class.ilTestQuestionFilterLabelTranslator.php @@ -149,7 +149,7 @@ public function getTaxonomyFilterLabel( public function getLifecycleFilterLabel(array $filter = []): string { - $lifecycle_translations = ilAssQuestionLifecycle::getDraftInstance()->getSelectOptions($this->lng); + $lifecycle_translations = new ilAssQuestionLifecycle()->getSelectOptions($this->lng); $lifecycles = []; foreach ($filter as $lifecycle) { diff --git a/components/ILIAS/Test/src/Questions/Presentation/QuestionsBrowserFilter.php b/components/ILIAS/Test/src/Questions/Presentation/QuestionsBrowserFilter.php index 8a79803341cf..b0cc810ae01b 100644 --- a/components/ILIAS/Test/src/Questions/Presentation/QuestionsBrowserFilter.php +++ b/components/ILIAS/Test/src/Questions/Presentation/QuestionsBrowserFilter.php @@ -71,7 +71,7 @@ private function getFields(FieldFactory $input): array { $lifecycle_options = array_merge( ['' => $this->lng->txt('qst_lifecycle_filter_all')], - ilAssQuestionLifecycle::getDraftInstance()->getSelectOptions($this->lng) + new ilAssQuestionLifecycle()->getSelectOptions($this->lng) ); $yes_no_all_options = [ '' => $this->lng->txt('resulttable_all'), diff --git a/components/ILIAS/Test/src/Questions/Presentation/QuestionsBrowserTable.php b/components/ILIAS/Test/src/Questions/Presentation/QuestionsBrowserTable.php index e2ab7e31744d..4bfb81613037 100644 --- a/components/ILIAS/Test/src/Questions/Presentation/QuestionsBrowserTable.php +++ b/components/ILIAS/Test/src/Questions/Presentation/QuestionsBrowserTable.php @@ -155,7 +155,7 @@ public function getRows( $record['question_type'] = $record['question_type']; $record['complete'] = (bool) $record['complete']; - $record['lifecycle'] = \ilAssQuestionLifecycle::getInstance($record['lifecycle'])->getTranslation($this->lng) ?? ''; + $record['lifecycle'] = new \ilAssQuestionLifecycle($record['lifecycle'])->getTranslation($this->lng) ?? ''; $record['created'] = (new \DateTimeImmutable("@{$record['created']}"))->setTimezone($timezone); $record['tstamp'] = (new \DateTimeImmutable("@{$record['tstamp']}"))->setTimezone($timezone); diff --git a/components/ILIAS/Test/src/Questions/Properties/Properties.php b/components/ILIAS/Test/src/Questions/Properties/Properties.php index 21aa07136104..c2b22cd723e2 100644 --- a/components/ILIAS/Test/src/Questions/Properties/Properties.php +++ b/components/ILIAS/Test/src/Questions/Properties/Properties.php @@ -107,7 +107,7 @@ public function getAsQuestionsTableRow( 'points' => $this->question_properties->getAvailablePoints(), 'author' => $this->question_properties->getAuthor(), 'complete' => $this->question_properties->isRequiredInformationComplete(), - 'lifecycle' => \ilAssQuestionLifecycle::getInstance($this->question_properties->getLifecycle())->getTranslation($lng) ?? '', + 'lifecycle' => new \ilAssQuestionLifecycle($this->question_properties->getLifecycle())->getTranslation($lng) ?? '', 'qpl' => $title_builder->buildAccessCheckedQuestionpoolTitleAsLink($this->question_properties->getOriginObjectId()), 'nr_of_answers' => $this->getAggregatedResults()->getNumberOfAnswers(), 'average_points' => $this->getAggregatedResults()->getAveragePoints(), diff --git a/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php b/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php index 71d21ef35e55..276871acbc50 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assClozeTest.php @@ -172,9 +172,9 @@ public function loadFromDb(int $question_id): void $this->setFeedbackMode($data['feedback_mode'] === null ? ilAssClozeTestFeedback::FB_MODE_GAP_QUESTION : $data['feedback_mode']); try { - $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle'])); + $this->setLifecycle(new ilAssQuestionLifecycle($data['lifecycle'])); } catch (ilTestQuestionPoolInvalidArgumentException $e) { - $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance()); + $this->setLifecycle(new ilAssQuestionLifecycle()); } $this->question = ilRTE::_replaceMediaObjectImageSrc($this->question, 1); diff --git a/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php b/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php index ccdd669c9354..7b47bb3f7e92 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assErrorText.php @@ -175,9 +175,9 @@ public function loadFromDb($question_id): void $this->setPointsWrong($data["points_wrong"]); try { - $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle'])); + $this->setLifecycle(new ilAssQuestionLifecycle($data['lifecycle'])); } catch (ilTestQuestionPoolInvalidArgumentException $e) { - $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance()); + $this->setLifecycle(new ilAssQuestionLifecycle()); } try { diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php b/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php index 5ada3cf53b4b..a72b8835a50b 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFileUpload.php @@ -167,9 +167,9 @@ public function loadFromDb(int $question_id): void $this->setCompletionBySubmission($data['compl_by_submission'] == 1 ? true : false); try { - $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle'])); + $this->setLifecycle(new ilAssQuestionLifecycle($data['lifecycle'])); } catch (ilTestQuestionPoolInvalidArgumentException $e) { - $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance()); + $this->setLifecycle(new ilAssQuestionLifecycle()); } try { diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php index 01a601537ad7..d5beaab1e4ed 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestion.php @@ -714,9 +714,9 @@ public function loadFromDb(int $question_id): void $this->setOwner($data["owner"]); try { - $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle'])); + $this->setLifecycle(new ilAssQuestionLifecycle($data['lifecycle'])); } catch (ilTestQuestionPoolInvalidArgumentException $e) { - $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance()); + $this->setLifecycle(new ilAssQuestionLifecycle()); } try { diff --git a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionGUI.php b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionGUI.php index 3af4a7ed05f0..83b2fd4c3687 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assFormulaQuestionGUI.php @@ -98,7 +98,7 @@ protected function writePostData(bool $always = false): int } try { - $lifecycle = ilAssQuestionLifecycle::getInstance($this->request_data_collector->string('lifecycle')); + $lifecycle = new ilAssQuestionLifecycle($this->request_data_collector->string('lifecycle')); $this->object->setLifecycle($lifecycle); } catch (ilTestQuestionPoolInvalidArgumentException $e) { } diff --git a/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php index 54ef538e5027..4e55cb7645b1 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assImagemapQuestion.php @@ -259,9 +259,9 @@ public function loadFromDb(int $question_id): void $this->setImageFilename($data['image_file'] ?? ''); try { - $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle'])); + $this->setLifecycle(new ilAssQuestionLifecycle($data['lifecycle'])); } catch (ilTestQuestionPoolInvalidArgumentException $e) { - $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance()); + $this->setLifecycle(new ilAssQuestionLifecycle()); } try { diff --git a/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php index 29a9dac0dd0b..bd781b632666 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assKprimChoice.php @@ -261,9 +261,9 @@ public function loadFromDb($questionId): void } try { - $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle'])); + $this->setLifecycle(new ilAssQuestionLifecycle($data['lifecycle'])); } catch (ilTestQuestionPoolInvalidArgumentException $e) { - $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance()); + $this->setLifecycle(new ilAssQuestionLifecycle()); } try { diff --git a/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php b/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php index f9fa182c29e0..b3c299090cb6 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assLongMenu.php @@ -387,9 +387,9 @@ public function loadFromDb($question_id): void } try { - $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle'])); + $this->setLifecycle(new ilAssQuestionLifecycle($data['lifecycle'])); } catch (ilTestQuestionPoolInvalidArgumentException $e) { - $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance()); + $this->setLifecycle(new ilAssQuestionLifecycle()); } try { diff --git a/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php index 6d336d348c6d..56203be65cd4 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assMatchingQuestion.php @@ -262,9 +262,9 @@ public function loadFromDb($question_id): void $this->setMatchingMode($data['matching_mode'] === null ? self::MATCHING_MODE_1_ON_1 : $data['matching_mode']); try { - $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle'])); + $this->setLifecycle(new ilAssQuestionLifecycle($data['lifecycle'])); } catch (ilTestQuestionPoolInvalidArgumentException $e) { - $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance()); + $this->setLifecycle(new ilAssQuestionLifecycle()); } try { diff --git a/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php index 11e00d070d52..45f230b17846 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assMultipleChoice.php @@ -144,9 +144,9 @@ public function loadFromDb(int $question_id): void } try { - $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle'])); + $this->setLifecycle(new ilAssQuestionLifecycle($data['lifecycle'])); } catch (ilTestQuestionPoolInvalidArgumentException $e) { - $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance()); + $this->setLifecycle(new ilAssQuestionLifecycle()); } try { diff --git a/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php b/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php index 82588b65e218..b6ae2aa43eb5 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assNumeric.php @@ -88,9 +88,9 @@ public function loadFromDb(int $question_id): void $this->setMaxChars($data["maxnumofchars"]); try { - $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle'])); + $this->setLifecycle(new ilAssQuestionLifecycle($data['lifecycle'])); } catch (ilTestQuestionPoolInvalidArgumentException $e) { - $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance()); + $this->setLifecycle(new ilAssQuestionLifecycle()); } try { diff --git a/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php b/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php index e35a0ef8e7cc..6e9a40fc75a6 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assOrderingHorizontal.php @@ -132,9 +132,9 @@ public function loadFromDb($question_id): void $this->setTextSize($data["textsize"]); try { - $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle'])); + $this->setLifecycle(new ilAssQuestionLifecycle($data['lifecycle'])); } catch (ilTestQuestionPoolInvalidArgumentException $e) { - $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance()); + $this->setLifecycle(new ilAssQuestionLifecycle()); } try { diff --git a/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php index d01a9d786935..11982ea0d8b7 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assOrderingQuestion.php @@ -149,9 +149,9 @@ public function loadFromDb($question_id): void $this->element_height = $data["element_height"] ? (int) $data['element_height'] : null; try { - $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle'])); + $this->setLifecycle(new ilAssQuestionLifecycle($data['lifecycle'])); } catch (ilTestQuestionPoolInvalidArgumentException $e) { - $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance()); + $this->setLifecycle(new ilAssQuestionLifecycle()); } try { diff --git a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php index 0e99545495ef..d60ba080c425 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assQuestion.php @@ -164,7 +164,7 @@ public function __construct( $this->questionActionCmd = 'handleQuestionAction'; $this->export_image_path = ''; $this->shuffler = $DIC->refinery()->random()->dontShuffle(); - $this->lifecycle = ilAssQuestionLifecycle::getDraftInstance(); + $this->lifecycle = new ilAssQuestionLifecycle(); $this->skillUsageService = $DIC->skills()->usage(); $this->test_result_repository = TestDIC::dic()['results.data.repository']; diff --git a/components/ILIAS/TestQuestionPool/classes/class.assQuestionGUI.php b/components/ILIAS/TestQuestionPool/classes/class.assQuestionGUI.php index f6645a6b19bb..b07f212af67a 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assQuestionGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assQuestionGUI.php @@ -1669,7 +1669,7 @@ protected function writeQuestionGenericPostData(): void } try { - $lifecycle = ilAssQuestionLifecycle::getInstance( + $lifecycle = new ilAssQuestionLifecycle( $this->request_data_collector->string('lifecycle') ); $this->object->setLifecycle($lifecycle); diff --git a/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php b/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php index 540e57410634..798603bbb875 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assSingleChoice.php @@ -127,9 +127,9 @@ public function loadFromDb(int $question_id): void $this->feedback_setting = $data['feedback_setting'] ?? self::FEEDBACK_MODE_SELECTED_ANSWERS; try { - $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle'])); + $this->setLifecycle(new ilAssQuestionLifecycle($data['lifecycle'])); } catch (ilTestQuestionPoolInvalidArgumentException $e) { - $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance()); + $this->setLifecycle(new ilAssQuestionLifecycle()); } try { diff --git a/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php b/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php index f07009e857b1..2731005a3bd4 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assTextQuestion.php @@ -145,9 +145,9 @@ public function loadFromDb(int $question_id): void $this->setKeywordRelation(($data['keyword_relation'])); try { - $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle'])); + $this->setLifecycle(new ilAssQuestionLifecycle($data['lifecycle'])); } catch (ilTestQuestionPoolInvalidArgumentException $e) { - $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance()); + $this->setLifecycle(new ilAssQuestionLifecycle()); } try { diff --git a/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php b/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php index e7fdeeecea4a..59fbee0d2d91 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php +++ b/components/ILIAS/TestQuestionPool/classes/class.assTextSubset.php @@ -92,9 +92,9 @@ public function loadFromDb(int $question_id): void $this->setTextRating($data['textgap_rating'] ?? assClozeGap::TEXTGAP_RATING_CASEINSENSITIVE); try { - $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle'])); + $this->setLifecycle(new ilAssQuestionLifecycle($data['lifecycle'])); } catch (ilTestQuestionPoolInvalidArgumentException $e) { - $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance()); + $this->setLifecycle(new ilAssQuestionLifecycle()); } try { diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilBulkEditQuestionsGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilBulkEditQuestionsGUI.php index 58c4856b3367..4e8b94e81f28 100644 --- a/components/ILIAS/TestQuestionPool/classes/class.ilBulkEditQuestionsGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilBulkEditQuestionsGUI.php @@ -175,7 +175,7 @@ protected function getAuthorUpdater(): \Closure protected function getFormLifecycle(): Form\Standard { - $lifecycle = \ilAssQuestionLifecycle::getDraftInstance(); + $lifecycle = new \ilAssQuestionLifecycle(); $options = $lifecycle->getSelectOptions($this->lng); return $this->ui_factory->input()->container()->form()->standard( $this->ctrl->getFormAction($this, self::CMD_SAVELIFECYCLE), @@ -191,7 +191,7 @@ protected function getFormLifecycle(): Form\Standard protected function getLifecycleUpdater(): \Closure { return function (array $questions, string $lifecycle) { - $lc = ilAssQuestionLifecycle::getInstance($lifecycle); + $lc = new ilAssQuestionLifecycle($lifecycle); foreach ($questions as $q) { $q->setLifecycle($lc); $q->saveToDb(); diff --git a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assQuestionImport.php b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assQuestionImport.php index c9b9f2fe8154..3f7c4ea39b80 100755 --- a/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assQuestionImport.php +++ b/components/ILIAS/TestQuestionPool/classes/import/qti12/class.assQuestionImport.php @@ -211,7 +211,7 @@ protected function addGeneralMetadata(ilQTIItem $item): void protected function fetchLifecycle(ilQTIItem $item): ilAssQuestionLifecycle { try { - $lifecycle = ilAssQuestionLifecycle::getInstance( + $lifecycle = new ilAssQuestionLifecycle( $item->getMetadataEntry('ilias_lifecycle') ); } catch (ilTestQuestionPoolInvalidArgumentException $e) { @@ -220,11 +220,11 @@ protected function fetchLifecycle(ilQTIItem $item): ilAssQuestionLifecycle $item->getMetadataEntry('lifecycle') ); - $lifecycle = ilAssQuestionLifecycle::getInstance( + $lifecycle = new ilAssQuestionLifecycle( $lomLifecycle->getMappedIliasLifecycleIdentifer() ); } catch (ilTestQuestionPoolInvalidArgumentException $e) { - $lifecycle = ilAssQuestionLifecycle::getDraftInstance(); + $lifecycle = new ilAssQuestionLifecycle(); } } diff --git a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionLifecycle.php b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionLifecycle.php index 9cc5c3357144..16bd96421f84 100755 --- a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionLifecycle.php +++ b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssQuestionLifecycle.php @@ -34,11 +34,12 @@ class ilAssQuestionLifecycle implements ToNormalized, FromNormalized public const SHARABLE = 'sharable'; public const OUTDATED = 'outdated'; - protected string $identifier; + private string $identifier; - private function __construct() + public function __construct($identifier = self::DRAFT) { - $this->setIdentifier(self::DRAFT); + $this->validateIdentifier($identifier); + $this->setIdentifier($identifier); } public function getIdentifier(): string @@ -136,31 +137,7 @@ private function validateIdentifier($identifier): void } } - /** - * @param mixed $identifier - * @return self - * @throws ilTestQuestionPoolInvalidArgumentException - */ - public static function getInstance($identifier): self - { - $lifecycle = new self(); - $lifecycle->validateIdentifier($identifier); - $lifecycle->setIdentifier($identifier); - - return $lifecycle; - } - - public static function getDraftInstance(): self - { - $lifecycle = new self(); - $lifecycle->setIdentifier(self::DRAFT); - - return $lifecycle; - } - - /** - * @inheritDoc - */ + #[\Override] public function toNormalized( Transformations $transformations, array $context = [] @@ -171,14 +148,12 @@ public function toNormalized( ]; } - /** - * @inheritDoc - */ + #[\Override] public function fromNormalized( array $normalized, Transformations $transformations ): self { - return self::getInstance($normalized['identifier']); + return new self($normalized['identifier']); } } diff --git a/components/ILIAS/TestQuestionPool/classes/tables/class.ilQuestionBrowserTableGUI.php b/components/ILIAS/TestQuestionPool/classes/tables/class.ilQuestionBrowserTableGUI.php index 2a7deefc1c74..f7a3d6b391a2 100755 --- a/components/ILIAS/TestQuestionPool/classes/tables/class.ilQuestionBrowserTableGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/tables/class.ilQuestionBrowserTableGUI.php @@ -306,7 +306,7 @@ public function initFilter(): void // lifecycle $lifecycleOptions = array_merge( ['' => $this->lng->txt('qst_lifecycle_filter_all')], - ilAssQuestionLifecycle::getDraftInstance()->getSelectOptions($this->lng) + new ilAssQuestionLifecycle()->getSelectOptions($this->lng) ); $lifecycleInp = new ilSelectInputGUI($this->lng->txt('qst_lifecycle'), 'lifecycle'); $lifecycleInp->setOptions($lifecycleOptions); @@ -405,7 +405,7 @@ public function fillRow(array $a_set): void $this->tpl->parseCurrentBlock(); } if ($c == 'lifecycle') { - $lifecycle = ilAssQuestionLifecycle::getInstance($a_set['lifecycle']); + $lifecycle = new ilAssQuestionLifecycle($a_set['lifecycle']); $this->tpl->setCurrentBlock('lifecycle'); $this->tpl->setVariable("QUESTION_LIFECYCLE", $lifecycle->getTranslation($this->lng)); diff --git a/components/ILIAS/TestQuestionPool/src/Questions/Presentation/QuestionTable.php b/components/ILIAS/TestQuestionPool/src/Questions/Presentation/QuestionTable.php index e7668c436057..dd75369d4d5c 100755 --- a/components/ILIAS/TestQuestionPool/src/Questions/Presentation/QuestionTable.php +++ b/components/ILIAS/TestQuestionPool/src/Questions/Presentation/QuestionTable.php @@ -109,7 +109,7 @@ public function getFilter(\ilUIService $ui_service, string $action): Filter { $lifecycle_options = array_merge( ['' => $this->lng->txt('qst_lifecycle_filter_all')], - \ilAssQuestionLifecycle::getDraftInstance()->getSelectOptions($this->lng) + new \ilAssQuestionLifecycle()->getSelectOptions($this->lng) ); $question_type_options = [ '' => $this->lng->txt('filter_all_question_types') @@ -320,7 +320,7 @@ public function getRows( $row_id = (string) $record['question_id']; $record['created'] = (new \DateTimeImmutable("@{$record['created']}"))->setTimezone($timezone); $record['tstamp'] = (new \DateTimeImmutable("@{$record['tstamp']}"))->setTimezone($timezone); - $lifecycle = \ilAssQuestionLifecycle::getInstance($record['lifecycle']); + $lifecycle = new \ilAssQuestionLifecycle($record['lifecycle']); $record['lifecycle'] = $lifecycle->getTranslation($this->lng); $title = $record['title']; diff --git a/components/ILIAS/TestQuestionPool/tests/ilAssQuestionLifecycleTest.php b/components/ILIAS/TestQuestionPool/tests/ilAssQuestionLifecycleTest.php index 14a34bb76923..5be6810d66ca 100644 --- a/components/ILIAS/TestQuestionPool/tests/ilAssQuestionLifecycleTest.php +++ b/components/ILIAS/TestQuestionPool/tests/ilAssQuestionLifecycleTest.php @@ -50,16 +50,4 @@ public function testConstructAcceptsIdentifier(): void $this->assertSame(ilAssQuestionLifecycle::FINAL, $lifecycle->getIdentifier()); } - - public function testFactoryRemainsWrapper(): void - { - $this->assertSame( - ilAssQuestionLifecycle::REVIEW, - ilAssQuestionLifecycle::getInstance(ilAssQuestionLifecycle::REVIEW)->getIdentifier() - ); - $this->assertSame( - ilAssQuestionLifecycle::DRAFT, - ilAssQuestionLifecycle::getDraftInstance()->getIdentifier() - ); - } } From bf96c0b71a88fd9b0cc6d13a397bfab24f549c39 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Tue, 22 Sep 2026 15:30:54 +0200 Subject: [PATCH 59/66] fix: revert unintentional changes --- .../src/Stream/ReattachableStream.php | 18 +++++------------- .../ILIAS/Test/classes/class.ilObjTestGUI.php | 3 +-- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/components/ILIAS/Filesystem/src/Stream/ReattachableStream.php b/components/ILIAS/Filesystem/src/Stream/ReattachableStream.php index 78d83c7a520f..085e899b9456 100644 --- a/components/ILIAS/Filesystem/src/Stream/ReattachableStream.php +++ b/components/ILIAS/Filesystem/src/Stream/ReattachableStream.php @@ -25,23 +25,15 @@ */ class ReattachableStream extends Stream { - private ?string $reattach_uri = null; - private ?string $reattach_mode = null; - - #[\Override] - public function detach() - { - $this->reattach_uri = $this->uri; - $this->reattach_mode = $this->_mode; - - return parent::detach(); - } - + /** + * Checks if the stream is attached to the wrapper. + * If not, the stream is reattached. + */ #[\Override] protected function assertStreamAttached(): void { if ($this->stream === null) { - $this->stream = fopen($this->reattach_uri, $this->reattach_mode); + $this->stream = fopen($this->uri, $this->_mode); } } } diff --git a/components/ILIAS/Test/classes/class.ilObjTestGUI.php b/components/ILIAS/Test/classes/class.ilObjTestGUI.php index 966b95c19ae2..e17262eca59f 100755 --- a/components/ILIAS/Test/classes/class.ilObjTestGUI.php +++ b/components/ILIAS/Test/classes/class.ilObjTestGUI.php @@ -1403,8 +1403,7 @@ protected function importFile(string $file_to_import, string $path_to_uploaded_f public function processImportObject(): void { - $permission = $this->creation_mode ? 'create' : 'read'; - if (!$this->checkPermissionBool($permission, '', $this->object->getType())) { + if (!$this->checkPermissionBool('write', '', $this->object->getType())) { $this->redirectAfterMissingWrite(); return; } From baa034bf0047480bf46c1ca39d1e82396cef2c1e Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Tue, 22 Sep 2026 16:03:40 +0200 Subject: [PATCH 60/66] docs: update privacy and roadmap --- components/ILIAS/Test/PRIVACY.md | 20 ++++++++++++++++++-- components/ILIAS/Test/ROADMAP.md | 3 +-- components/ILIAS/TestQuestionPool/PRIVACY.md | 1 + 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/components/ILIAS/Test/PRIVACY.md b/components/ILIAS/Test/PRIVACY.md index e4c1616016f5..0370c389c217 100644 --- a/components/ILIAS/Test/PRIVACY.md +++ b/components/ILIAS/Test/PRIVACY.md @@ -40,6 +40,8 @@ At the Administration node for Test and Assessment accounts having 'Edit Setting At Administration > Repository and Objects > Test and Assessment > Settings it is possible to select which 'Unique user criteria' is used in test imports/exports. The selected type of personal data will be included in export type 'XML incl. Participants Results' of a test for each account. Options are: usr_id, login, email, matriculation, ext_account. This personal data is required to match accounts results when importing an export file at the same or another platform. +When importing such an export file, the accounts are looked up by the 'Unique user criteria' configured at the importing platform. Test attempts of accounts which cannot be matched are not discarded but assigned to the anonymous account. The personal data contained in the export file for those accounts is therefore imported without being linked to an account of the importing platform. + At Administration > Repository and Objects > Test and Assessment > Log Data accounts with 'Edit Settings' permission can activate the History-tab via the checkbox 'Activate Test and Assessment Logging'. Additionally the setting 'Log IP' allows for logging the IP-adress of participants along with the interactions as well as specific settings of said test during the interaction. The purpose of both options is to store important events and information for configuring and performing tests. This gives the possibility to check those information in case of issues or concerns after performing tests. @@ -230,9 +232,23 @@ There are three export files at the tab Test > Export which contain personal dat The ‘Archive file’ contains all personal data which is being stored and presented in the test (see above). Its purpose is to have this data easily accessible outside of ILIAS, e.g. for long-term archiving of the data. -The 'XML' export contains the personal data 'Author' of the questions and of the test itself within the metadata (see above). Its purpose is to be imported into ILIAS again, although the contained personal data is easily accessible. +The 'XML' export contains the personal data 'Author' of the questions and of the test itself within the metadata (see above). In addition it contains the 'User ID' of the owner of each question. Its purpose is to be imported into ILIAS again, although the contained personal data is easily accessible. + +The ‘XML export incl. Participant Results’ contains all personal data which is being stored and presented in the test (see above). The data in the ‘History’ tab is an exception; it is not included. Its purpose is to be imported into ILIAS again, although the contained personal data is easily accessible. This export file can be used to, e.g., provide the test results to the participants at another ILIAS installation. In addition to the data of the 'XML' export, it contains the following personal data for each participant: -The ‘XML export incl. Participant Results’ contains all personal data which is being stored and presented in the test (see above). The data in the ‘History’ tab is an exception; it is not included. Its purpose is to be imported into ILIAS again, although the contained personal data is easily accessible. This export file can be used to, e.g., provide the test results to the participants at another ILIAS installation. +- User ID (exported as the 'Unique user criteria' configured at Administration > Repository and Objects > Test and Assessment > Settings, see above) +- Anonymous ID, if the test is set to anonymous +- Name (first name and last name) +- Login +- Matriculation Number +- Import name, if the account was imported +- Client IP Range +- Invitation date +- Additional working time granted to the participant +- Timestamp of the first and the last access +- All test attempts with their timestamps, answers, scoring and status + +For questions which have been scored manually, the 'User ID' of the account which finalized the scoring is contained as well. ### Administration > Repository and Objects > Test and Assessment > Log Data > Log Data Output diff --git a/components/ILIAS/Test/ROADMAP.md b/components/ILIAS/Test/ROADMAP.md index da4b23f58c30..193e98d0322e 100755 --- a/components/ILIAS/Test/ROADMAP.md +++ b/components/ILIAS/Test/ROADMAP.md @@ -23,7 +23,7 @@ Among other challenges, we are planning to use the given developing budget from * Use of IRSS in T&A * Further reduction of access and use of request variables (GPCR) 5. Work on [(Project) Revision of Test Question Creation](https://docu.ilias.de/go/wiki/wpage_8111_1357) -6. Work on Project Revision of Import/Export of the T&A +6. Work on Project Revision of Import/Export of the T&A. Export and import of Test and Question Pool have been rebuilt upon a normalized XML format in ILIAS 12. The QTI based export and the Excel export of questions are deprecated, import of files created with previous ILIAS versions is covered by dedicated legacy importers. Still open are the revision of the Archive file and the removal of the deprecated legacy code. 7. Feasibility study Test in Learning sequence - Could the LS uses the actual Exam View (since ILIAS9 - General Kiosk Mode) 8. Authorized Saving of Participant Input - Problems e.g. in the question list, see [Mantis 44211](https://mantis.ilias.de/view.php?id=44211). In order to address the problem, we need a concept for the entire test run at the technical level and an analysis of the current situation. From this, we want to derive an operating concept for controlling the test run. By redesigning the test run, we know where we want to go and can at least mitigate the problems in existing ILIAS versions with usability fixes. 9. KS Improvements / Exchange of single elements' trough existing KS elements independently of the Legacy-UI-Project @@ -46,7 +46,6 @@ Remarks on the individual items are marked with "@PHP8-CR" * \ilTestPlayerAbstractGUI::autosaveCmd / This looks like another issue in the autosaving. Left for review/analysis by TechSquad * \ilAssLacCompositeValidator::validateSubTree / Incompatible type. Left for review/analysis by TechSquad ### TestQuestionPool -* \ilObjQuestionPoolGUI::exportQuestionObject / Void result used. Left for review/analysis by TechSquad * \assMatchingQuestionGUI::writeAnswerSpecificPostData / Incompatible type. Left for review/analysis by TechSquad * \assMatchingQuestionGUI::populateAnswerSpecificFormPart / Incompatible type. Left for review/analysis by TechSquad * \assMatchingQuestionImport::fromXML / Incompatible type. Left for review/analysis by TechSquad diff --git a/components/ILIAS/TestQuestionPool/PRIVACY.md b/components/ILIAS/TestQuestionPool/PRIVACY.md index afcf3ed4c589..a39834feb174 100755 --- a/components/ILIAS/TestQuestionPool/PRIVACY.md +++ b/components/ILIAS/TestQuestionPool/PRIVACY.md @@ -43,4 +43,5 @@ It is possible to delete questions. Within this the personal data at the field ' ## Data being exported - The XML export of the question pool contains the personal data 'Author' of the questions and of the question pool itself within the metadata. +- In addition the XML export contains the 'User ID' of the owner of each question (see 'Ownership of Questions' above). - It's purpose is to being imported in ILIAS again, although the contained personal data is easily accesible. Including this information ensures that the authorship of the question is not lost after import. In addition it is possible to contact the author, e.g. if there are problems with the question or the configuration of it. From 48beb74d04ff559f1295eaac79c5add4c011c4b7 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Tue, 22 Sep 2026 16:21:18 +0200 Subject: [PATCH 61/66] fix: merge changes from 63f95a1 after rebase --- components/ILIAS/Test/src/ExportImport/Import/TestImporter.php | 3 ++- components/ILIAS/Test/src/ExportImport/TestExporter.php | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php index 0cba30b22be0..e7dd970e25b8 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php @@ -290,7 +290,8 @@ private function importTest( $mapping->addMapping('components/ILIAS/Test', 'tst', (string) $old_test_id, (string) $test_object->getTestId()); $mapping->addMapping('components/ILIAS/Test', 'object', (string) $old_obj_id, (string) $new_obj_id); $mapping->addMapping('components/ILIAS/MetaData', 'md', "{$old_obj_id}:0:tst", "{$new_obj_id}:0:tst"); - + $mapping->addMapping('components/ILIAS/Tracking', 'lpsettings', (string) $old_obj_id, (string) $new_obj_id); + return $test_object; } diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index ae9c6ed49bd5..d15736af2872 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -295,6 +295,7 @@ private function exportObject( 'tax', $this->taxonomy->getUsageOfObject($obj_id) ); + $state->addDependency('components/ILIAS/Tracking', 'lpsettings', [$obj_id]); } private function exportSettings( From 9c4ae57ebca3de3ef6760931c9f3f991c351728e Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Tue, 22 Sep 2026 16:21:47 +0200 Subject: [PATCH 62/66] fix: code style --- .../class.ilTestRandomQuestionSetSourcePoolDefinition.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php b/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php index c3f74c639632..34544c4238e9 100755 --- a/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php +++ b/components/ILIAS/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinition.php @@ -479,8 +479,7 @@ public function getPoolInfoLabel(ilLanguage $lng): string public function toNormalized( Transformations $transformations, array $context = [] - ): array|float|bool|int|string|null - { + ): array|float|bool|int|string|null { $normalized = [ 'id' => $transformations->normalize(new Id($this->getId(), 'rnd_src_pool_def')), 'pool_id' => $transformations->normalize(new Id($this->getPoolId(), 'qpl')), @@ -522,8 +521,7 @@ public function toNormalized( public function fromNormalized( array $normalized, Transformations $transformations - ): static - { + ): static { $clone = clone $this; $clone->setId($transformations->denormalize($normalized['id'], Id::class)->getId()); $clone->setPoolId($transformations->denormalize($normalized['pool_id'], Id::class)->getId()); @@ -557,4 +555,4 @@ public function fromNormalized( return $clone; } -} \ No newline at end of file +} From c2e5edeab8847371c9257ef4e6fb5b7f4c3767ad Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Thu, 22 Oct 2026 14:38:12 +0200 Subject: [PATCH 63/66] refactor: adjust Id-envelope and remove debug logging --- .../src/ExportImport/Import/PersistStage.php | 1 - .../Import/RandomTestConfigImporter.php | 8 ---- .../Import/SkillLevelThresholdsImporter.php | 2 - .../src/ExportImport/Import/TestImporter.php | 32 ++-------------- .../Import/TestResultsImporter.php | 10 ----- .../Import/UserImportResolver.php | 3 -- .../Test/src/ExportImport/TestExporter.php | 3 -- .../ExportImport/TransformationsBuilder.php | 4 +- components/ILIAS/Test/src/TestDIC.php | 1 - .../Export/QuestionPoolExporter.php | 2 - .../Foundation/Export/XmlExporterBridge.php | 16 -------- .../Foundation/Normalize/Envelopes/Id.php | 38 +++++-------------- ...tration.php => NormalizerRegistration.php} | 5 ++- .../Normalize/Processors/CollectResources.php | 5 +-- .../src/ExportImport/Import/CleanupStage.php | 5 +-- .../Import/DetectLegacyImportStage.php | 2 - .../Import/QuestionPoolImporter.php | 9 ----- .../Import/QuestionSelectionStage.php | 4 -- .../ExportImport/Import/QuestionsImporter.php | 11 ------ .../Import/SkillAssignmentsImporter.php | 2 - .../Import/UploadValidationStage.php | 3 -- .../ExportImport/TransformationsBuilder.php | 7 ++-- 22 files changed, 26 insertions(+), 147 deletions(-) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/{FoundationNormalizerRegistration.php => NormalizerRegistration.php} (98%) diff --git a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php index 8f14fd279b53..05cf0d49c203 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php +++ b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php @@ -103,7 +103,6 @@ private function importMappingsFile(ImportContext $context): ?StageResult }); $deserializer->process(); - $this->log->info("Processed mappings file: {$mappings_file}"); $this->session->setContext($context); return null; diff --git a/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php b/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php index ca8ad9393383..7d799e160b90 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/RandomTestConfigImporter.php @@ -22,7 +22,6 @@ use ILIAS\Data\Factory as DataFactory; use ILIAS\Test\ExportImport\Normalize\Envelopes\QuestionSetConfig; -use Psr\Log\LoggerInterface; /** * Imports the random question set config and its related data. It also remaps the taxonomy IDs in the mapped_taxonomy_filter @@ -32,7 +31,6 @@ class RandomTestConfigImporter { public function __construct( private readonly \ilDBInterface $database, - private readonly LoggerInterface $log, private readonly DataFactory $data_factory ) { } @@ -51,7 +49,6 @@ public function import( } $config->getConfig()->saveToDb(); - $this->log->debug("Imported random question set config for test {$test_object->getTestId()}"); foreach ($config->getStagingPools() as $pool_id => $questions) { $this->importRandomQuestionStagingPool($pool_id, $questions, $mapping, $test_object); @@ -75,7 +72,6 @@ private function importRandomQuestionStagingPool( (string) $old_pool_id, (string) $new_pool_id ); - $this->log->debug("Imported random question staging pool: {$old_pool_id} -> {$new_pool_id}"); // QuestionID was mapped during question set config denormalization foreach ($questions as $question_id) { @@ -84,7 +80,6 @@ private function importRandomQuestionStagingPool( $question->setPoolId($new_pool_id); $question->setQuestionId($question_id); $question->saveQuestionStaging(); - $this->log->debug("Imported random question staging question: {$question_id}"); } } @@ -101,14 +96,12 @@ private function importSourcePoolDefinition( $ref_ids = $this->data_factory->objId($new_pool_id)->toReferenceIds(); if ($ref_ids !== []) { $definition->setPoolRefId(current($ref_ids)->toInt()); - $this->log->debug("Derived source pool definition from Object ID: {$old_pool_id} -> {$definition->getPoolRefId()}"); } } $old_definition_id = $definition->getId(); $definition->setId(0); $definition->saveToDb(); - $this->log->debug("Imported source pool definition: {$old_definition_id} -> {$definition->getId()}"); $mapping->addMapping( 'components/ILIAS/Test', @@ -150,7 +143,6 @@ public function finalizeTaxonomyFilters(\ilImportMapping $mapping): void $definition->setMappedTaxonomyFilter($this->remapTaxonomyFilter($mapping, $mapped_filter)); $definition->saveToDb(); - $this->log->debug("Remapped taxonomy filter for definition {$definition->getId()}"); } } } diff --git a/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php b/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php index 432494bb7b84..a45382aa5406 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/SkillLevelThresholdsImporter.php @@ -69,7 +69,6 @@ public function import( $result['failed'][] = $this->buildResultData($threshold); continue; } - $this->log->debug("Found skill level id mapping for threshold: {$threshold->getSkillLevelId()} -> {$local_level_id}"); $mapping->addMapping( $this->component, @@ -84,7 +83,6 @@ public function import( } $threshold_list->saveToDb(); - $this->log->debug('Saved skill level thresholds'); return $result; } diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php index e7dd970e25b8..7ec64546dedc 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php @@ -194,24 +194,17 @@ function (array $times) use ($transformations): void { } ); - $this->log->info('Importing users and resources mappings...'); $this->importMappings($mapping, $resource_collector, $context); - $this->log->info('...Finished importing users and resources mappings'); - $this->log->info('Importing test export file...'); $deserializer->process(); - $this->log->info('...Finished importing test export file'); - $this->log->info('Importing question images...'); $this->questions_importer->importQuestionImages( $test_object->getId(), $mapping, $context, $question_images_collector ); - $this->log->info('...Finished importing question images'); - $this->log->info("Finished importing test {$test_object->getTestId()} (Test ID), {$test_object->getId()} (Object ID)"); return $context->withTestObjId($test_object->getId())->withTestRefId($test_object->getRefId()); } @@ -222,10 +215,8 @@ function (array $times) use ($transformations): void { */ public function finalize(\ilImportMapping $mapping): void { - $this->log->info('Finalizing test import...'); $this->questions_importer->finalizeQuestionPages($mapping); $this->random_test_config_importer->finalizeTaxonomyFilters($mapping); - $this->log->info('...Finished finalizing test'); } @@ -236,16 +227,13 @@ private function importMappings( ): void { $user_mapping = $context->userMappings(); - $this->log->info('Importing user mappings...'); $user_resolver = new UserImportResolver($this->database, $this->log); $imported_users = $user_resolver->resolve( UserIdentifiers::from($user_mapping['identifier']), $user_mapping['mapping'] ); $user_resolver->store($imported_users, $mapping); - $this->log->info('...Finished importing user mappings'); - $this->log->info('Importing resources and storing mappings...'); $import_dir = dirname($context->componentImportFile()) . '/expDir_1'; foreach ($context->resourceMappings() as $resource) { $clean_id = str_replace(['-', '_'], '', $resource['id']); @@ -261,9 +249,7 @@ private function importMappings( $resource['title'] ); $resource_collector->storeMapping($resource['id'], $new_id); - $this->log->debug("Imported resource: {$resource_path} -> {$new_id->serialize()}"); } - $this->log->info('...Finished importing resources and storing mappings'); } private function importTest( @@ -280,18 +266,16 @@ private function importTest( $test_object->setTitle($test_object->getTitle()); $new_obj_id = $test_object->create(); $test_object->saveToDb(true); - $this->log->debug("Created new test object: {$old_test_id} -> {$test_object->getTestId()} (Test ID), {$old_obj_id} -> {$new_obj_id} (Object ID)"); $test_object->createReference(); $test_object->putInTree($parent_id->toInt()); $test_object->setPermissions($parent_id->toInt()); - $this->log->debug("Stored test object in tree: {$parent_id->toInt()} (Parent Ref) -> {$test_object->getRefId()} (Test Ref)"); $mapping->addMapping('components/ILIAS/Test', 'tst', (string) $old_test_id, (string) $test_object->getTestId()); $mapping->addMapping('components/ILIAS/Test', 'object', (string) $old_obj_id, (string) $new_obj_id); $mapping->addMapping('components/ILIAS/MetaData', 'md', "{$old_obj_id}:0:tst", "{$new_obj_id}:0:tst"); $mapping->addMapping('components/ILIAS/Tracking', 'lpsettings', (string) $old_obj_id, (string) $new_obj_id); - + return $test_object; } @@ -313,7 +297,6 @@ private function importSettings( $main_settings = $main_settings->withIntroductionSettings( $main_settings->getIntroductionSettings()->withIntroductionPageId($new_page_id) ); - $this->log->debug("Imported introduction page: {$intro_page_id} -> {$new_page_id}"); } $concluding_page_id = $main_settings->getFinishingSettings()->getConcludingRemarksPageId(); @@ -322,13 +305,11 @@ private function importSettings( $main_settings = $main_settings->withFinishingSettings( $main_settings->getFinishingSettings()->withConcludingRemarksPageId($new_page_id) ); - $this->log->debug("Imported concluding remarks page: {$concluding_page_id} -> {$new_page_id}"); } $test_object->getMainSettingsRepository()->store($main_settings); $test_object->getScoreSettingsRepository()->store($scoring_settings); $this->marks_repository->storeMarkSchema($mark_schema); - $this->log->debug("Imported test settings and mark schema: {$settings_id} (Settings ID)"); } private function createPage(int $imported_page_id, int $parent_id, \ilImportMapping $mapping): int @@ -356,9 +337,9 @@ private function importQuestions( ): void { foreach ($list as $normalized) { $question = $this->questions_importer->importQuestion( - $normalized, - $transformations, - $mapping, + $normalized, + $transformations, + $mapping, $context->selectedQuestionIds() ); if (!($question instanceof \assQuestion) || $normalized['sequence'] === null) { @@ -367,11 +348,9 @@ private function importQuestions( $sequence = $transformations->int($normalized['sequence']); $test_object->questions[$sequence] = $question->getId(); - $this->log->debug("Stored question {$question->getId()} at sequence {$sequence} in test"); } $test_object->saveQuestionsToDb(); - $this->log->debug('Saved test questions to database'); } private function importQuestionSetConfig( @@ -398,7 +377,6 @@ private function importParticipants(array $list, Transformations $transformation $old_active_id = $transformations->denormalize($normalized['active_id'], Id::class)->getId(); $new_active_id = $this->database->nextId('tst_active'); $mapping->addMapping('components/ILIAS/Test', 'participant', (string) $old_active_id, (string) $new_active_id); - $this->log->debug("Stored participant/test session mapping: {$old_active_id} -> {$new_active_id}"); // The mapping processor replaces TestID, UserID and ActiveID $participant = $transformations->denormalize($normalized, Participant::class); @@ -422,7 +400,6 @@ private function importParticipants(array $list, Transformations $transformation 'start_lock' => [\ilDBConstants::T_TEXT, $transformations->nullableString($normalized['start_lock'])], ] ); - $this->log->debug("Stored test session in database: {$new_active_id} (Active ID)"); } } @@ -441,6 +418,5 @@ private function importInvitedParticipant(array $normalized, Transformations $tr 'tstamp' => [\ilDBConstants::T_INTEGER, $participant->getInvitationDate()], ] ); - $this->log->debug("Stored invited participant in database: {$participant->getUserId()} (User ID), {$participant->getTestId()} (Test ID)"); } } diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php index 0170e452da9d..6dcff8011a07 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestResultsImporter.php @@ -74,7 +74,6 @@ private function importTestSequences(array $list, Transformations $tt): void // The mapping processor replaces ActiveID and QuestionIDs $sequence = $tt->denormalize($normalized, \ilTestSequence::class); $sequence->saveToDb(); - $this->log->debug("Stored test sequence in database: {$sequence->getActiveId()} (Active ID), {$sequence->getPass()} (Pass)"); } } @@ -100,7 +99,6 @@ private function importQuestionAttempts(array $list, Transformations $tt): void 'tstamp' => [\ilDBConstants::T_INTEGER, time()], ] ); - $this->log->debug("Stored attempt in database: {$next_id}"); } } @@ -125,7 +123,6 @@ private function importQuestionResults(array $list, Transformations $tt): void 'step' => [\ilDBConstants::T_INTEGER, $result->step], ] ); - $this->log->debug("Stored question result in database: {$next_id}"); } } private function importAttemptResults(array $list, Transformations $tt): void @@ -149,7 +146,6 @@ private function importAttemptResults(array $list, Transformations $tt): void 'tstamp' => [\ilDBConstants::T_INTEGER, time()], ] ); - $this->log->debug("Stored attempt result in database: {$attempt->getActiveId()} (Active ID), {$attempt->getAttempt()} (Pass)"); } } @@ -177,7 +173,6 @@ private function importTestResult(?array $normalized, Transformations $tt): void 'tstamp' => [\ilDBConstants::T_INTEGER, time()], ] ); - $this->log->debug("Stored test result in database: {$result->getActiveId()} (Active ID), {$result->getAttempt()} (Pass)"); } private function importWorkingTimes(array $list, Transformations $tt): void @@ -198,7 +193,6 @@ private function importWorkingTimes(array $list, Transformations $tt): void 'tstamp' => [\ilDBConstants::T_INTEGER, time()], ] ); - $this->log->debug("Stored working time in database: {$next_id}"); } } @@ -223,7 +217,6 @@ private function importManualFeedback(array $list, Transformations $tt): void 'tstamp' => [\ilDBConstants::T_INTEGER, time()], ] ); - $this->log->debug("Stored manual feedback in database: {$next_id}"); } } @@ -233,7 +226,6 @@ private function importManualScoring(array $normalized, Transformations $tt): vo $active_id = $tt->denormalize($normalized['active_id'], Id::class)->getId(); (new TestManScoringDoneHelper())->setDone($active_id, $tt->bool($normalized['done'])); - $this->log->debug("Stored manual scoring in database: {$active_id} (Active ID)"); } public function importAdditionalWorkingTimes(array $list, Transformations $tt): void @@ -251,7 +243,6 @@ public function importAdditionalWorkingTimes(array $list, Transformations $tt): 'tstamp' => [\ilDBConstants::T_TIMESTAMP, $time->timestamp], ] ); - $this->log->debug("Stored additional working time in database: {$time->user_id->getId()} (User ID)"); } } @@ -274,7 +265,6 @@ private function importRandomTestQuestions(array $list, Transformations $tt): vo 'src_pool_def_fi' => [\ilDBConstants::T_INTEGER, $question->src_pool_def_id->getId()], ] ); - $this->log->debug("Stored random test question in database: {$next_id}"); } } } diff --git a/components/ILIAS/Test/src/ExportImport/Import/UserImportResolver.php b/components/ILIAS/Test/src/ExportImport/Import/UserImportResolver.php index 26730e45317b..90bce47d26b1 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/UserImportResolver.php +++ b/components/ILIAS/Test/src/ExportImport/Import/UserImportResolver.php @@ -39,8 +39,6 @@ public function __construct( */ public function resolve(UserIdentifiers $criteria, array $users): array { - $this->log->debug("Resolving user import for criteria {$criteria->value}"); - if (!$this->db->tableColumnExists('usr_data', $criteria->value)) { $this->log->error("User criteria field {$criteria->value} does not exist in usr_data table, using anonymous user ID for all users"); @@ -64,7 +62,6 @@ public function resolve(UserIdentifiers $criteria, array $users): array $mapping[$original_id] = ANONYMOUS_USER_ID; } - $this->log->debug("User identifier {$identifier} found, mapping user {$original_id} to {$db_mapping[$identifier]}"); $mapping[$original_id] = $db_mapping[$identifier]; } diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index d15736af2872..96081ea50bea 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -254,7 +254,6 @@ public function write(ExportState $state): void $file['from'], "{$export_dir}/" . $file['to'] ); - $state->logger()->debug("Copied question image {$file['from']} to {$export_dir}/{$file['to']}"); } foreach ($resource_collector->getResources() as $id => $resource) { @@ -265,7 +264,6 @@ public function write(ExportState $state): void $id, "{$export_dir}/resources/{$file}" ); - $state->logger()->debug("Copied resource {$id} to {$export_dir}/resources/{$file}"); } $this->writeMappings( @@ -273,7 +271,6 @@ public function write(ExportState $state): void $state->transformations(), $state ); - $state->logger()->debug('Stored test export mappings'); $state->logger()->info('...Finished writing test export (3/3)'); } diff --git a/components/ILIAS/Test/src/ExportImport/TransformationsBuilder.php b/components/ILIAS/Test/src/ExportImport/TransformationsBuilder.php index e73db3cd5aab..62725e71278b 100644 --- a/components/ILIAS/Test/src/ExportImport/TransformationsBuilder.php +++ b/components/ILIAS/Test/src/ExportImport/TransformationsBuilder.php @@ -22,7 +22,7 @@ use ILIAS\DI\Container; use ILIAS\Test\TestDIC; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FoundationNormalizerRegistration; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizerRegistration; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer\Registry; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\DenormalizingProcessor; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\FinalizeNormalizing; @@ -75,7 +75,7 @@ private function create( ]) ); - (new FoundationNormalizerRegistration($this->global_dic)) + (new NormalizerRegistration($this->global_dic)) ->register($registry, $transformations); (new QuestionPoolNormalizerRegistration($this->global_dic)) ->register($registry, $transformations); diff --git a/components/ILIAS/Test/src/TestDIC.php b/components/ILIAS/Test/src/TestDIC.php index 7bc482c5ea78..33f390e9caac 100755 --- a/components/ILIAS/Test/src/TestDIC.php +++ b/components/ILIAS/Test/src/TestDIC.php @@ -309,7 +309,6 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic['exportimport.random_test_config_importer'] = static fn($c): RandomTestConfigImporter => new RandomTestConfigImporter( $DIC->database(), - $c['exportimport.logging'](), new DataFactory() ); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php index afa3be86773c..a56f6189383a 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php @@ -133,13 +133,11 @@ public function write(ExportState $state): void throw new \LogicException('Unexpected question image processor'); } - $state->logger()->debug("Copying question images to export directory {$export_dir}"); foreach ($question_image_collector->getFiles() as $file) { $state->writer()->writeFileByFilePath( $file['from'], "{$export_dir}/" . $file['to'] ); - $state->logger()->debug("Copied question image {$file['from']} to {$export_dir}/{$file['to']}"); } $state->logger()->info('...Finished writing question pool export (3/3)'); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/XmlExporterBridge.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/XmlExporterBridge.php index a3d6e7962a7b..f3c4292da25d 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/XmlExporterBridge.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/XmlExporterBridge.php @@ -52,17 +52,13 @@ private function processExport(): ExportState $state = $this->state_holder->get(); if ($state->getStep()->value < ExportStep::PREPARE->value) { - $this->logger->debug("Preparing export for component {$state->target()->getComponent()}..."); $state->setLogger($this->logger); $this->exporter->prepare($state); - $this->logger->debug('...Finished preparing export'); } if ($state->getStep()->value < ExportStep::PROCESS->value) { - $this->logger->debug("Processing export for component {$state->target()->getComponent()}..."); $state->setSerializer(XmlSerializer::inMemory()); $this->exporter->process($state); - $this->logger->debug('...Finished processing export'); } $this->state_holder->set($state); @@ -81,9 +77,7 @@ private function finalizeExport(): ExportState $state->setPathInfo($this->createPathInfo()); $state->setWriter($this->exp->getExportWriter()); - $this->logger->debug("Writing export for component {$state->target()->getComponent()}..."); $this->exporter->write($state); - $this->logger->debug('...Finished writing export'); } $this->state_holder->clear(); @@ -111,16 +105,6 @@ private function initExportState( $option ); - $this->logger->debug(sprintf( - 'Export state created for component %s with release %s, type %s, class %s, object ids %s, option %s', - $target->getComponent(), - $target->getTargetRelease(), - $target->getType(), - $target->getClassname(), - implode(', ', $object_ids), - $option - )); - return $state; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Envelopes/Id.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Envelopes/Id.php index 299cd5ad856f..5c8104d65220 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Envelopes/Id.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Envelopes/Id.php @@ -20,31 +20,24 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes; +use ILIAS\Data\UUID\Uuid; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelope; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizingException; /** - * Wrapper object for an id (int, string, uuid, ...) and an optional object type the id belongs to. The wrapper can be - * used to detect ids within the normalization and denormalization pipes. - * - * @template T + * Wrapper object for an object identifier and an optional object type the identifier belongs to. + * The wrapper can be used to detect ids within the normalization and denormalization pipes. */ class Id implements Envelope { - /** - * @param T $id - */ public function __construct( - private mixed $id = null, + private int|Uuid|null $id = null, private string $object = '' ) { } - /** - * @return T - */ - public function getId(): mixed + public function getId(): int|Uuid|null { return $this->id; } @@ -57,19 +50,12 @@ public function getObject(): string #[\Override] public function unpack(Transformations $tt): array { - if (is_object($this->id)) { - return [ - 'id' => $tt->normalize($this->id), - 'type' => get_class($this->id), - 'object' => $this->object, - ]; - } - return [ - 'id' => (string) $this->id, - 'type' => gettype($this->id), + 'id' => $tt->normalize($this->id), + 'type' => $this->id instanceof Uuid ? Uuid::class : gettype($this->id), 'object' => $this->object, ]; + } #[\Override] @@ -78,15 +64,9 @@ public static function pack(array $value, Transformations $tt): static $raw_id = $value['id']; $type = $value['type']; - if (class_exists($type)) { - return new Id($tt->denormalize($raw_id, $type), $value['object']); - } - $id = match ($type) { + Uuid::class => $tt->denormalize($raw_id, Uuid::class), 'integer' => (int) $raw_id, - 'string' => (string) $raw_id, - 'float' => (float) $raw_id, - 'bool' => (bool) $raw_id, 'null' => null, default => throw new NormalizingException("Invalid type for id: {$type}") }; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/FoundationNormalizerRegistration.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/NormalizerRegistration.php similarity index 98% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/FoundationNormalizerRegistration.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/NormalizerRegistration.php index 6b1156015d37..488354cf878b 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/FoundationNormalizerRegistration.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/NormalizerRegistration.php @@ -35,9 +35,10 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer\TransformationNormalizer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer\UUIDNormalizer; -final class FoundationNormalizerRegistration +final class NormalizerRegistration { - public function __construct(private readonly Container $dic) { + public function __construct(private readonly Container $dic) + { } public function register(Registry $registry, Transformations $transformations): void diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Processors/CollectResources.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Processors/CollectResources.php index abb8bd407b89..ac146ce63188 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Processors/CollectResources.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Processors/CollectResources.php @@ -44,10 +44,9 @@ class CollectResources implements Processor private array $import_mapping = []; public function __construct( - private readonly IRSS $irss, + private readonly IRSS $irss, private readonly LoggerInterface $log - ) - { + ) { } /** diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php index 7d2c597efbe8..25c43b18dd94 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php @@ -31,7 +31,8 @@ */ class CleanupStage implements ImportStage { - public function __construct(private readonly LoggerInterface $log) { + public function __construct(private readonly LoggerInterface $log) + { } public function getIdentifier(): string @@ -55,7 +56,6 @@ public function process(ImportContext $context): StageResult $temp_dir = dirname($context->fileToImport()); if ($temp_dir !== '' && file_exists($temp_dir) && is_dir($temp_dir)) { $this->removeDirectory($temp_dir); - $this->log->info("Removed temporary import directory: {$temp_dir}"); } else { $this->log->warning("Temporary import directory does not exist: {$temp_dir}"); } @@ -63,7 +63,6 @@ public function process(ImportContext $context): StageResult if ($context->hasImportBaseDir() && is_dir($context->importBaseDir())) { $this->removeDirectory($context->importBaseDir()); - $this->log->info("Removed import target base directory: {$context->importBaseDir()}"); } else { $this->log->warning("Import target base directory does not exist: {$context->importBaseDir()}"); } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php index 9ff29038116d..6b944c348ecc 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php @@ -58,11 +58,9 @@ public function process(ImportContext $context): StageResult $qti_file = $context->importBaseDir() . DIRECTORY_SEPARATOR . str_replace(['_qpl_', '_tst_'], '_qti_', $import_name) . '.xml'; if (!file_exists($qti_file) || !file_exists($xml_file)) { - $this->log->debug("No legacy import files found for {$import_name}"); return StageResult::advance($context); } - $this->log->info("Detected legacy import files for {$import_name}"); return StageResult::advance( $context->withLegacyQtiFile($qti_file)->withLegacyXmlFile($xml_file) ); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php index 224cc9852b3f..491f2bd49a6e 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php @@ -102,20 +102,15 @@ function (array $assignments) use ($transformations, $mapping, &$context): void } ); - $this->log->info('Importing question pool export file...'); $deserializer->process(); - $this->log->info('...Finished importing question pool export file'); - $this->log->info('Importing question images...'); $this->questions_importer->importQuestionImages( $context->poolObjId(), $mapping, $context, $images_collector ); - $this->log->info('...Finished importing question images'); - $this->log->info("Finished importing question pool {$context->poolObjId()} (Object ID)"); return $context; } @@ -125,9 +120,7 @@ function (array $assignments) use ($transformations, $mapping, &$context): void */ public function finalize(\ilImportMapping $mapping): void { - $this->log->info('Finalizing question pool import...'); $this->questions_importer->finalizeQuestionPages($mapping); - $this->log->info('...Finished finalizing question pool'); } private function importQuestionPool( @@ -145,12 +138,10 @@ private function importQuestionPool( $pool_object->getObjectProperties()->getPropertyIsOnline()->withOffline() ); $pool_object->saveToDb(); - $this->log->debug("Created new pool object: {$old_pool_id} -> {$new_pool_id}"); $pool_object->createReference(); $pool_object->putInTree($parent_id->toInt()); $pool_object->setPermissions($parent_id->toInt()); - $this->log->debug("Stored pool object in tree: {$parent_id->toInt()} (Parent Ref) -> {$pool_object->getRefId()} (Pool Ref)"); $mapping->addMapping('components/ILIAS/TestQuestionPool', 'qpl', (string) $old_pool_id, (string) $new_pool_id); $mapping->addMapping('components/ILIAS/TestQuestionPool', 'object', (string) $old_pool_id, (string) $new_pool_id); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php index c7702dca3b4a..39fecbc9f0dc 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php @@ -142,7 +142,6 @@ function (array $questions) use (&$options): void { $deserializer->process(); $count = count($options); - $this->log->info("Found {$count} questions in import file"); return $options; } @@ -165,9 +164,6 @@ private function readQuestionsFromQTI(ImportContext $context): array $options[$item['ident']] = "{$item['title']} ({$this->getLabelForQuestionType($item['type'])})"; } - $count = count($options); - $this->log->info("Found {$count} questions in legacy import file"); - return $options; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php index 2e290c3b7d12..bd0867d1d60c 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php @@ -67,7 +67,6 @@ public function importQuestion( $question = $transformations->denormalize($normalized, new $question_class()); $old_question_id = $question->getId(); if (!in_array($old_question_id, $selected_questions)) { - $this->log->debug("Skipping question import for ID {$old_question_id} (not selected)"); return null; } @@ -77,7 +76,6 @@ public function importQuestion( // Create new question and store basic question properties $new_question_id = $question->createNewQuestion(false); - $this->log->debug("Created new question: {$old_question_id} -> {$new_question_id}"); $this->storeQuestionMappings($mapping, $old_question_id, $new_question_id, $question->getObjId()); if ($question instanceof \assFormulaQuestion) { @@ -86,7 +84,6 @@ public function importQuestion( // Save question-specific properties $question->saveToDb(); - $this->log->debug("Imported question {$new_question_id} (type: {$question->getQuestionType()})"); $feedback = $transformations->denormalize($normalized['feedback'], Feedback::class); $this->importFeedback($feedback, $question); @@ -119,7 +116,6 @@ public function importQuestionImages( $input_stream = Streams::ofReattachableResource(fopen($source_path, 'rb')); $this->filesystem->writeStream($image_path, $input_stream); - $this->log->debug("Imported question image: {$source_path} -> {$image_path}"); $thumbnail = $this->generateThumbnail($input_stream); if (!($thumbnail instanceof FileStream)) { @@ -128,7 +124,6 @@ public function importQuestionImages( $thumbnail_path = "{$image_base_path}/thumb.{$envelope->getFilename()}"; $this->filesystem->writeStream($thumbnail_path, $thumbnail); - $this->log->debug("Generated question image thumbnail: {$thumbnail_path}"); $thumbnail->close(); $input_stream->close(); @@ -181,7 +176,6 @@ public function finalizeQuestionPages(\ilImportMapping $mapping): void continue; } $new_question_id = $new_matches[1]; - $this->log->debug("Finalizing question page: {$old_question_id} -> {$new_question_id}"); $page = new \ilAssQuestionPage((int) $new_question_id); $xml = preg_replace( @@ -204,7 +198,6 @@ public function finalizeQuestionPages(\ilImportMapping $mapping): void } $page->updateFromXML(); - $this->log->debug("Updated question page: {$page->getId()}"); unset($page); } } @@ -261,8 +254,6 @@ private function importFeedback(Feedback $feedback, \assQuestion $question): voi $specific_feedback['feedback'] ); } - - $this->log->debug("Imported feedback for question: {$question_id}"); } private function importFormulaQuestion( @@ -280,7 +271,6 @@ private function importFormulaQuestion( $old_category_id = $category->getId(); $repository->saveNewUnitCategory($category); - $this->log->debug("Imported formula question unit category: {$old_category_id} -> {$category->getId()}"); $mapping->addMapping($this->component, 'unit_category', (string) $old_category_id, (string) $category->getId()); } @@ -296,7 +286,6 @@ private function importFormulaQuestion( $unit = $transformations->denormalize($normalized_unit, $unit); $repository->saveUnit($unit); - $this->log->debug("Imported formula question unit: {$old_unit_id} -> {$unit->getId()}"); } // The question object is denormalized again to ensure the new unit ids are set in the variables and results. diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php index 4f120d324c20..1510b59d6150 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/SkillAssignmentsImporter.php @@ -85,7 +85,6 @@ public function import( ); $assignment->setSkillBaseId($skill_data['skill_id']); $assignment->setSkillTrefId($skill_data['tref_id']); - $this->log->debug("Found skill assignment: {$assignment->getSkillBaseId()}/{$assignment->getSkillTrefId()} -> {$skill_data['skill_id']}/{$skill_data['tref_id']}"); $assignment->initSolutionComparisonExpressionList(); foreach ($assignment->getSolutionComparisonExpressionList()->get() as $expression) { @@ -95,7 +94,6 @@ public function import( $assignment->saveToDb(); $assignment->saveComparisonExpressions(); - $this->log->debug("Saved skill assignment: {$assignment->getSkillBaseId()}/{$assignment->getSkillTrefId()}"); $this->skill_usage_service->addUsage( $assignment->getParentObjId(), diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php index 5b5d8413595c..9186d36d2804 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php @@ -78,7 +78,6 @@ public function process(ImportContext $context): StageResult $handle = fopen($context->fileToImport(), 'r'); $unzip = $this->archives->unzip(Streams::ofResource($handle), $options); $unzip->extract(); - $this->log->info("Extracted import file: {$context->fileToImport()} -> {$import_base_dir}"); $manifest = new \ilManifestParser($import_base_dir . DIRECTORY_SEPARATOR . 'manifest.xml'); $export_file = array_find( @@ -92,8 +91,6 @@ public function process(ImportContext $context): StageResult } $component_import_file = $import_base_dir . DIRECTORY_SEPARATOR . $export_file['path']; - $this->log->info("Found export file for {$this->component}: -> {$component_import_file}"); - $this->log->info("Found valid export file from installation: {$manifest->getInstallId()}"); return StageResult::advance( $context diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/TransformationsBuilder.php b/components/ILIAS/TestQuestionPool/src/ExportImport/TransformationsBuilder.php index 73bae7963af5..b68394d4a853 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/TransformationsBuilder.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/TransformationsBuilder.php @@ -21,7 +21,7 @@ namespace ILIAS\TestQuestionPool\ExportImport; use ILIAS\DI\Container; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\FoundationNormalizerRegistration; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\NormalizerRegistration; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Normalizer\Registry; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\DenormalizingProcessor; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\FinalizeNormalizing; @@ -32,7 +32,8 @@ final class TransformationsBuilder { - public function __construct(private readonly Container $dic) { + public function __construct(private readonly Container $dic) + { } public function forExport(Processor ...$runtime_processors): Transformations @@ -70,7 +71,7 @@ private function create( ]) ); - (new FoundationNormalizerRegistration($this->dic)) + (new NormalizerRegistration($this->dic)) ->register($registry, $transformations); (new QuestionPoolNormalizerRegistration($this->dic)) ->register($registry, $transformations); From dc2fabd1cc86d5b9f3f765c722c96da5548ec268 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Thu, 22 Oct 2026 15:02:35 +0200 Subject: [PATCH 64/66] style: fix nested function calls --- .../src/ExportImport/Import/UserImportResolver.php | 5 ++++- .../classes/questions/class.ilAssOrderingElement.php | 10 ++-------- .../ExportImport/Foundation/Export/ExportState.php | 12 ++++++++---- .../Foundation/Normalize/Normalizer/Registry.php | 12 +++++++----- .../src/ExportImport/Foundation/Queue/Queue.php | 10 ++++++---- 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/components/ILIAS/Test/src/ExportImport/Import/UserImportResolver.php b/components/ILIAS/Test/src/ExportImport/Import/UserImportResolver.php index 90bce47d26b1..bf2d8bee8af7 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/UserImportResolver.php +++ b/components/ILIAS/Test/src/ExportImport/Import/UserImportResolver.php @@ -42,7 +42,10 @@ public function resolve(UserIdentifiers $criteria, array $users): array if (!$this->db->tableColumnExists('usr_data', $criteria->value)) { $this->log->error("User criteria field {$criteria->value} does not exist in usr_data table, using anonymous user ID for all users"); - return array_fill_keys(array_keys($users), ANONYMOUS_USER_ID); + return array_fill_keys( + array_keys($users), + ANONYMOUS_USER_ID + ); } $in_clause = $this->db->in( diff --git a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php index 737386041afa..7fe46cb562a8 100755 --- a/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php +++ b/components/ILIAS/TestQuestionPool/classes/questions/class.ilAssOrderingElement.php @@ -448,10 +448,7 @@ public function withContent(string $content): self } #[\Override] - public function toNormalized( - Transformations $transformations, - array $context = [] - ): array|float|bool|int|string|null + public function toNormalized(Transformations $transformations, array $context = []): array|float|bool|int|string|null { return [ 'id' => $transformations->normalize(new Id($this->id, 'ordering')), @@ -466,10 +463,7 @@ public function toNormalized( } #[\Override] - public function fromNormalized( - array $normalized, - Transformations $transformations - ): self + public function fromNormalized(array $normalized, Transformations $transformations): self { $clone = $this->withRandomIdentifier($transformations->int($normalized['random_identifier'])) ->withSolutionIdentifier($transformations->int($normalized['solution_identifier'])) diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportState.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportState.php index 8b61d05612ba..6f9550d2f7a4 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportState.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportState.php @@ -169,10 +169,14 @@ public function addDependency(string $component, string $entity, array $ids): vo ]; } - $this->dependencies[$key]['ids'] = array_values(array_unique(array_merge( - $this->dependencies[$key]['ids'], - $ids - ))); + $this->dependencies[$key]['ids'] = array_values( + array_unique( + array_merge( + $this->dependencies[$key]['ids'], + $ids + ) + ) + ); } public function getContent(): string diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/Registry.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/Registry.php index cecb071dd2c2..2fc019ac3610 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/Registry.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Normalize/Normalizer/Registry.php @@ -78,11 +78,13 @@ private function normalizerFor(string $type, ?string $legacy_version): ?Normaliz /** @return list */ private function candidateTypes(string $type): array { - $candidates = array_values(array_filter( - array_keys($this->type_map), - static fn(string $registered_type): bool => - $type === $registered_type || is_subclass_of($type, $registered_type) - )); + $candidates = array_values( + array_filter( + array_keys($this->type_map), + static fn(string $registered_type): bool => + $type === $registered_type || is_subclass_of($type, $registered_type) + ) + ); usort($candidates, static function (string $a, string $b): int { if ($a === $b) { diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Queue/Queue.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Queue/Queue.php index c24b1cd5154f..b08b650bd544 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Queue/Queue.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Queue/Queue.php @@ -42,10 +42,12 @@ public function process(object $carry): object public function get(string $class): Processor { - $matches = array_values(array_filter( - $this->processors, - static fn(Processor $processor): bool => $processor instanceof $class - )); + $matches = array_values( + array_filter( + $this->processors, + static fn(Processor $processor): bool => $processor instanceof $class + ) + ); if (count($matches) !== 1) { throw new \InvalidArgumentException( From 6c8f3cb9f93d320ea4cabda738cc90007ebeb30e Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Fri, 23 Oct 2026 10:47:44 +0200 Subject: [PATCH 65/66] refactor: rename export/import state/context/stage/result to provide clear SoC --- .../ILIAS/Test/classes/class.ilObjTestGUI.php | 32 ++- .../Test/classes/class.ilTestExporter.php | 14 +- .../Test/classes/class.ilTestImporter.php | 12 +- .../classes/class.ilTestLegacyImporter.php | 24 +-- .../ExportOptions/XMLWithResultsOption.php | 10 +- .../src/ExportImport/Import/PersistStage.php | 26 +-- .../src/ExportImport/Import/TestImporter.php | 42 ++-- .../Test/src/ExportImport/TestExporter.php | 184 +++++++++--------- components/ILIAS/Test/src/TestDIC.php | 6 +- .../classes/class.ilObjQuestionPoolGUI.php | 29 ++- .../class.ilTestQuestionPoolExporter.php | 16 +- .../class.ilTestQuestionPoolImporter.php | 12 +- ...class.ilTestQuestionPoolLegacyImporter.php | 24 +-- .../Export/QuestionPoolExporter.php | 100 +++++----- .../{ExportState.php => ExportContext.php} | 22 +-- ...tateHolder.php => ExportContextHolder.php} | 30 +-- .../{ExportStep.php => ExportPhase.php} | 10 +- .../Foundation/Export/Exporter.php | 8 +- .../Foundation/Export/XmlExporterBridge.php | 58 +++--- .../{ImportContext.php => ImportSession.php} | 62 +++--- .../Import/ImportSessionRepository.php | 14 +- .../Foundation/Import/ImportStage.php | 13 +- .../Foundation/Import/ImportStageRunner.php | 130 ------------- .../Foundation/Import/ImportWizard.php | 83 ++++++++ .../Foundation/Import/StageResult.php | 36 +--- .../Foundation/Import/StageResultType.php | 25 +-- .../src/ExportImport/Import/CleanupStage.php | 16 +- .../Import/DetectLegacyImportStage.php | 16 +- .../src/ExportImport/Import/PersistStage.php | 10 +- .../Import/QuestionPoolImporter.php | 26 +-- .../Import/QuestionSelectionStage.php | 34 ++-- .../ExportImport/Import/QuestionsImporter.php | 6 +- .../Import/UploadValidationStage.php | 24 +-- .../TestQuestionPool/src/QuestionPoolDIC.php | 6 +- ...erTest.php => ExportContextHolderTest.php} | 12 +- .../ExportImportCharacterizationTest.php | 30 +-- 36 files changed, 555 insertions(+), 647 deletions(-) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/{ExportState.php => ExportContext.php} (89%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/{StateHolder.php => ExportContextHolder.php} (54%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/{ExportStep.php => ExportPhase.php} (85%) rename components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/{ImportContext.php => ImportSession.php} (84%) delete mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportStageRunner.php create mode 100644 components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportWizard.php rename components/ILIAS/TestQuestionPool/tests/ExportImport/{StateHolderTest.php => ExportContextHolderTest.php} (82%) diff --git a/components/ILIAS/Test/classes/class.ilObjTestGUI.php b/components/ILIAS/Test/classes/class.ilObjTestGUI.php index e17262eca59f..3aab97229f3d 100755 --- a/components/ILIAS/Test/classes/class.ilObjTestGUI.php +++ b/components/ILIAS/Test/classes/class.ilObjTestGUI.php @@ -69,9 +69,9 @@ use ILIAS\Test\Settings\Templates\PersonalSettingsTableShowAction; use ILIAS\Test\TestDIC; use ILIAS\Test\Utilities\TitleColumnsBuilder; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSession; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSessionRepository; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportStageRunner; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportWizard; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\StageResultType; use ILIAS\TestQuestionPool\ExportImport\Import\CleanupStage; use ILIAS\TestQuestionPool\ExportImport\Import\DetectLegacyImportStage; @@ -1394,8 +1394,8 @@ protected function importFile(string $file_to_import, string $path_to_uploaded_f { $this->import_session_repository->clear(); - $context = (new ImportContext())->withFileToImport($file_to_import); - $this->import_session_repository->setContext($context); + $session = (new ImportSession())->withFileToImport($file_to_import); + $this->import_session_repository->setSession($session); $this->import_session_repository->setCurrentStageIndex(0); $this->ctrl->redirectByClass(self::class, 'processImport'); @@ -1408,20 +1408,16 @@ public function processImportObject(): void return; } - $result = $this->buildImportStageRunner()->run(); + $result = $this->buildImportWizard()->run(); switch ($result->type) { - case StageResultType::INTERACT: + case StageResultType::RequiresUserInteraction: $this->tpl->setContent( $this->ui_renderer->render($result->components) ); break; - case StageResultType::ADVANCE: - $this->ctrl->redirectByClass(self::class, 'processImport'); - break; - - case StageResultType::ERROR: + case StageResultType::Error: $this->tpl->setOnScreenMessage( GlobalTemplate::MESSAGE_TYPE_FAILURE, $result->error_message, @@ -1429,17 +1425,17 @@ public function processImportObject(): void ); break; - case StageResultType::COMPLETE: - $this->afterImportCompleted($result->context); + case StageResultType::CompletedSuccessfully: + $this->afterImportCompleted($result->session); break; } } - private function afterImportCompleted(ImportContext $context): void + private function afterImportCompleted(ImportSession $session): void { $new_obj = new ilObjTest(0, false); - $new_obj->setId($context->testObjId() ?? 0); - $new_obj->setRefId($context->testRefId() ?? 0); + $new_obj->setId($session->testObjId() ?? 0); + $new_obj->setRefId($session->testRefId() ?? 0); if ($new_obj->getTestLogger()->isLoggingEnabled()) { $new_obj->getTestLogger()->logTestAdministrationInteraction( @@ -1470,9 +1466,9 @@ private function afterImportCompleted(ImportContext $context): void $this->ctrl->redirectByClass(self::class, self::SHOW_QUESTIONS_CMD); } - private function buildImportStageRunner(): ImportStageRunner + private function buildImportWizard(): ImportWizard { - return new ImportStageRunner( + return new ImportWizard( [ new UploadValidationStage( $this->archives, diff --git a/components/ILIAS/Test/classes/class.ilTestExporter.php b/components/ILIAS/Test/classes/class.ilTestExporter.php index e66ade8befa0..4d04bdb2816c 100755 --- a/components/ILIAS/Test/classes/class.ilTestExporter.php +++ b/components/ILIAS/Test/classes/class.ilTestExporter.php @@ -21,7 +21,7 @@ use ILIAS\Export\ExportHandler\Factory as ExportHandler; use ILIAS\Test\ExportImport\Types; use ILIAS\Test\TestDIC; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportStep; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportPhase; use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\XmlExporterBridge; class ilTestExporter extends ilXmlExporter @@ -33,7 +33,7 @@ public function init(): void $local_dic = TestDIC::dic(); $this->export_handler = new ExportHandler(); - $this->state_holder = $local_dic['exportimport.state_holder']; + $this->context_holder = $local_dic['exportimport.export_context_holder']; $this->exporter = $local_dic['exportimport.exporter']; $this->logger = $local_dic['exportimport.logging'](); } @@ -57,7 +57,7 @@ public function getXmlRepresentation(string $a_entity, string $a_schema_version, * Collects export tail dependencies for the test. * * The export framework calls this method before `getXmlRepresentation()`. Therefore this method only prepares and - * processes the export in memory using the export state. The export state is created if it does not exist yet. + * processes the export in memory using the export context. The export context is created if it does not exist yet. */ public function getXmlExportTailDependencies(string $a_entity, string $a_target_release, array $a_ids): array { @@ -65,12 +65,12 @@ public function getXmlExportTailDependencies(string $a_entity, string $a_target_ throw new InvalidArgumentException("Invalid entity for test export: {$a_entity}"); } - // If the default export option was used, the state is not initialized yet. + // If the default export option was used, the context is not initialized yet. if ( - !$this->state_holder->exists() - || $this->state_holder->get()->getStep() !== ExportStep::INIT + !$this->context_holder->exists() + || $this->context_holder->get()->getPhase() !== ExportPhase::Init ) { - $this->initExportState( + $this->initExportContext( 'components/ILIAS/Test', $a_target_release, $a_entity, diff --git a/components/ILIAS/Test/classes/class.ilTestImporter.php b/components/ILIAS/Test/classes/class.ilTestImporter.php index fa395dc680f3..523af1a7ed87 100755 --- a/components/ILIAS/Test/classes/class.ilTestImporter.php +++ b/components/ILIAS/Test/classes/class.ilTestImporter.php @@ -59,8 +59,8 @@ public function importXmlRepresentation( ilImportMapping $a_mapping ): void { // Check if forward to legacy importer is needed - $context = $this->session->getContext(); - if ($context->isLegacyImport()) { + $session = $this->session->getSession(); + if ($session->isLegacyImport()) { $this->legacy_importer->setInstallId($this->getInstallId()); $this->legacy_importer->setInstallUrl($this->getInstallUrl()); $this->legacy_importer->setSchemaVersion($this->getSchemaVersion()); @@ -73,16 +73,16 @@ public function importXmlRepresentation( XmlDeserializer::fromString($a_xml), $a_mapping, new ReferenceId($a_mapping->getTargetId()), - $context, + $session, ); - $this->session->setContext($result); + $this->session->setSession($result); } public function finalProcessing(ilImportMapping $a_mapping): void { // Check if forward to legacy importer is needed - $context = $this->session->getContext(); - if ($context->isLegacyImport()) { + $session = $this->session->getSession(); + if ($session->isLegacyImport()) { $this->legacy_importer->finalProcessing($a_mapping); return; } diff --git a/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php b/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php index a52f5fa45fb3..120a9c22b7fa 100755 --- a/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php +++ b/components/ILIAS/Test/classes/class.ilTestLegacyImporter.php @@ -72,15 +72,15 @@ public function importXmlRepresentation( $a_mapping->addMapping('components/ILIAS/Test', 'tst', 'new_id', (string) $new_obj->getId()); - $context = $this->session->getContext(); + $session = $this->session->getSession(); // start parsing of QTI files $qti_parser = new ilQTIParser( - $context->importBaseDir(), - $context->legacyQtiFile(), + $session->importBaseDir(), + $session->legacyQtiFile(), ilQTIParser::IL_MO_PARSE_QTI, $new_obj->getId(), - $context->selectedQuestionIds(), + $session->selectedQuestionIds(), $a_mapping->getAllMappings() ); $qti_parser->setTestObject($new_obj); @@ -90,8 +90,8 @@ public function importXmlRepresentation( // import page data $question_page_parser = new ilQuestionPageParser( $new_obj, - $context->legacyXmlFile(), - $context->importBaseDir() + $session->legacyXmlFile(), + $session->importBaseDir() ); $question_page_parser->setQuestionMapping($qti_parser->getImportMapping()); $question_page_parser->startParsing(); @@ -99,10 +99,10 @@ public function importXmlRepresentation( $a_mapping = $this->addTaxonomyAndQuestionsMapping($qti_parser->getQuestionIdMapping(), $new_obj->getId(), $a_mapping); if ($new_obj->isRandomTest()) { - $this->importRandomQuestionSetConfig($new_obj, $context->legacyXmlFile(), $a_mapping); + $this->importRandomQuestionSetConfig($new_obj, $session->legacyXmlFile(), $a_mapping); } - $results_file_path = str_replace('__tst', '__results', $context->legacyXmlFile()); + $results_file_path = str_replace('__tst', '__results', $session->legacyXmlFile()); if (file_exists($results_file_path)) { $results = new ilTestResultsImportParser($results_file_path, $new_obj, $this->db, $this->logger, $this->irss); $results->setQuestionIdMapping($a_mapping->getMappingsOfEntity('components/ILIAS/Test', 'quest')); @@ -115,9 +115,9 @@ public function importXmlRepresentation( $this->importSkillLevelThresholds( $a_mapping, - $this->importQuestionSkillAssignments($a_mapping, $new_obj, $context->legacyXmlFile()), + $this->importQuestionSkillAssignments($a_mapping, $new_obj, $session->legacyXmlFile()), $new_obj, - $context->legacyXmlFile() + $session->legacyXmlFile() ); $a_mapping->addMapping( @@ -127,8 +127,8 @@ public function importXmlRepresentation( "{$new_obj->getId()}:0:tst" ); - $context = $context->withTestObjId($new_obj->getId())->withTestRefId($new_obj->getRefId()); - $this->session->setContext($context); + $session = $session->withTestObjId($new_obj->getId())->withTestRefId($new_obj->getRefId()); + $this->session->setSession($session); } public function addTaxonomyAndQuestionsMapping( diff --git a/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php b/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php index 487e0d1b06ef..2261c29f4820 100644 --- a/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php +++ b/components/ILIAS/Test/src/ExportImport/ExportOptions/XMLWithResultsOption.php @@ -27,12 +27,12 @@ use ILIAS\Language\Language; use ILIAS\Test\ExportImport\Types; use ILIAS\Test\TestDIC; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\StateHolder; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportContextHolder; class XMLWithResultsOption extends BasicLegacyExportOption { private readonly Language $lng; - private readonly StateHolder $state_holder; + private readonly ExportContextHolder $context_holder; public function init( Container $DIC @@ -40,7 +40,7 @@ public function init( parent::init($DIC); $this->lng = $DIC->language(); - $this->state_holder = TestDIC::dic()['exportimport.state_holder']; + $this->context_holder = TestDIC::dic()['exportimport.export_context_holder']; } public function getExportType(): string @@ -77,8 +77,8 @@ public function onExportOptionSelected(ConsumerContext $context): void $handler->consumer()->exportConfig()->allExportConfigs() ); - // Prepare export state to bridge between export option and the xml exporter - $this->state_holder->create( + // Prepare export context to bridge between export option and the xml exporter + $this->context_holder->create( $export_info->getTarget(), $handler->consumer()->exportConfig()->allExportConfigs(), Types::XML_WITH_RESULTS->value diff --git a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php index 05cf0d49c203..47451ebc3699 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php +++ b/components/ILIAS/Test/src/ExportImport/Import/PersistStage.php @@ -22,7 +22,7 @@ use ILIAS\Language\Language; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportStage; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSession; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\StageResult; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\XmlDeserializer; @@ -58,10 +58,10 @@ public function getDescription(): ?string return ''; } - public function process(ImportContext $context): StageResult + public function process(ImportSession $session): StageResult { - if (!$context->isLegacyImport()) { - $result = $this->importMappingsFile($context); + if (!$session->isLegacyImport()) { + $result = $this->importMappingsFile($session); if ($result !== null) { return $result; } @@ -69,42 +69,42 @@ public function process(ImportContext $context): StageResult (new \ilImport($this->requested_ref_id))->importObject( null, - $context->fileToImport(), - basename($context->fileToImport()), + $session->fileToImport(), + basename($session->fileToImport()), 'tst', 'components/ILIAS/Test', true, ); // Context is updated by the TestImporter so we need to reload it - return StageResult::complete($this->session->getContext()); + return StageResult::completedSuccessfully($this->session->getSession()); } - private function importMappingsFile(ImportContext $context): ?StageResult + private function importMappingsFile(ImportSession $session): ?StageResult { - $component_import_dir = dirname($context->componentImportFile()); + $component_import_dir = dirname($session->componentImportFile()); $mappings_file = "{$component_import_dir}/mappings.xml"; if (!is_file($mappings_file)) { $this->log->error("Mappings file not found: {$mappings_file}"); - return StageResult::error($context, $this->lng->txt('obj_import_file_error')); + return StageResult::error($session, $this->lng->txt('obj_import_file_error')); } $deserializer = XmlDeserializer::fromFile($mappings_file); - $deserializer->addHandler('mappings', function (array $mappings) use (&$context): void { + $deserializer->addHandler('mappings', function (array $mappings) use (&$session): void { $user_mappings = $mappings[0] ?? null; $resource_mappings = $mappings[1] ?? []; if (!is_array($user_mappings) || !is_array($resource_mappings)) { return; } - $context = $context + $session = $session ->withUserMappings($user_mappings) ->withResourceMappings($resource_mappings); }); $deserializer->process(); - $this->session->setContext($context); + $this->session->setSession($session); return null; } } diff --git a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php index 7ec64546dedc..91d7a9c1ba80 100644 --- a/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php +++ b/components/ILIAS/Test/src/ExportImport/Import/TestImporter.php @@ -35,7 +35,7 @@ use ILIAS\Test\ExportImport\TransformationsBuilder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\Deserializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSession; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\CollectResources; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\IdMappingProcessor; @@ -73,8 +73,8 @@ public function import( Deserializer $deserializer, \ilImportMapping $mapping, ReferenceId $parent_id, - ImportContext $context - ): ImportContext { + ImportSession $session + ): ImportSession { $resource_collector = new CollectResources($this->irss, $this->log); $question_images_collector = new CollectQuestionImages(new UUIDFactory(), $this->data_factory->objId(0)); @@ -114,12 +114,12 @@ function (array $settings) use ($transformations, $mapping, &$test_object): void $deserializer->addHandler( 'questions', - function (array $normalized) use ($transformations, $mapping, $context, &$test_object): void { + function (array $normalized) use ($transformations, $mapping, $session, &$test_object): void { $this->importQuestions( $normalized, $transformations, $mapping, - $context, + $session, $test_object ); } @@ -127,7 +127,7 @@ function (array $normalized) use ($transformations, $mapping, $context, &$test_o $deserializer->addHandler( 'question_set_config', - function (array $normalized) use ($transformations, $mapping, $context, &$test_object): void { + function (array $normalized) use ($transformations, $mapping, $session, &$test_object): void { $this->importQuestionSetConfig( reset($normalized), $transformations, @@ -139,27 +139,27 @@ function (array $normalized) use ($transformations, $mapping, $context, &$test_o $deserializer->addHandler( 'skill_assignments', - function (array $assignments) use ($transformations, $mapping, &$context): void { + function (array $assignments) use ($transformations, $mapping, &$session): void { $result = $this->skill_importer->import( $assignments, - $context->installId(), + $session->installId(), $transformations, $mapping, ); - $context = $context->withSkillAssignments($result); + $session = $session->withSkillAssignments($result); } ); $deserializer->addHandler( 'skill_thresholds', - function (array $thresholds) use ($transformations, $mapping, &$context): void { + function (array $thresholds) use ($transformations, $mapping, &$session): void { $result = $this->skill_thresholds_importer->import( $thresholds, - $context->installId(), + $session->installId(), $transformations, $mapping, ); - $context = $context->withSkillThresholds($result); + $session = $session->withSkillThresholds($result); } ); @@ -194,18 +194,18 @@ function (array $times) use ($transformations): void { } ); - $this->importMappings($mapping, $resource_collector, $context); + $this->importMappings($mapping, $resource_collector, $session); $deserializer->process(); $this->questions_importer->importQuestionImages( $test_object->getId(), $mapping, - $context, + $session, $question_images_collector ); - return $context->withTestObjId($test_object->getId())->withTestRefId($test_object->getRefId()); + return $session->withTestObjId($test_object->getId())->withTestRefId($test_object->getRefId()); } /** @@ -223,9 +223,9 @@ public function finalize(\ilImportMapping $mapping): void private function importMappings( \ilImportMapping $mapping, CollectResources $resource_collector, - ImportContext $context + ImportSession $session ): void { - $user_mapping = $context->userMappings(); + $user_mapping = $session->userMappings(); $user_resolver = new UserImportResolver($this->database, $this->log); $imported_users = $user_resolver->resolve( @@ -234,8 +234,8 @@ private function importMappings( ); $user_resolver->store($imported_users, $mapping); - $import_dir = dirname($context->componentImportFile()) . '/expDir_1'; - foreach ($context->resourceMappings() as $resource) { + $import_dir = dirname($session->componentImportFile()) . '/expDir_1'; + foreach ($session->resourceMappings() as $resource) { $clean_id = str_replace(['-', '_'], '', $resource['id']); $resource_path = "$import_dir/resources/{$clean_id}.{$resource['suffix']}"; if (!file_exists($resource_path)) { @@ -332,7 +332,7 @@ private function importQuestions( array $list, Transformations $transformations, \ilImportMapping $mapping, - ImportContext $context, + ImportSession $session, \ilObjTest $test_object ): void { foreach ($list as $normalized) { @@ -340,7 +340,7 @@ private function importQuestions( $normalized, $transformations, $mapping, - $context->selectedQuestionIds() + $session->selectedQuestionIds() ); if (!($question instanceof \assQuestion) || $normalized['sequence'] === null) { continue; diff --git a/components/ILIAS/Test/src/ExportImport/TestExporter.php b/components/ILIAS/Test/src/ExportImport/TestExporter.php index 96081ea50bea..4847f343351d 100644 --- a/components/ILIAS/Test/src/ExportImport/TestExporter.php +++ b/components/ILIAS/Test/src/ExportImport/TestExporter.php @@ -32,8 +32,8 @@ use ILIAS\Test\Participants\ParticipantRepository; use ILIAS\Test\Questions\Properties\Repository as QuestionsRepository; use ILIAS\Test\Results\Data\Repository as ResultsRepository; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportStep; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportState; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportPhase; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\Exporter; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\Serializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; @@ -61,13 +61,13 @@ public function __construct( ) { } - public function prepare(ExportState $state): void + public function prepare(ExportContext $context): void { - $state->logger()->info('Preparing test export (1/3)...'); - $state->assertStep(ExportStep::INIT); - $state->setStep(ExportStep::PREPARE); + $context->logger()->info('Preparing test export (1/3)...'); + $context->assertPhase(ExportPhase::Init); + $context->setPhase(ExportPhase::Prepare); - $object_id = $this->extractObjectId($state); + $object_id = $this->extractObjectId($context); if ($object_id === null) { return; } @@ -84,7 +84,7 @@ public function prepare(ExportState $state): void $this->component_repository, $object_id ); - $state->setCollector($collector); + $context->setCollector($collector); $transformations = $this->builder->forExport( new CollectUserIds(), @@ -98,21 +98,21 @@ public function prepare(ExportState $state): void ) ); - $state->setTransformations($transformations); - $state->logger()->info('...Finished preparing test export (1/3)'); + $context->setTransformations($transformations); + $context->logger()->info('...Finished preparing test export (1/3)'); } - private function extractObjectId(ExportState $state): ?ObjectId + private function extractObjectId(ExportContext $context): ?ObjectId { - $target_ids = $state->target()->getObjectIds(); + $target_ids = $context->target()->getObjectIds(); if ($target_ids === []) { - $state->logger()->warning('No target object IDs found for test export'); + $context->logger()->warning('No target object IDs found for test export'); return null; } if (count($target_ids) > 1) { - $state->logger()->warning( + $context->logger()->warning( 'Multiple target object IDs found for test export. Only the first one will be used.' ); } @@ -120,9 +120,9 @@ private function extractObjectId(ExportState $state): ?ObjectId return $this->data_factory->objId(array_shift($target_ids)); } - private function collector(ExportState $state): TestCollector + private function collector(ExportContext $context): TestCollector { - $collector = $state->collector(); + $collector = $context->collector(); if (!($collector instanceof TestCollector)) { throw new \LogicException('Unexpected test collector'); } @@ -130,115 +130,115 @@ private function collector(ExportState $state): TestCollector return $collector; } - public function process(ExportState $state): void + public function process(ExportContext $context): void { - $state->logger()->info('Processing test export (2/3)...'); - $state->assertStep(ExportStep::PREPARE); - $state->setStep(ExportStep::PROCESS); + $context->logger()->info('Processing test export (2/3)...'); + $context->assertPhase(ExportPhase::Prepare); + $context->setPhase(ExportPhase::Process); - $state->serializer()->group( + $context->serializer()->group( 'general', fn() => $this->exportObject( - $this->collector($state), - $state->transformations(), - $state->serializer(), - $state + $this->collector($context), + $context->transformations(), + $context->serializer(), + $context ) ); - $state->serializer()->group( + $context->serializer()->group( 'settings', fn() => $this->exportSettings( - $this->collector($state), - $state->transformations(), - $state->serializer(), - $state + $this->collector($context), + $context->transformations(), + $context->serializer(), + $context ) ); - $state->serializer()->group( + $context->serializer()->group( 'questions', fn() => $this->exportQuestions( - $this->collector($state), - $state->transformations(), - $state->serializer(), - $state + $this->collector($context), + $context->transformations(), + $context->serializer(), + $context ) ); - $state->serializer()->group( + $context->serializer()->group( 'question_set_config', fn() => $this->exportQuestionSetConfig( - $this->collector($state), - $state->transformations(), - $state->serializer(), + $this->collector($context), + $context->transformations(), + $context->serializer(), ) ); - $state->serializer()->group( + $context->serializer()->group( 'additional_working_times', fn() => $this->exportAdditionalWorkingTimes( - $this->collector($state), - $state->transformations(), - $state->serializer(), + $this->collector($context), + $context->transformations(), + $context->serializer(), ) ); - $state->serializer()->group( + $context->serializer()->group( 'skill_assignments', fn() => $this->exportSkillAssignments( - $this->collector($state), - $state->transformations(), - $state->serializer(), + $this->collector($context), + $context->transformations(), + $context->serializer(), ) ); - $state->serializer()->group( + $context->serializer()->group( 'skill_thresholds', fn() => $this->exportSkillLevelThresholds( - $this->collector($state), - $state->transformations(), - $state->serializer(), + $this->collector($context), + $context->transformations(), + $context->serializer(), ) ); - if ($state->getOption() === Types::XML_WITH_RESULTS->value) { - $state->logger()->info('Processing test results export ...'); - $this->processResults($state); - $state->logger()->info('...Finished processing test results export'); + if ($context->getOption() === Types::XML_WITH_RESULTS->value) { + $context->logger()->info('Processing test results export ...'); + $this->processResults($context); + $context->logger()->info('...Finished processing test results export'); } - $state->logger()->info('...Finished processing test export (2/3)'); + $context->logger()->info('...Finished processing test export (2/3)'); } - private function processResults(ExportState $state): void + private function processResults(ExportContext $context): void { - $state->serializer()->group( + $context->serializer()->group( 'participants', fn() => $this->exportParticipants( - $this->collector($state), - $state->transformations(), - $state->serializer() + $this->collector($context), + $context->transformations(), + $context->serializer() ) ); - $state->serializer()->group( + $context->serializer()->group( 'results', fn() => $this->exportResults( - $this->collector($state), - $state->transformations(), - $state->serializer() + $this->collector($context), + $context->transformations(), + $context->serializer() ) ); } - public function write(ExportState $state): void + public function write(ExportContext $context): void { - $state->logger()->info('Writing test export (3/3)...'); - $state->assertStep(ExportStep::PROCESS); - $state->setStep(ExportStep::WRITE); + $context->logger()->info('Writing test export (3/3)...'); + $context->assertPhase(ExportPhase::Process); + $context->setPhase(ExportPhase::Write); - $export_dir = $state->path()->getPathToComponentExpDirInContainer(); - $question_image_collector = $state->transformations() + $export_dir = $context->path()->getPathToComponentExpDirInContainer(); + $question_image_collector = $context->transformations() ->normalizationProcessor(CollectQuestionImages::class); if (!($question_image_collector instanceof CollectQuestionImages)) { throw new \LogicException('Unexpected question image processor'); } - $resource_collector = $state->transformations() + $resource_collector = $context->transformations() ->normalizationProcessor(CollectResources::class); if (!($resource_collector instanceof CollectResources)) { throw new \LogicException('Unexpected resource processor'); @@ -246,11 +246,11 @@ public function write(ExportState $state): void foreach ($question_image_collector->getFiles() as $file) { if (!file_exists($file['from'])) { - $state->logger()->warning('Question image file not found: ' . $file['from']); + $context->logger()->warning('Question image file not found: ' . $file['from']); continue; } - $state->writer()->writeFileByFilePath( + $context->writer()->writeFileByFilePath( $file['from'], "{$export_dir}/" . $file['to'] ); @@ -260,19 +260,19 @@ public function write(ExportState $state): void $clean_id = str_replace(['-', '_'], '', $id); $file = "{$clean_id}.{$resource->getCurrentRevision()->getInformation()->getSuffix()}"; - $state->writer()->writeFilesByResourceId( + $context->writer()->writeFilesByResourceId( $id, "{$export_dir}/resources/{$file}" ); } $this->writeMappings( - $this->collector($state), - $state->transformations(), - $state + $this->collector($context), + $context->transformations(), + $context ); - $state->logger()->info('...Finished writing test export (3/3)'); + $context->logger()->info('...Finished writing test export (3/3)'); } @@ -280,26 +280,26 @@ private function exportObject( TestCollector $collector, Transformations $transformations, Serializer $serializer, - ExportState $state + ExportContext $context ): void { $serializer->append('object', $transformations->normalize($collector->getObject())); $obj_id = $collector->getObjectId()->toInt(); - $state->addDependency('components/ILIAS/ILIASObject', 'common', [$obj_id]); - $state->addDependency('components/ILIAS/MetaData', 'tst', ["{$obj_id}:0:tst"]); - $state->addDependency( + $context->addDependency('components/ILIAS/ILIASObject', 'common', [$obj_id]); + $context->addDependency('components/ILIAS/MetaData', 'tst', ["{$obj_id}:0:tst"]); + $context->addDependency( 'components/ILIAS/Taxonomy', 'tax', $this->taxonomy->getUsageOfObject($obj_id) ); - $state->addDependency('components/ILIAS/Tracking', 'lpsettings', [$obj_id]); + $context->addDependency('components/ILIAS/Tracking', 'lpsettings', [$obj_id]); } private function exportSettings( TestCollector $collector, Transformations $transformations, Serializer $serializer, - ExportState $state + ExportContext $context ): void { $test = $collector->getObject(); $main_settings = $test->getMainSettings(); @@ -310,11 +310,11 @@ private function exportSettings( $intro_page_id = $main_settings->getIntroductionSettings()->getIntroductionPageId(); if ($intro_page_id !== null && $intro_page_id !== 0) { - $state->addDependency('components/ILIAS/COPage', 'pg', ["tst:{$intro_page_id}"]); + $context->addDependency('components/ILIAS/COPage', 'pg', ["tst:{$intro_page_id}"]); } $concluding_page_id = $main_settings->getFinishingSettings()->getConcludingRemarksPageId(); if ($concluding_page_id !== null && $concluding_page_id !== 0) { - $state->addDependency('components/ILIAS/COPage', 'pg', ["tst:{$concluding_page_id}"]); + $context->addDependency('components/ILIAS/COPage', 'pg', ["tst:{$concluding_page_id}"]); } } @@ -322,7 +322,7 @@ private function exportQuestions( TestCollector $collector, Transformations $transformations, Serializer $serializer, - ExportState $state + ExportContext $context ): void { $question_properties = $collector->getTestQuestionProperties(); @@ -341,7 +341,7 @@ private function exportQuestions( } $serializer->append('question', $normalized); - $state->addDependency('components/ILIAS/COPage', 'pg', ["qpl:{$question->getId()}"]); + $context->addDependency('components/ILIAS/COPage', 'pg', ["qpl:{$question->getId()}"]); } } @@ -421,7 +421,7 @@ private function exportAdditionalWorkingTimes( private function writeMappings( TestCollector $collector, Transformations $transformations, - ExportState $state + ExportContext $context ): void { $serializer = XmlSerializer::inMemory(); $serializer->createDocument('Test Export Mappings'); @@ -446,9 +446,9 @@ private function writeMappings( $serializer->endGroup('mappings'); - $state->writer()->writeFileByStream( + $context->writer()->writeFileByStream( Streams::ofString($serializer->write()), - "{$state->path()->getPathToComponentDirInContainer()}/mappings.xml" + "{$context->path()->getPathToComponentDirInContainer()}/mappings.xml" ); } } diff --git a/components/ILIAS/Test/src/TestDIC.php b/components/ILIAS/Test/src/TestDIC.php index 33f390e9caac..9ba14bd1a28b 100755 --- a/components/ILIAS/Test/src/TestDIC.php +++ b/components/ILIAS/Test/src/TestDIC.php @@ -53,7 +53,7 @@ use ILIAS\Test\Questions\Properties\DatabaseRepository as TestQuestionsDatabaseRepository; use ILIAS\Test\Results\Data\Factory as ResultsDataFactory; use ILIAS\Test\Results\Presentation\Factory as ResultsPresentationFactory; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\StateHolder; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportContextHolder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionsImporter; use ILIAS\TestQuestionPool\ExportImport\Import\SkillAssignmentsImporter; @@ -244,8 +244,8 @@ protected static function buildDIC(ILIASContainer $DIC): self $dic['exportimport.logging'] = static fn($c): LoggingProvider => new LoggingProvider(); - $dic['exportimport.state_holder'] = static fn($c): StateHolder => - new StateHolder(); + $dic['exportimport.export_context_holder'] = static fn($c): ExportContextHolder => + new ExportContextHolder(); $dic['exportimport.session'] = static fn($c): ImportSessionRepository => new ImportSessionRepository('tst'); diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php index 265daf50283a..607880181bdb 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilObjQuestionPoolGUI.php @@ -26,9 +26,9 @@ use ILIAS\Style\Content\Service as ContentStyle; use ILIAS\Taxonomy\Service; use ILIAS\Test\Settings\GlobalSettings\GlobalTestSettings; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSession; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSessionRepository; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportStageRunner; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportWizard; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\StageResult; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\StageResultType; use ILIAS\TestQuestionPool\ExportImport\Import\CleanupStage; @@ -1120,8 +1120,8 @@ protected function importFile(string $file_to_import, string $path_to_uploaded_f { $this->import_session_repository->clear(); - $context = (new ImportContext())->withFileToImport($file_to_import); - $this->import_session_repository->setContext($context); + $session = (new ImportSession())->withFileToImport($file_to_import); + $this->import_session_repository->setSession($session); $this->import_session_repository->setCurrentStageIndex(0); $this->ctrl->redirectByClass(self::class, 'processImport'); @@ -1135,22 +1135,20 @@ public function processImportObject(): void return; } - $runner = $this->buildImportStageRunner(); - $result = $runner->run(); + $result = $this->buildImportWizard()->run(); match ($result->type) { - StageResultType::INTERACT => $this->renderImportStage($runner, $result), - StageResultType::ADVANCE => $this->ctrl->redirectByClass(self::class, 'processImport'), - StageResultType::ERROR => $this->renderImportError($result), - StageResultType::COMPLETE => $this->renderImportSuccess($result), + StageResultType::RequiresUserInteraction => $this->renderImportStage($result), + StageResultType::Error => $this->renderImportError($result), + StageResultType::CompletedSuccessfully => $this->renderImportSuccess($result), }; } - private function buildImportStageRunner(): ImportStageRunner + private function buildImportWizard(): ImportWizard { $form_action = $this->ctrl->getFormActionByClass(self::class, 'processImport'); - return new ImportStageRunner( + return new ImportWizard( [ new UploadValidationStage( $this->archives, @@ -1179,11 +1177,10 @@ private function buildImportStageRunner(): ImportStageRunner ); } - private function renderImportStage(ImportStageRunner $runner, StageResult $result): void + private function renderImportStage(StageResult $result): void { - $workflow = $runner->buildWorkflow($this->ui_factory, $this->lng->txt('import')); $this->tpl->setContent( - $this->ui_renderer->render([$workflow, ...$result->components]) + $this->ui_renderer->render($result->components) ); } @@ -1198,7 +1195,7 @@ private function renderImportError(StageResult $result): void private function renderImportSuccess(StageResult $result): void { - $pool_obj_id = $this->data_factory->objId($result->context->poolObjId()); + $pool_obj_id = $this->data_factory->objId($result->session->poolObjId()); $pool_ref_id = $pool_obj_id->toReferenceIds()[0]->toInt(); $this->tpl->setOnScreenMessage( diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php index dd24fc14079f..1bd8cea80a25 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolExporter.php @@ -17,7 +17,7 @@ *********************************************************************/ use ILIAS\Export\ExportHandler\Factory as ExportHandler; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportStep; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportPhase; use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\XmlExporterBridge; use ILIAS\TestQuestionPool\QuestionPoolDIC; @@ -30,7 +30,7 @@ public function init(): void $local_dic = QuestionPoolDIC::dic(); $this->export_handler = new ExportHandler(); - $this->state_holder = $local_dic['exportimport.state_holder']; + $this->context_holder = $local_dic['exportimport.export_context_holder']; $this->exporter = $local_dic['exportimport.exporter']; $this->logger = $local_dic['exportimport.logging'](); } @@ -55,8 +55,8 @@ public function getXmlRepresentation(string $a_entity, string $a_schema_version, * Collects export tail dependencies for the question pool. * * The export framework calls this method before `getXmlRepresentation()`. - * Therefore this method only prepares and processes the export in memory using the export state. - * The export state is created if it does not exist yet. + * Therefore this method only prepares and processes the export in memory using the export context. + * The export context is created if it does not exist yet. */ public function getXmlExportTailDependencies(string $a_entity, string $a_target_release, array $a_ids): array { @@ -64,12 +64,12 @@ public function getXmlExportTailDependencies(string $a_entity, string $a_target_ throw new InvalidArgumentException("Invalid entity for question pool export: {$a_entity}"); } - // If the default export option was used, the state is not initialized yet. + // If the default export option was used, the context is not initialized yet. if ( - !$this->state_holder->exists() - || $this->state_holder->get()->getStep() !== ExportStep::INIT + !$this->context_holder->exists() + || $this->context_holder->get()->getPhase() !== ExportPhase::Init ) { - $this->initExportState( + $this->initExportContext( 'components/ILIAS/TestQuestionPool', $a_target_release, $a_entity, diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php index 1d9f1045b10e..85c6e1f4bced 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolImporter.php @@ -54,8 +54,8 @@ public function importXmlRepresentation( ilImportMapping $a_mapping ): void { // Check if forward to legacy importer is needed - $context = $this->session->getContext(); - if ($context->isLegacyImport()) { + $session = $this->session->getSession(); + if ($session->isLegacyImport()) { $this->legacy_importer->setInstallId($this->getInstallId()); $this->legacy_importer->setInstallUrl($this->getInstallUrl()); $this->legacy_importer->setSchemaVersion($this->getSchemaVersion()); @@ -68,16 +68,16 @@ public function importXmlRepresentation( XmlDeserializer::fromString($a_xml), $a_mapping, new ReferenceId($a_mapping->getTargetId()), - $context, + $session, ); - $this->session->setContext($result); + $this->session->setSession($result); } public function finalProcessing(ilImportMapping $a_mapping): void { // Check if forward to legacy importer is needed - $context = $this->session->getContext(); - if ($context->isLegacyImport()) { + $session = $this->session->getSession(); + if ($session->isLegacyImport()) { $this->legacy_importer->finalProcessing($a_mapping); return; } diff --git a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php index 12db37b8e937..4c6063afb1a2 100755 --- a/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php +++ b/components/ILIAS/TestQuestionPool/classes/class.ilTestQuestionPoolLegacyImporter.php @@ -54,11 +54,11 @@ public function importXmlRepresentation( $a_mapping->addMapping('components/ILIAS/TestQuestionPool', 'qpl', $a_id, (string) $this->pool_obj->getId()); - $context = $this->session->getContext(); - $context = $context->withPoolObjId($this->pool_obj->getId()); - $this->session->setContext($context); + $session = $this->session->getSession(); + $session = $session->withPoolObjId($this->pool_obj->getId()); + $this->session->setSession($session); - $qpl_parser = new ilObjQuestionPoolXMLParser($this->pool_obj, $context->legacyXmlFile()); + $qpl_parser = new ilObjQuestionPoolXMLParser($this->pool_obj, $session->legacyXmlFile()); $qpl_parser->startParsing(); // set another question pool name (if possible) @@ -71,19 +71,19 @@ public function importXmlRepresentation( $this->pool_obj->saveToDb(); $qti_parser = new ilQTIParser( - $context->importBaseDir(), - $context->legacyQtiFile(), + $session->importBaseDir(), + $session->legacyQtiFile(), ilQTIParser::IL_MO_PARSE_QTI, $this->pool_obj->getId(), - $context->selectedQuestionIds() + $session->selectedQuestionIds() ); $qti_parser->startParsing(); $page_parser = new ilQuestionPageParser( - $this->pool_obj, - $context->legacyXmlFile(), - $context->importBaseDir() - ); + $this->pool_obj, + $session->legacyXmlFile(), + $session->importBaseDir() + ); $page_parser->setQuestionMapping($qti_parser->getImportMapping()); $page_parser->startParsing(); @@ -113,7 +113,7 @@ public function importXmlRepresentation( ); } - $this->importQuestionSkillAssignments($context->legacyXmlFile(), $a_mapping, $this->pool_obj->getId()); + $this->importQuestionSkillAssignments($session->legacyXmlFile(), $a_mapping, $this->pool_obj->getId()); $a_mapping->addMapping( 'components/ILIAS/MetaData', diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php index a56f6189383a..6fdd5eeb4499 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Export/QuestionPoolExporter.php @@ -24,8 +24,8 @@ use ILIAS\Data\ObjectId; use ILIAS\Data\UUID\Factory as UUIDFactory; use ILIAS\Taxonomy\DomainService as Taxonomy; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportStep; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportState; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportPhase; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportContext; use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\Exporter; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\Serializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; @@ -50,15 +50,15 @@ public function __construct( /** * Prepares the export transformations and question image collector. */ - public function prepare(ExportState $state): void + public function prepare(ExportContext $context): void { - $state->logger()->info('Preparing question pool export (1/3)...'); - $state->assertStep(ExportStep::INIT); - $state->setStep(ExportStep::PREPARE); + $context->logger()->info('Preparing question pool export (1/3)...'); + $context->assertPhase(ExportPhase::Init); + $context->setPhase(ExportPhase::Prepare); - $pool_id = $this->extractObjectId($state); + $pool_id = $this->extractObjectId($context); if ($pool_id === null) { - $state->logger()->warning('No question pool object ID found for export'); + $context->logger()->warning('No question pool object ID found for export'); return; } @@ -67,94 +67,94 @@ public function prepare(ExportState $state): void $this->db, $pool_id ); - $state->setCollector($collector); + $context->setCollector($collector); $question_image_collector = new CollectQuestionImages(new UUIDFactory(), $pool_id); $transformations = $this->builder->forExport($question_image_collector); - $state->setTransformations($transformations); + $context->setTransformations($transformations); - $state->logger()->info('...Finished preparing question pool export (1/3)'); + $context->logger()->info('...Finished preparing question pool export (1/3)'); } /** * Normalizes the question pool object and its questions and writes them to the serializer. It also collects the * dependencies of the export. */ - public function process(ExportState $state): void + public function process(ExportContext $context): void { - $state->logger()->info('Processing question pool export (2/3)...'); - $state->assertStep(ExportStep::PREPARE); - $state->setStep(ExportStep::PROCESS); + $context->logger()->info('Processing question pool export (2/3)...'); + $context->assertPhase(ExportPhase::Prepare); + $context->setPhase(ExportPhase::Process); - $state->serializer()->group( + $context->serializer()->group( 'general', fn() => $this->exportObject( - $this->collector($state), - $state->transformations(), - $state->serializer(), - $state + $this->collector($context), + $context->transformations(), + $context->serializer(), + $context ) ); - $state->serializer()->group( + $context->serializer()->group( 'questions', fn() => $this->exportQuestions( - $this->collector($state), - $state->transformations(), - $state->serializer(), - $state + $this->collector($context), + $context->transformations(), + $context->serializer(), + $context ) ); - $state->serializer()->group( + $context->serializer()->group( 'skill_assignments', fn() => $this->exportSkillAssignments( - $this->collector($state), - $state->transformations(), - $state->serializer(), + $this->collector($context), + $context->transformations(), + $context->serializer(), ) ); - $state->logger()->info('...Finished processing question pool export (2/3)'); + $context->logger()->info('...Finished processing question pool export (2/3)'); } /** * Finalizes the export by copying the question images to the export directory and returning the export context. */ - public function write(ExportState $state): void + public function write(ExportContext $context): void { - $state->logger()->info('Writing question pool export (3/3)...'); - $state->assertStep(ExportStep::PROCESS); - $state->setStep(ExportStep::WRITE); + $context->logger()->info('Writing question pool export (3/3)...'); + $context->assertPhase(ExportPhase::Process); + $context->setPhase(ExportPhase::Write); - $export_dir = $state->path()->getPathToComponentExpDirInContainer(); - $question_image_collector = $state->transformations() + $export_dir = $context->path()->getPathToComponentExpDirInContainer(); + $question_image_collector = $context->transformations() ->normalizationProcessor(CollectQuestionImages::class); if (!($question_image_collector instanceof CollectQuestionImages)) { throw new \LogicException('Unexpected question image processor'); } foreach ($question_image_collector->getFiles() as $file) { - $state->writer()->writeFileByFilePath( + $context->writer()->writeFileByFilePath( $file['from'], "{$export_dir}/" . $file['to'] ); } - $state->logger()->info('...Finished writing question pool export (3/3)'); + $context->logger()->info('...Finished writing question pool export (3/3)'); } - private function extractObjectId(ExportState $state): ?ObjectId + private function extractObjectId(ExportContext $context): ?ObjectId { - $target_ids = $state->target()->getObjectIds(); + $target_ids = $context->target()->getObjectIds(); if ($target_ids === []) { - $state->logger()->warning('No target object IDs found for question pool export'); + $context->logger()->warning('No target object IDs found for question pool export'); return null; } if (count($target_ids) > 1) { - $state->logger()->warning( + $context->logger()->warning( 'Multiple target object IDs found for question pool export. Only the first one will be used.' ); } @@ -162,9 +162,9 @@ private function extractObjectId(ExportState $state): ?ObjectId return $this->data_factory->objId(array_shift($target_ids)); } - private function collector(ExportState $state): QuestionPoolCollector + private function collector(ExportContext $context): QuestionPoolCollector { - $collector = $state->collector(); + $collector = $context->collector(); if (!($collector instanceof QuestionPoolCollector)) { throw new \LogicException('Unexpected question pool collector'); } @@ -176,15 +176,15 @@ private function exportObject( QuestionPoolCollector $collector, Transformations $transformations, Serializer $serializer, - ExportState $state + ExportContext $context ): void { $serializer->append('object', $transformations->normalize($collector->getObject())); $obj_id = $collector->getObjectId()->toInt(); - $state->addDependency('components/ILIAS/ILIASObject', 'common', [$obj_id]); - $state->addDependency('components/ILIAS/MetaData', 'qpl', ["{$obj_id}:0:qpl"]); - $state->addDependency( + $context->addDependency('components/ILIAS/ILIASObject', 'common', [$obj_id]); + $context->addDependency('components/ILIAS/MetaData', 'qpl', ["{$obj_id}:0:qpl"]); + $context->addDependency( 'components/ILIAS/Taxonomy', 'tax', $this->taxonomy->getUsageOfObject($obj_id) @@ -195,7 +195,7 @@ private function exportQuestions( QuestionPoolCollector $collector, Transformations $transformations, Serializer $serializer, - ExportState $state + ExportContext $context ): void { foreach ($collector->getQuestionObjects() as $question) { $normalized = [ @@ -210,7 +210,7 @@ private function exportQuestions( } $serializer->append('question', $normalized); - $state->addDependency('components/ILIAS/COPage', 'pg', ["qpl:{$question->getId()}"]); + $context->addDependency('components/ILIAS/COPage', 'pg', ["qpl:{$question->getId()}"]); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportState.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportContext.php similarity index 89% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportState.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportContext.php index 6f9550d2f7a4..1ed75e108611 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportState.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportContext.php @@ -28,9 +28,9 @@ use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; use Psr\Log\LoggerInterface as Logger; -class ExportState +class ExportContext { - private ExportStep $step = ExportStep::INIT; + private ExportPhase $phase = ExportPhase::Init; private ?Logger $logger = null; private ?ExportPath $path_info = null; private ?Transformations $transformations = null; @@ -65,23 +65,23 @@ public function getOption(): string return $this->option; } - public function getStep(): ExportStep + public function getPhase(): ExportPhase { - return $this->step; + return $this->phase; } - public function setStep(ExportStep $step): void + public function setPhase(ExportPhase $phase): void { - $this->step = $step; + $this->phase = $phase; } - public function assertStep(ExportStep $step): void + public function assertPhase(ExportPhase $phase): void { - if ($this->step->value < $step->value) { - throw new \RuntimeException("Expected step {$step->name}, but got {$this->step->name} instead"); + if ($this->phase->value < $phase->value) { + throw new \RuntimeException("Expected phase {$phase->name}, but got {$this->phase->name} instead"); } - $this->step = $step; + $this->phase = $phase; } public function logger(): Logger @@ -188,7 +188,7 @@ private function assertNotNull(mixed $value, string $property): void { if ($value === null) { throw new \RuntimeException( - "{$property} not set. This may happen if the exporter steps are not executed in the correct order." + "{$property} not set. This may happen if the exporter phases are not executed in the correct order." ); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/StateHolder.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportContextHolder.php similarity index 54% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/StateHolder.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportContextHolder.php index 282662a24bd1..b80efb6e2a4f 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/StateHolder.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportContextHolder.php @@ -24,42 +24,42 @@ use ILIAS\Export\ExportHandler\I\Consumer\ExportConfig\CollectionInterface as ExportConfig; /** - * State management for the export process. It is used to pass the export target, config and tools between the ILIAS - * export component and the local exporter classes, as the legacy `ilExport` and `ilXmlExporter` do not provide access - * to the core export functionality. + * Holds the export context for one request. It passes the export target, config and tools between the ILIAS export + * component and the local exporter classes, as the legacy `ilExport` and `ilXmlExporter` do not provide access to the + * core export functionality. */ -class StateHolder +class ExportContextHolder { - private ?ExportState $export_state = null; + private ?ExportContext $export_context = null; public function create( ExportTarget $target, ExportConfig $config, string $option = '' - ): ExportState { - return $this->export_state = new ExportState($target, $config, $option); + ): ExportContext { + return $this->export_context = new ExportContext($target, $config, $option); } public function exists(): bool { - return $this->export_state !== null; + return $this->export_context !== null; } - public function get(): ExportState + public function get(): ExportContext { - if ($this->export_state === null) { - throw new \RuntimeException('Export state not found. You need to create the state first.'); + if ($this->export_context === null) { + throw new \RuntimeException('Export context not found. You need to create the context first.'); } - return $this->export_state; + return $this->export_context; } - public function set(ExportState $export_state): void + public function set(ExportContext $export_context): void { - $this->export_state = $export_state; + $this->export_context = $export_context; } public function clear(): void { - $this->export_state = null; + $this->export_context = null; } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportStep.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportPhase.php similarity index 85% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportStep.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportPhase.php index 818295f83b0e..d3c10b53304b 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportStep.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/ExportPhase.php @@ -20,10 +20,10 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Export; -enum ExportStep: int +enum ExportPhase: int { - case INIT = 1; - case PREPARE = 2; - case PROCESS = 3; - case WRITE = 4; + case Init = 1; + case Prepare = 2; + case Process = 3; + case Write = 4; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/Exporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/Exporter.php index 7385680953bf..37b6c1d1252d 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/Exporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/Exporter.php @@ -20,7 +20,7 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Export; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportState; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportContext; /** * An Exporter is responsible for orchestrating the export process. It prepares the dependencies, collects the data to @@ -36,7 +36,7 @@ interface Exporter * - Config * - Logger */ - public function prepare(ExportState $state): void; + public function prepare(ExportContext $context): void; /** * Processes the export by collecting the data to export, transforming the data and writing the data to the @@ -48,7 +48,7 @@ public function prepare(ExportState $state): void; * - Logger * - Serializer */ - public function process(ExportState $state): void; + public function process(ExportContext $context): void; /** * Finalizes the export by persisting additional data (which cannot be provided by the serializer) to the file @@ -62,5 +62,5 @@ public function process(ExportState $state): void; * - Writer * - Path */ - public function write(ExportState $state): void; + public function write(ExportContext $context): void; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/XmlExporterBridge.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/XmlExporterBridge.php index f3c4292da25d..441e1fdba658 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/XmlExporterBridge.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Export/XmlExporterBridge.php @@ -31,66 +31,66 @@ * needed because the `ilXmlExporter` does not provide access to the core export functionality. * * The `getXmlExportTailDependencies` method is called by the core before the XML can be generated, because ExportWriter - * and ExportPath are not initialized at that point. The export therefore has to be separated into multiple steps. The - * bridge orchestrates these steps and provides the required dependencies via the `ExportState` at each step. + * and ExportPath are not initialized at that point. The export therefore has to be separated into multiple phases. The + * bridge orchestrates these phases and provides the required dependencies via the `ExportContext` at each phase. * * @mixin \ilXmlExporter */ trait XmlExporterBridge { protected readonly ExportHandler $export_handler; - protected readonly StateHolder $state_holder; + protected readonly ExportContextHolder $context_holder; protected readonly Exporter $exporter; protected readonly Logger $logger; /** - * Processes the export in memory using the global export state. It performs the prepare and process steps of the - * exporter (if not already done) and returns the state. + * Processes the export in memory using the request export context. It performs the prepare and process phases of + * the exporter (if not already done) and returns the context. */ - private function processExport(): ExportState + private function processExport(): ExportContext { - $state = $this->state_holder->get(); + $context = $this->context_holder->get(); - if ($state->getStep()->value < ExportStep::PREPARE->value) { - $state->setLogger($this->logger); - $this->exporter->prepare($state); + if ($context->getPhase()->value < ExportPhase::Prepare->value) { + $context->setLogger($this->logger); + $this->exporter->prepare($context); } - if ($state->getStep()->value < ExportStep::PROCESS->value) { - $state->setSerializer(XmlSerializer::inMemory()); - $this->exporter->process($state); + if ($context->getPhase()->value < ExportPhase::Process->value) { + $context->setSerializer(XmlSerializer::inMemory()); + $this->exporter->process($context); } - $this->state_holder->set($state); - return $state; + $this->context_holder->set($context); + return $context; } /** - * Finalizes the export by setting the path info and writer and calling the write step of the exporter. It performs - * the prepare and process steps if not already done. + * Finalizes the export by setting the path info and writer and calling the write phase of the exporter. It + * performs the prepare and process phases if not already done. */ - private function finalizeExport(): ExportState + private function finalizeExport(): ExportContext { - $state = $this->processExport(); + $context = $this->processExport(); - if ($state->getStep()->value < ExportStep::WRITE->value) { - $state->setPathInfo($this->createPathInfo()); - $state->setWriter($this->exp->getExportWriter()); + if ($context->getPhase()->value < ExportPhase::Write->value) { + $context->setPathInfo($this->createPathInfo()); + $context->setWriter($this->exp->getExportWriter()); - $this->exporter->write($state); + $this->exporter->write($context); } - $this->state_holder->clear(); - return $state; + $this->context_holder->clear(); + return $context; } - private function initExportState( + private function initExportContext( string $component, string $target_release, string $type, array $object_ids, string $option = '' - ): ExportState { + ): ExportContext { $object_ids = array_slice($object_ids, 0, 1); $target = $this->export_handler->target()->handler() ->withType($type) @@ -99,13 +99,13 @@ private function initExportState( ->withClassname(static::class) ->withComponent($component); - $state = $this->state_holder->create( + $context = $this->context_holder->create( $target, $this->export_handler->consumer()->exportConfig()->collection(), $option ); - return $state; + return $context; } private function createPathInfo(): ExportPath diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportContext.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportSession.php similarity index 84% rename from components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportContext.php rename to components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportSession.php index 7d97a9bd3ad8..927baf50ac75 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportContext.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportSession.php @@ -21,12 +21,12 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Import; /** - * Immutable JSON-capable import context shared by question pool and test wizards. + * Immutable JSON-capable import session shared by question pool and test wizards. * * @phpstan-type SkillAssignmentResult array{failed: list, success: list} * @phpstan-type SkillThresholdResult array{failed: list, success: list} */ -final class ImportContext +final class ImportSession { private ?string $file_to_import = null; private ?string $component_import_file = null; @@ -64,7 +64,7 @@ public function hasFileToImport(): bool public function fileToImport(): string { - if($this->file_to_import === null) { + if ($this->file_to_import === null) { throw new \LogicException('File to import is not set'); } @@ -80,7 +80,7 @@ public function withComponentImportFile(string $path): self public function componentImportFile(): string { - if($this->component_import_file === null) { + if ($this->component_import_file === null) { throw new \LogicException('Component import file is not set'); } @@ -101,7 +101,7 @@ public function hasImportBaseDir(): bool public function importBaseDir(): string { - if($this->import_base_dir === null) { + if ($this->import_base_dir === null) { throw new \LogicException('Import base dir is not set'); } @@ -117,7 +117,7 @@ public function withInstallId(int $install_id): self public function installId(): int { - if($this->install_id === null) { + if ($this->install_id === null) { throw new \LogicException('Install id is not set'); } @@ -133,7 +133,7 @@ public function withLegacyQtiFile(string $path): self public function legacyQtiFile(): string { - if($this->legacy_qti_file === null) { + if ($this->legacy_qti_file === null) { throw new \LogicException('Legacy QTI file is not set'); } @@ -149,7 +149,7 @@ public function withLegacyXmlFile(string $path): self public function legacyXmlFile(): string { - if($this->legacy_xml_file === null) { + if ($this->legacy_xml_file === null) { throw new \LogicException('Legacy XML file is not set'); } @@ -201,7 +201,7 @@ public function withPoolObjId(int $pool_obj_id): self public function poolObjId(): int { - if($this->pool_obj_id === null) { + if ($this->pool_obj_id === null) { throw new \LogicException('Pool object id is not set'); } @@ -217,7 +217,7 @@ public function withTestObjId(int $test_obj_id): self public function testObjId(): ?int { - if($this->test_obj_id === null) { + if ($this->test_obj_id === null) { throw new \LogicException('Test object id is not set'); } @@ -233,7 +233,7 @@ public function withTestRefId(int $test_ref_id): self public function testRefId(): int { - if($this->test_ref_id === null) { + if ($this->test_ref_id === null) { throw new \LogicException('Test reference id is not set'); } @@ -255,7 +255,7 @@ public function withUserMappings(array $mappings): self */ public function userMappings(): array { - if($this->user_mappings === null) { + if ($this->user_mappings === null) { throw new \LogicException('User mappings are not set'); } @@ -295,7 +295,7 @@ public function withSkillAssignments(array $result): self */ public function skillAssignments(): array { - if($this->skill_assignments === null) { + if ($this->skill_assignments === null) { throw new \LogicException('Skill assignments are not set'); } @@ -317,7 +317,7 @@ public function withSkillThresholds(array $result): self */ public function skillThresholds(): array { - if($this->skill_thresholds === null) { + if ($this->skill_thresholds === null) { throw new \LogicException('Skill thresholds are not set'); } @@ -333,23 +333,23 @@ public static function fromJson(string $json): self { $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); - $context = new self(); - $context->file_to_import = $data['file_to_import'] ?? null; - $context->component_import_file = $data['component_import_file'] ?? null; - $context->import_base_dir = $data['import_base_dir'] ?? null; - $context->install_id = $data['install_id'] ?? null; - $context->legacy_qti_file = $data['legacy_qti_file'] ?? null; - $context->legacy_xml_file = $data['legacy_xml_file'] ?? null; - $context->selectable_question_ids = $data['selectable_question_ids'] ?? []; - $context->selected_question_ids = $data['selected_question_ids'] ?? []; - $context->pool_obj_id = $data['pool_obj_id'] ?? null; - $context->test_obj_id = $data['test_obj_id'] ?? null; - $context->test_ref_id = $data['test_ref_id'] ?? null; - $context->user_mappings = $data['user_mappings'] ?? null; - $context->resource_mappings = $data['resource_mappings'] ?? []; - $context->skill_assignments = $data['skill_assignments'] ?? null; - $context->skill_thresholds = $data['skill_thresholds'] ?? null; - return $context; + $session = new self(); + $session->file_to_import = $data['file_to_import'] ?? null; + $session->component_import_file = $data['component_import_file'] ?? null; + $session->import_base_dir = $data['import_base_dir'] ?? null; + $session->install_id = $data['install_id'] ?? null; + $session->legacy_qti_file = $data['legacy_qti_file'] ?? null; + $session->legacy_xml_file = $data['legacy_xml_file'] ?? null; + $session->selectable_question_ids = $data['selectable_question_ids'] ?? []; + $session->selected_question_ids = $data['selected_question_ids'] ?? []; + $session->pool_obj_id = $data['pool_obj_id'] ?? null; + $session->test_obj_id = $data['test_obj_id'] ?? null; + $session->test_ref_id = $data['test_ref_id'] ?? null; + $session->user_mappings = $data['user_mappings'] ?? null; + $session->resource_mappings = $data['resource_mappings'] ?? []; + $session->skill_assignments = $data['skill_assignments'] ?? null; + $session->skill_thresholds = $data['skill_thresholds'] ?? null; + return $session; } public function toJson(): string diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportSessionRepository.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportSessionRepository.php index 15cfd7e4d1fb..c45a26011c50 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportSessionRepository.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportSessionRepository.php @@ -21,7 +21,7 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Import; /** - * Persists the import stage index and context across HTTP requests using `\ilSession`. Each instance is scoped by a + * Persists the import stage index and session across HTTP requests using `\ilSession`. Each instance is scoped by a * namespace string so that multiple concurrent import workflows do not interfere with each other. */ class ImportSessionRepository @@ -49,23 +49,23 @@ public function setCurrentStageIndex(int $index): void \ilSession::set($this->key_index, $index); } - public function getContext(): ImportContext + public function getSession(): ImportSession { if (!\ilSession::has($this->key_context)) { - return new ImportContext(); + return new ImportSession(); } $payload = \ilSession::get($this->key_context); if (!is_string($payload)) { - return new ImportContext(); + return new ImportSession(); } - return ImportContext::fromJson($payload); + return ImportSession::fromJson($payload); } - public function setContext(ImportContext $context): void + public function setSession(ImportSession $session): void { - \ilSession::set($this->key_context, $context->toJson()); + \ilSession::set($this->key_context, $session->toJson()); } public function clear(): void diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportStage.php index 2b9fb5e034a9..8707d515ac1b 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportStage.php @@ -20,12 +20,12 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Import; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSession; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\StageResult; /** - * A single step in a multi-stage import workflow. Each stage is called by the ImportStageRunner and returns a - * StageResult that determines the next action (render UI, advance, report error, or complete). + * One step of an import wizard. The wizard calls the stage and continues, stops for the UI, or reports an error + * based on the returned StageResult. */ interface ImportStage { @@ -47,9 +47,8 @@ public function getLabel(): ?string; public function getDescription(): ?string; /** - * Process the current stage. On the first invocation the stage should return `StageResult::interact()` with the UI - * components to display. On form submission it should validate the input and return `StageResult::advance()` or - * `StageResult::error()`. + * Process the current stage. A stage that needs input returns `StageResult::requiresUserInteraction()`. A finished + * stage returns `StageResult::completedSuccessfully()`. A failed stage returns `StageResult::error()`. */ - public function process(ImportContext $context): StageResult; + public function process(ImportSession $session): StageResult; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportStageRunner.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportStageRunner.php deleted file mode 100644 index 6d5946b3497d..000000000000 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportStageRunner.php +++ /dev/null @@ -1,130 +0,0 @@ - $stages */ - public function __construct( - private readonly array $stages, - private readonly ImportSessionRepository $session, - private readonly ?ImportStage $final_stage = null, - ) { - } - - /** - * Run the import stage runner. It manages the state of the import process and delegates to the current stage. - * It will return a StageResult that indicates the next action to take. - */ - public function run(): StageResult - { - $index = $this->session->getCurrentStageIndex(); - $context = $this->session->getContext(); - - if ($index >= count($this->stages)) { - return StageResult::complete($context); - } - - $result = $this->stages[$index]->process($context); - - switch ($result->type) { - case StageResultType::ADVANCE: - $next_index = $index + 1; - $this->session->setContext($result->context); - $this->session->setCurrentStageIndex($next_index); - - if ($next_index >= count($this->stages)) { - return StageResult::complete($result->context); - } - - return $result; - - case StageResultType::ERROR: - $this->session->setContext($result->context); - $this->reset(); - return $result; - - case StageResultType::INTERACT: - $this->session->setContext($result->context); - return $result; - - case StageResultType::COMPLETE: - $this->reset(); - return $result; - } - - throw new \LogicException("Invalid stage result type: {$result->type->name}"); - } - - /** - * Build the workflow UI component for the import process. - */ - public function buildWorkflow(UIFactory $ui, string $title): Linear - { - $steps = []; - $active_index = $this->session->getCurrentStageIndex(); - - foreach ($this->stages as $i => $stage) { - if ($stage->getLabel() === null) { - continue; - } - - $step = $ui->listing()->workflow()->step( - $stage->getLabel(), - $stage->getDescription() - ); - - if ($i < $active_index) { - $step = $step->withStatus(Step::SUCCESSFULLY)->withAvailability(Step::NOT_ANYMORE); - } elseif ($i === $active_index) { - $step = $step->withStatus(Step::IN_PROGRESS)->withAvailability(Step::AVAILABLE); - } else { - $step = $step->withStatus(Step::NOT_STARTED)->withAvailability(Step::NOT_AVAILABLE); - } - - $steps[] = $step; - } - - return $ui->listing()->workflow()->linear($title, $steps)->withActive($active_index); - } - - /** - * Reset the import stage session. If a final stage is set, it will be processed. - */ - public function reset(): void - { - if ($this->final_stage) { - $this->final_stage->process($this->session->getContext()); - } - - $this->session->clear(); - } -} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportWizard.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportWizard.php new file mode 100644 index 000000000000..13253f4f8230 --- /dev/null +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/ImportWizard.php @@ -0,0 +1,83 @@ + $stages */ + public function __construct( + private readonly array $stages, + private readonly ImportSessionRepository $session_repository, + private readonly ?ImportStage $cleanup_stage = null, + ) { + } + + public function run(): StageResult + { + $index = $this->session_repository->getCurrentStageIndex(); + $session = $this->session_repository->getSession(); + + while ($index < count($this->stages)) { + $result = $this->stages[$index]->process($session); + + switch ($result->type) { + case StageResultType::CompletedSuccessfully: + $session = $result->session; + $index++; + $this->session_repository->setSession($session); + $this->session_repository->setCurrentStageIndex($index); + break; + + case StageResultType::RequiresUserInteraction: + $this->session_repository->setSession($result->session); + return $result; + + case StageResultType::Error: + $this->session_repository->setSession($result->session); + $this->reset(); + return $result; + + default: + throw new \LogicException("Invalid stage result type: {$result->type->name}"); + } + } + + $this->reset(); + + return StageResult::completedSuccessfully($session); + } + + /** + * Clears the stored import session. The cleanup stage runs first when one is configured. + */ + public function reset(): void + { + if ($this->cleanup_stage) { + $this->cleanup_stage->process($this->session_repository->getSession()); + } + + $this->session_repository->clear(); + } +} diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/StageResult.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/StageResult.php index f4599a4ee99f..814a94a3b83b 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/StageResult.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/StageResult.php @@ -23,9 +23,7 @@ use ILIAS\UI\Component\Component; /** - * Represents the outcome of processing a single import stage. The GUI controller inspects the type to decide whether - * to render UI components (INTERACT), redirect for the next stage (ADVANCE), display an error (ERROR), or finalize the - * import (COMPLETE). + * Outcome of one import stage. The wizard decides whether another stage follows. */ class StageResult { @@ -34,43 +32,27 @@ class StageResult */ private function __construct( public readonly StageResultType $type, - public readonly ImportContext $context, + public readonly ImportSession $session, public readonly array $components = [], public readonly ?string $error_message = null, ) { } /** - * Create a result that indicates the process should interrupt to display the given UI components. - * - * @param list $components - */ - public static function interact(ImportContext $context, array $components): self - { - return new self(StageResultType::INTERACT, $context, $components); - } - - /** - * Create a result that indicates the process should advance to the next stage. + * @param list $components */ - public static function advance(ImportContext $context): self + public static function requiresUserInteraction(ImportSession $session, array $components): self { - return new self(StageResultType::ADVANCE, $context); + return new self(StageResultType::RequiresUserInteraction, $session, $components); } - /** - * Create a result that indicates the process should fail with the given error message. - */ - public static function error(ImportContext $context, string $message): self + public static function error(ImportSession $session, string $message): self { - return new self(StageResultType::ERROR, $context, [], $message); + return new self(StageResultType::Error, $session, [], $message); } - /** - * Create a result that indicates the process should complete successfully. - */ - public static function complete(ImportContext $context): self + public static function completedSuccessfully(ImportSession $session): self { - return new self(StageResultType::COMPLETE, $context); + return new self(StageResultType::CompletedSuccessfully, $session); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/StageResultType.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/StageResultType.php index 790c9d3ebc20..79e101b0f365 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/StageResultType.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Foundation/Import/StageResultType.php @@ -20,28 +20,9 @@ namespace ILIAS\TestQuestionPool\ExportImport\Foundation\Import; -/** - * Represents the type of result of a single import stage. - */ enum StageResultType { - /** - * The stage has encountered an error and the workflow should display an error message. - */ - case ERROR; - - /** - * The stage has returned UI components to be rendered. - */ - case INTERACT; - - /** - * The stage has completed successfully and the workflow should advance to the next stage. - */ - case ADVANCE; - - /** - * The stage has completed successfully and the workflow should complete. - */ - case COMPLETE; + case Error; + case RequiresUserInteraction; + case CompletedSuccessfully; } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php index 25c43b18dd94..cfe8e2770311 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/CleanupStage.php @@ -21,7 +21,7 @@ namespace ILIAS\TestQuestionPool\ExportImport\Import; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportStage; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSession; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\StageResult; use Psr\Log\LoggerInterface; @@ -50,10 +50,10 @@ public function getDescription(): ?string return null; } - public function process(ImportContext $context): StageResult + public function process(ImportSession $session): StageResult { - if ($context->hasFileToImport()) { - $temp_dir = dirname($context->fileToImport()); + if ($session->hasFileToImport()) { + $temp_dir = dirname($session->fileToImport()); if ($temp_dir !== '' && file_exists($temp_dir) && is_dir($temp_dir)) { $this->removeDirectory($temp_dir); } else { @@ -61,13 +61,13 @@ public function process(ImportContext $context): StageResult } } - if ($context->hasImportBaseDir() && is_dir($context->importBaseDir())) { - $this->removeDirectory($context->importBaseDir()); + if ($session->hasImportBaseDir() && is_dir($session->importBaseDir())) { + $this->removeDirectory($session->importBaseDir()); } else { - $this->log->warning("Import target base directory does not exist: {$context->importBaseDir()}"); + $this->log->warning("Import target base directory does not exist: {$session->importBaseDir()}"); } - return StageResult::complete($context); + return StageResult::completedSuccessfully($session); } private function removeDirectory(string $path): void diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php index 6b944c348ecc..12ac607659b9 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/DetectLegacyImportStage.php @@ -21,7 +21,7 @@ namespace ILIAS\TestQuestionPool\ExportImport\Import; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportStage; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSession; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\StageResult; use Psr\Log\LoggerInterface; @@ -50,19 +50,19 @@ public function getDescription(): ?string return null; } - public function process(ImportContext $context): StageResult + public function process(ImportSession $session): StageResult { - $import_name = basename($context->importBaseDir()); + $import_name = basename($session->importBaseDir()); - $xml_file = $context->importBaseDir() . DIRECTORY_SEPARATOR . "{$import_name}.xml"; - $qti_file = $context->importBaseDir() . DIRECTORY_SEPARATOR . str_replace(['_qpl_', '_tst_'], '_qti_', $import_name) . '.xml'; + $xml_file = $session->importBaseDir() . DIRECTORY_SEPARATOR . "{$import_name}.xml"; + $qti_file = $session->importBaseDir() . DIRECTORY_SEPARATOR . str_replace(['_qpl_', '_tst_'], '_qti_', $import_name) . '.xml'; if (!file_exists($qti_file) || !file_exists($xml_file)) { - return StageResult::advance($context); + return StageResult::completedSuccessfully($session); } - return StageResult::advance( - $context->withLegacyQtiFile($qti_file)->withLegacyXmlFile($xml_file) + return StageResult::completedSuccessfully( + $session->withLegacyQtiFile($qti_file)->withLegacyXmlFile($xml_file) ); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php index 4436a35cdad2..468db6c270a1 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/PersistStage.php @@ -22,7 +22,7 @@ use ILIAS\Language\Language; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportStage; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSession; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\StageResult; use ILIAS\TestQuestionPool\RequestDataCollector; @@ -55,19 +55,19 @@ public function getDescription(): ?string return ''; } - public function process(ImportContext $context): StageResult + public function process(ImportSession $session): StageResult { $importer = new \ilImport($this->request_data_collector->getRefId()); $importer->importObject( null, - $context->fileToImport(), - basename($context->fileToImport()), + $session->fileToImport(), + basename($session->fileToImport()), 'qpl', 'components/ILIAS/TestQuestionPool', true, ); // Context is updated by the QuestionPoolImporter so we need to reload it - return StageResult::complete($this->session->getContext()); + return StageResult::completedSuccessfully($this->session->getSession()); } } diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php index 491f2bd49a6e..ebb425b4ae99 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionPoolImporter.php @@ -25,7 +25,7 @@ use ILIAS\Data\UUID\Factory; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\Deserializer; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSession; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\IdMappingProcessor; use ILIAS\TestQuestionPool\ExportImport\TransformationsBuilder; use TestQuestionPool\ExportImport\Normalize\Processors\CollectQuestionImages; @@ -53,8 +53,8 @@ public function import( Deserializer $deserializer, \ilImportMapping $mapping, ReferenceId $parent_id, - ImportContext $context - ): ImportContext { + ImportSession $session + ): ImportSession { $images_collector = new CollectQuestionImages(new Factory(), $this->data_factory->objId(0)); $transformations = $this->builder->forImport( null, @@ -64,26 +64,26 @@ public function import( $deserializer->addHandler( 'general', - function (array $objects) use ($transformations, $mapping, $parent_id, &$context): void { + function (array $objects) use ($transformations, $mapping, $parent_id, &$session): void { $new_pool_id = $this->importQuestionPool( array_pop($objects), $transformations, $mapping, $parent_id ); - $context = $context->withPoolObjId($new_pool_id); + $session = $session->withPoolObjId($new_pool_id); } ); $deserializer->addHandler( 'questions', - function (array $questions) use ($transformations, $mapping, $context): void { + function (array $questions) use ($transformations, $mapping, $session): void { foreach ($questions as $question) { $this->questions_importer->importQuestion( $question, $transformations, $mapping, - $context->selectedQuestionIds() + $session->selectedQuestionIds() ); } } @@ -91,27 +91,27 @@ function (array $questions) use ($transformations, $mapping, $context): void { $deserializer->addHandler( 'skill_assignments', - function (array $assignments) use ($transformations, $mapping, &$context): void { + function (array $assignments) use ($transformations, $mapping, &$session): void { $result = $this->skill_importer->import( $assignments, - $context->installId(), + $session->installId(), $transformations, $mapping, ); - $context = $context->withSkillAssignments($result); + $session = $session->withSkillAssignments($result); } ); $deserializer->process(); $this->questions_importer->importQuestionImages( - $context->poolObjId(), + $session->poolObjId(), $mapping, - $context, + $session, $images_collector ); - return $context; + return $session; } /** diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php index 39fecbc9f0dc..d74f0234d7ee 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionSelectionStage.php @@ -22,7 +22,7 @@ use ILIAS\Language\Language; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportStage; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSession; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\StageResult; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\XmlDeserializer; use ILIAS\UI\Component\Input\Container\Form\Form; @@ -76,11 +76,11 @@ public function getDescription(): ?string return ''; } - public function process(ImportContext $context): StageResult + public function process(ImportSession $session): StageResult { - if ($context->selectableQuestionIds() !== []) { + if ($session->selectableQuestionIds() !== []) { $options = []; - foreach ($context->selectableQuestionIds() as $question) { + foreach ($session->selectableQuestionIds() as $question) { $options[$question] = (string) $question; } @@ -90,19 +90,19 @@ public function process(ImportContext $context): StageResult if (is_array($data) && is_array($data['selected_questions'])) { $selected_questions = array_map('intval', $data['selected_questions']); - return StageResult::advance( - $context->withSelectedQuestionIds($selected_questions) + return StageResult::completedSuccessfully( + $session->withSelectedQuestionIds($selected_questions) ); } } - $options = $context->isLegacyImport() - ? $this->readQuestionsFromQTI($context) - : $this->readQuestions($context); + $options = $session->isLegacyImport() + ? $this->readQuestionsFromQTI($session) + : $this->readQuestions($session); if ($options === []) { $this->log->error('No questions found in import file'); - return StageResult::error($context, $this->lng->txt('qpl_import_no_items')); + return StageResult::error($session, $this->lng->txt('qpl_import_no_items')); } $panel = $this->ui_factory->panel()->standard( @@ -113,17 +113,17 @@ public function process(ImportContext $context): StageResult ] ); - return StageResult::interact( - $context->withSelectableQuestionIds(array_keys($options)), + return StageResult::requiresUserInteraction( + $session->withSelectableQuestionIds(array_keys($options)), [$panel] ); } - private function readQuestions(ImportContext $context): array + private function readQuestions(ImportSession $session): array { $options = []; - $deserializer = XmlDeserializer::fromFile($context->componentImportFile()); + $deserializer = XmlDeserializer::fromFile($session->componentImportFile()); $deserializer->addHandler( 'questions', @@ -149,11 +149,11 @@ function (array $questions) use (&$options): void { /** * @deprecated This method is only used for legacy imports and will be removed with further ILIAS versions. */ - private function readQuestionsFromQTI(ImportContext $context): array + private function readQuestionsFromQTI(ImportSession $session): array { $parser = new \ilQTIParser( - $context->importBaseDir(), - $context->legacyQtiFile(), + $session->importBaseDir(), + $session->legacyQtiFile(), \ilQTIParser::IL_MO_VERIFY_QTI, 0 ); diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php index bd0867d1d60c..05117c8c0a09 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/QuestionsImporter.php @@ -29,7 +29,7 @@ use ILIAS\Language\Language; use ILIAS\TestQuestionPool\ExportImport\Normalize\Envelopes\QuestionImage; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Transformations; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSession; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Envelopes\Id; use ILIAS\TestQuestionPool\ExportImport\Normalize\Envelopes\Feedback; use TestQuestionPool\ExportImport\Normalize\Processors\CollectQuestionImages; @@ -95,10 +95,10 @@ public function importQuestion( public function importQuestionImages( int $parent_obj_id, \ilImportMapping $mapping, - ImportContext $context, + ImportSession $session, CollectQuestionImages $collector, ): void { - $import_dir = dirname($context->componentImportFile()) . '/expDir_1'; + $import_dir = dirname($session->componentImportFile()) . '/expDir_1'; foreach ($collector->getEnvelopes() as $filename => $envelope) { $source_path = $import_dir . DIRECTORY_SEPARATOR . $filename; diff --git a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php index 9186d36d2804..645fa9bb9ab4 100644 --- a/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php +++ b/components/ILIAS/TestQuestionPool/src/ExportImport/Import/UploadValidationStage.php @@ -25,7 +25,7 @@ use ILIAS\Filesystem\Util\Archive\UnzipOptions; use ILIAS\Language\Language; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportStage; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSession; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\StageResult; use Psr\Log\LoggerInterface; @@ -60,22 +60,22 @@ public function getDescription(): ?string return ''; } - public function process(ImportContext $context): StageResult + public function process(ImportSession $session): StageResult { if ( - !$context->hasFileToImport() - || !is_file($context->fileToImport()) - || !str_ends_with(strtolower($context->fileToImport()), '.zip') + !$session->hasFileToImport() + || !is_file($session->fileToImport()) + || !str_ends_with(strtolower($session->fileToImport()), '.zip') ) { - $this->log->error("Invalid import file: {$context->fileToImport()}"); - return StageResult::error($context, $this->lng->txt('obj_import_file_error')); + $this->log->error("Invalid import file: {$session->fileToImport()}"); + return StageResult::error($session, $this->lng->txt('obj_import_file_error')); } - $subdir = basename($context->fileToImport(), '.zip'); + $subdir = basename($session->fileToImport(), '.zip'); $import_base_dir = self::IMPORT_TEMP_DIR . DIRECTORY_SEPARATOR . $subdir; $options = (new UnzipOptions())->withZipOutputPath(self::IMPORT_TEMP_DIR); - $handle = fopen($context->fileToImport(), 'r'); + $handle = fopen($session->fileToImport(), 'r'); $unzip = $this->archives->unzip(Streams::ofResource($handle), $options); $unzip->extract(); @@ -87,13 +87,13 @@ public function process(ImportContext $context): StageResult if ($export_file === null) { $this->log->error("No export file found for component: {$this->component}"); - return StageResult::error($context, $this->lng->txt('obj_import_file_error')); + return StageResult::error($session, $this->lng->txt('obj_import_file_error')); } $component_import_file = $import_base_dir . DIRECTORY_SEPARATOR . $export_file['path']; - return StageResult::advance( - $context + return StageResult::completedSuccessfully( + $session ->withComponentImportFile($component_import_file) ->withImportBaseDir($import_base_dir) ->withInstallId((int) $manifest->getInstallId()) diff --git a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php index 50c2cffa74d8..9f34aabc57ac 100755 --- a/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php +++ b/components/ILIAS/TestQuestionPool/src/QuestionPoolDIC.php @@ -21,7 +21,7 @@ namespace ILIAS\TestQuestionPool; use ILIAS\Data\Factory as DataFactory; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\StateHolder; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportContextHolder; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Export\QuestionPoolExporter; use ILIAS\TestQuestionPool\ExportImport\Import\QuestionPoolImporter; @@ -80,8 +80,8 @@ protected static function buildDIC(ILIASContainer $DIC): self new LoggingProvider(); $dic['exportimport.transformations_builder'] = static fn($c): TransformationsBuilder => new TransformationsBuilder($DIC); - $dic['exportimport.state_holder'] = static fn($c): StateHolder => - new StateHolder(); + $dic['exportimport.export_context_holder'] = static fn($c): ExportContextHolder => + new ExportContextHolder(); $dic['exportimport.exporter'] = static fn($c): QuestionPoolExporter => new QuestionPoolExporter( $c['exportimport.transformations_builder'], diff --git a/components/ILIAS/TestQuestionPool/tests/ExportImport/StateHolderTest.php b/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportContextHolderTest.php similarity index 82% rename from components/ILIAS/TestQuestionPool/tests/ExportImport/StateHolderTest.php rename to components/ILIAS/TestQuestionPool/tests/ExportImport/ExportContextHolderTest.php index f071a70a8cc1..ad761fd951cb 100644 --- a/components/ILIAS/TestQuestionPool/tests/ExportImport/StateHolderTest.php +++ b/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportContextHolderTest.php @@ -20,13 +20,13 @@ use ILIAS\Export\ExportHandler\I\Consumer\ExportConfig\CollectionInterface as ExportConfig; use ILIAS\Export\ExportHandler\I\Target\HandlerInterface as ExportTarget; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\StateHolder; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Export\ExportContextHolder; -class StateHolderTest extends assBaseTestCase +class ExportContextHolderTest extends assBaseTestCase { - public function testCreateReplacesExistingState(): void + public function testCreateReplacesExistingContext(): void { - $holder = new StateHolder(); + $holder = new ExportContextHolder(); $config = $this->createMock(ExportConfig::class); $first_target = $this->createMock(ExportTarget::class); $second_target = $this->createMock(ExportTarget::class); @@ -40,9 +40,9 @@ public function testCreateReplacesExistingState(): void $this->assertSame('second', $second->getOption()); } - public function testClearRemovesExistingState(): void + public function testClearRemovesExistingContext(): void { - $holder = new StateHolder(); + $holder = new ExportContextHolder(); $holder->create( $this->createMock(ExportTarget::class), $this->createMock(ExportConfig::class) diff --git a/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php b/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php index 1359d14fe21e..f56f57e92d1d 100644 --- a/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php +++ b/components/ILIAS/TestQuestionPool/tests/ExportImport/ExportImportCharacterizationTest.php @@ -25,7 +25,7 @@ use ILIAS\Test\ExportImport\TransformationsBuilder as TestTransformationsBuilder; use ILIAS\Test\TestDIC; use ILIAS\TestQuestionPool\ExportImport\Foundation\Serialize\Deserializer; -use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportContext; +use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSession; use ILIAS\TestQuestionPool\ExportImport\Foundation\Import\ImportSessionRepository; use ILIAS\TestQuestionPool\ExportImport\Foundation\Queue\Processor; use ILIAS\TestQuestionPool\ExportImport\Foundation\Normalize\Processors\DenormalizeCarry; @@ -317,8 +317,8 @@ public function testXmlSerializerRejectsMismatchedGroups(): void public function testImportSessionRepositoryPersistsNamedContextAsJson(): void { $repository = new ImportSessionRepository('characterization'); - $original = new ImportContext(); - $context = $original + $original = new ImportSession(); + $session = $original ->withFileToImport('/tmp/import.zip') ->withComponentImportFile('/tmp/import/qpl_data.xml') ->withImportBaseDir('/tmp/import') @@ -347,10 +347,10 @@ public function testImportSessionRepositoryPersistsNamedContextAsJson(): void ]); $repository->setCurrentStageIndex(3); - $repository->setContext($context); + $repository->setSession($session); $this->assertFalse($original->hasFileToImport()); - $this->assertSame('/tmp/import.zip', $context->fileToImport()); + $this->assertSame('/tmp/import.zip', $session->fileToImport()); $payload = ilSession::get('import_stage_characterization_context'); $this->assertIsString($payload); @@ -359,7 +359,7 @@ public function testImportSessionRepositoryPersistsNamedContextAsJson(): void $this->assertSame('/tmp/import.zip', $decoded['file_to_import']); $this->assertSame(123, $decoded['install_id']); - $restored = $repository->getContext(); + $restored = $repository->getSession(); $this->assertSame(3, $repository->getCurrentStageIndex()); $this->assertSame('/tmp/import.zip', $restored->fileToImport()); $this->assertSame('/tmp/import/qpl_data.xml', $restored->componentImportFile()); @@ -404,7 +404,7 @@ public function testImportSessionRepositoryPersistsNamedContextAsJson(): void $repository->clear(); $this->assertSame(0, $repository->getCurrentStageIndex()); - $empty = $repository->getContext(); + $empty = $repository->getSession(); $this->assertFalse($empty->hasFileToImport()); $this->assertSame([], $empty->selectedQuestionIds()); $this->assertFalse($empty->isLegacyImport()); @@ -421,7 +421,7 @@ public function testImportSessionRepositoryIgnoresUnknownJsonKeys(): void ], JSON_THROW_ON_ERROR) ); - $restored = $repository->getContext(); + $restored = $repository->getSession(); $decoded = json_decode($restored->toJson(), true); $this->assertSame('/tmp/import.zip', $restored->fileToImport()); @@ -429,22 +429,22 @@ public function testImportSessionRepositoryIgnoresUnknownJsonKeys(): void $this->assertArrayNotHasKey('bridge_tmp', $decoded); } - public function testImportContextIsLegacyImportOnlyWhenBothLegacyPathsAreSet(): void + public function testImportSessionIsLegacyImportOnlyWhenBothLegacyPathsAreSet(): void { $repository = new ImportSessionRepository('characterization'); - $repository->setContext( - (new ImportContext())->withLegacyQtiFile('/tmp/qti.xml') + $repository->setSession( + (new ImportSession())->withLegacyQtiFile('/tmp/qti.xml') ); - $this->assertFalse($repository->getContext()->isLegacyImport()); + $this->assertFalse($repository->getSession()->isLegacyImport()); - $repository->setContext( - (new ImportContext()) + $repository->setSession( + (new ImportSession()) ->withLegacyQtiFile('/tmp/qti.xml') ->withLegacyXmlFile('/tmp/qpl.xml') ); - $this->assertTrue($repository->getContext()->isLegacyImport()); + $this->assertTrue($repository->getSession()->isLegacyImport()); } private function deserializeGroup(Deserializer $deserializer, string $group): array From 1c848ba3fd08261e84f0af8dffadd2f1fda3def8 Mon Sep 17 00:00:00 2001 From: Lukas Eichenauer Date: Fri, 23 Oct 2026 11:00:36 +0200 Subject: [PATCH 66/66] fix: remove gamification settings in export normalizing --- .../Settings/ScoreReporting/ScoreSettings.php | 9 +++---- .../Scoring/Settings/ScoreSettingsTest.php | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/components/ILIAS/Test/src/Settings/ScoreReporting/ScoreSettings.php b/components/ILIAS/Test/src/Settings/ScoreReporting/ScoreSettings.php index ae83f277ec75..afbbafa8e82a 100755 --- a/components/ILIAS/Test/src/Settings/ScoreReporting/ScoreSettings.php +++ b/components/ILIAS/Test/src/Settings/ScoreReporting/ScoreSettings.php @@ -88,8 +88,7 @@ public function getArrayForLog(AdditionalInformationGenerator $additional_info): { return $this->settings_scoring->toLog($additional_info) + $this->settings_result_summary->toLog($additional_info) - + $this->settings_result_details->toLog($additional_info) - + $this->settings_gamification->toLog($additional_info); + + $this->settings_result_details->toLog($additional_info); } public function toExport(): array @@ -97,8 +96,7 @@ public function toExport(): array return [ 'settings_scoring' => $this->settings_scoring->toExport(), 'settings_result_summary' => $this->settings_result_summary->toExport(), - 'settings_result_details' => $this->settings_result_details->toExport(), - 'settings_gamification' => $this->settings_gamification->toExport() + 'settings_result_details' => $this->settings_result_details->toExport() ]; } @@ -108,8 +106,7 @@ public static function fromExport(array $data): static $data['id'] ?? -1, SettingsScoring::fromExport($data['settings_scoring']), SettingsResultSummary::fromExport($data['settings_result_summary']), - SettingsResultDetails::fromExport($data['settings_result_details']), - SettingsGamification::fromExport($data['settings_gamification']) + SettingsResultDetails::fromExport($data['settings_result_details']) ); } } diff --git a/components/ILIAS/Test/tests/Scoring/Settings/ScoreSettingsTest.php b/components/ILIAS/Test/tests/Scoring/Settings/ScoreSettingsTest.php index cd87d24ea795..d4253cbbd81c 100755 --- a/components/ILIAS/Test/tests/Scoring/Settings/ScoreSettingsTest.php +++ b/components/ILIAS/Test/tests/Scoring/Settings/ScoreSettingsTest.php @@ -49,6 +49,31 @@ public function testScoreSettingsBuild(): void $this->assertInstanceOf(SettingsResultDetails::class, $s->getResultDetailsSettings()); } + public function testScoreSettingsExportRoundtripWithoutGamification(): void + { + $s = new ScoreSettings( + 42, + (new SettingsScoring())->withCountSystem(1)->withScoreCutting(0)->withPassScoring(0), + (new SettingsResultSummary())->withShowGradingStatusEnabled(true), + (new SettingsResultDetails())->withResultsPresentation(1)->withExportSettings(2) + ); + + $export = $s->toExport(); + + $this->assertSame( + [ + 'settings_scoring', + 'settings_result_summary', + 'settings_result_details', + ], + array_keys($export) + ); + + $restored = ScoreSettings::fromExport(['id' => 42] + $export); + $this->assertSame(42, $restored->getId()); + $this->assertEquals($export, $restored->toExport()); + } + public function testScoreSettingsScoring(): void { $s = new SettingsScoring();