Skip to content

Commit

Permalink
Merge pull request #367 from oat-sa/feat/TR-5381/add-preconditions-fr…
Browse files Browse the repository at this point in the history
…om-test-parts

feat: make the first item consider testPart pre-conditions
  • Loading branch information
gabrielfs7 authored Oct 13, 2023
2 parents 3b95de9 + a5b719a commit 6e7e419
Show file tree
Hide file tree
Showing 14 changed files with 560 additions and 70 deletions.
5 changes: 5 additions & 0 deletions src/qtism/common/collections/AbstractCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ public function count(): int
return count($this->dataPlaceHolder);
}

public function isEmpty(): bool
{
return empty($this->dataPlaceHolder);
}

/**
* Return the current element of the collection while iterating.
*
Expand Down
16 changes: 16 additions & 0 deletions src/qtism/data/AssessmentSection.php
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,15 @@ public function getSectionParts(): SectionPartCollection
*/
public function setSectionParts(SectionPartCollection $sectionParts): void
{
if (!$sectionParts->isEmpty()) {
/** @var SectionPart $sectionPart */
foreach ($sectionParts as $sectionPart) {
$sectionPart->setParent($this);
}

$sectionPart->setIsLast(true);
}

$this->sectionParts = $sectionParts;
}

Expand Down Expand Up @@ -320,4 +329,11 @@ public function getComponents(): QtiComponentCollection

return new QtiComponentCollection($comp);
}

public function isLastSectionPart(SectionPart $sectionPart): bool
{
$sectionParts = $this->getSectionParts()->getArrayCopy();

return end($sectionParts) === $sectionPart;
}
}
24 changes: 24 additions & 0 deletions src/qtism/data/SectionPart.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ class SectionPart extends QtiComponent implements QtiIdentifiable, Shufflable
*/
private $timeLimits = null;

private ?SectionPart $parent = null;

private bool $isLast = false;

/**
* Create a new instance of SectionPart.
*
Expand Down Expand Up @@ -363,4 +367,24 @@ public function __clone()
// Reset observers.
$this->setObservers(new SplObjectStorage());
}

public function getParent(): ?SectionPart
{
return $this->parent;
}

public function setParent(SectionPart $parent): void
{
$this->parent = $parent;
}

public function isLast(): bool
{
return $this->isLast;
}

public function setIsLast(bool $isLast): void
{
$this->isLast = $isLast;
}
}
35 changes: 23 additions & 12 deletions src/qtism/data/TestPart.php
Original file line number Diff line number Diff line change
Expand Up @@ -348,20 +348,24 @@ public function getAssessmentSections(): SectionPartCollection
*/
public function setAssessmentSections(SectionPartCollection $assessmentSections): void
{
if (count($assessmentSections) > 0) {
// Check that we have only AssessmentSection and/ord AssessmentSectionRef objects.
foreach ($assessmentSections as $assessmentSection) {
if (!$assessmentSection instanceof AssessmentSection && !$assessmentSection instanceof AssessmentSectionRef) {
$msg = 'A TestPart contain only contain AssessmentSection or AssessmentSectionRef objects.';
throw new InvalidArgumentException($msg);
}
}
if ($assessmentSections->isEmpty()) {
throw new InvalidArgumentException('A TestPart must contain at least one AssessmentSection.');
}

$this->assessmentSections = $assessmentSections;
} else {
$msg = 'A TestPart must contain at least one AssessmentSection.';
throw new InvalidArgumentException($msg);
foreach ($assessmentSections as $assessmentSection) {
if (
!$assessmentSection instanceof AssessmentSection
&& !$assessmentSection instanceof AssessmentSectionRef
) {
throw new InvalidArgumentException(
'A TestPart contain only contain AssessmentSection or AssessmentSectionRef objects.'
);
}
}

$assessmentSection->setIsLast(true);

$this->assessmentSections = $assessmentSections;
}

/**
Expand Down Expand Up @@ -419,4 +423,11 @@ public function __clone()
{
$this->setObservers(new SplObjectStorage());
}

public function isLastSection(AssessmentSection $assessmentSection): bool
{
$sections = $this->getAssessmentSections()->getArrayCopy();

return end($sections) === $assessmentSection;
}
}
14 changes: 4 additions & 10 deletions src/qtism/runtime/tests/AbstractSessionManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use qtism\data\IAssessmentItem;
use qtism\data\NavigationMode;
use qtism\data\SubmissionMode;
use qtism\data\TestPart;

/**
* The AbstractSessionManager class is a bed for instantiating
Expand Down Expand Up @@ -159,6 +160,7 @@ protected function createRoute(AssessmentTest $test): Route
{
$routeStack = [];

/** @var TestPart $testPart */
foreach ($test->getTestParts() as $testPart) {
$assessmentSectionStack = [];

Expand Down Expand Up @@ -203,16 +205,6 @@ protected function createRoute(AssessmentTest $test): Route
$route->appendRoute($r);
}

// Add to the last item of the selection the branch rules of the AssessmentSection/testPart
// on which the selection is applied... Only if the route contains something (empty assessmentSection edge case).
if ($route->count() > 0) {
$route->getLastRouteItem()->addBranchRules($current->getBranchRules());

// Do the same as for branch rules for pre conditions, except that they must be
// attached on the first item of the route.
$route->getFirstRouteItem()->addPreConditions($current->getPreConditions());
}

array_push($routeStack, $route);
array_pop($assessmentSectionStack);
} elseif ($current instanceof AssessmentItemRef) {
Expand All @@ -227,6 +219,8 @@ protected function createRoute(AssessmentTest $test): Route

$finalRoutes = $routeStack;
$route = new SelectableRoute();

/** @var SelectableRoute $finalRoute */
foreach ($finalRoutes as $finalRoute) {
$route->appendRoute($finalRoute);
}
Expand Down
102 changes: 82 additions & 20 deletions src/qtism/runtime/tests/AssessmentTestSession.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
use qtism\data\IAssessmentItem;
use qtism\data\NavigationMode;
use qtism\data\processing\ResponseProcessing;
use qtism\data\rules\BranchRule;
use qtism\data\rules\PreConditionCollection;
use qtism\data\ShowHide;
use qtism\data\state\Weight;
use qtism\data\storage\php\PhpStorageException;
Expand Down Expand Up @@ -2406,10 +2408,12 @@ protected function nextRouteItem($ignoreBranchings = false, $ignorePreConditions
$stop = false;

while ($route->valid() === true && $stop === false) {
$branchRules = $route->current()->getEffectiveBranchRules();
$numberOfBranchRules = $branchRules->count();

// Branchings?
if ($ignoreBranchings === false && count($route->current()->getBranchRules()) > 0 && $this->mustApplyBranchRules() === true) {
$branchRules = $route->current()->getBranchRules();
for ($i = 0; $i < count($branchRules); $i++) {
if ($ignoreBranchings === false && $numberOfBranchRules > 0 && $this->mustApplyBranchRules() === true) {
for ($i = 0; $i < $numberOfBranchRules; $i++) {
$engine = new ExpressionEngine($branchRules[$i]->getExpression(), $this);
$condition = $engine->process();
if ($condition !== null && $condition->getValue() === true) {
Expand Down Expand Up @@ -2438,20 +2442,7 @@ protected function nextRouteItem($ignoreBranchings = false, $ignorePreConditions
}

// Preconditions on target?
if ($ignorePreConditions === false && $route->valid() === true && ($preConditions = $route->current()->getPreConditions()) && count($preConditions) > 0 && $this->mustApplyPreConditions() === true) {
for ($i = 0; $i < count($preConditions); $i++) {
$engine = new ExpressionEngine($preConditions[$i]->getExpression(), $this);
$condition = $engine->process();

if ($condition !== null && $condition->getValue() === true) {
// The item must be presented.
$stop = true;
break;
}
}
} else {
$stop = true;
}
$stop = !($ignorePreConditions === false) || $this->routeMatchesPreconditions($route);

// After a first iteration, we will not performed branching again, as they are executed
// as soon as we leave an item. Chains of branch rules are not expected.
Expand All @@ -2465,6 +2456,47 @@ protected function nextRouteItem($ignoreBranchings = false, $ignorePreConditions
}
}

public function routeMatchesPreconditions(Route $route = null): bool
{
$route = $route ?? $this->getRoute();

if (!$route->valid()) {
return true;
}

$routeItem = $route->current();
$testPart = $routeItem->getTestPart();
$navigationMode = $testPart->getNavigationMode();

if ($navigationMode === NavigationMode::LINEAR || $this->mustForcePreconditions()) {
return $this->preConditionsMatch($routeItem->getEffectivePreConditions());
}

if ($navigationMode === NavigationMode::NONLINEAR) {
return $this->preConditionsMatch($testPart->getPreConditions());
}

return true;
}

private function preConditionsMatch(PreConditionCollection $preConditions): bool
{
if ($preConditions->count() === 0) {
return true;
}

for ($i = 0; $i < $preConditions->count(); $i++) {
$engine = new ExpressionEngine($preConditions[$i]->getExpression(), $this);
$condition = $engine->process();

if ($condition === null || $condition->getValue() === false) {
return false;
}
}

return true;
}

/**
* Set the position in the Route at the very next TestPart in the Route sequence or, if the current
* testPart is the last one of the test session, the test session ends gracefully. If the submission mode
Expand All @@ -2483,8 +2515,21 @@ public function moveNextTestPart(): void

$route = $this->getRoute();
$from = $route->current();
$branchRules = $from->getTestPart()->getBranchRules();

while ($route->valid() === true && $route->current()->getTestPart() === $from->getTestPart()) {
/** @var BranchRule $branchRule */
foreach ($branchRules as $branchRule) {
$engine = new ExpressionEngine($branchRule->getExpression(), $this);
$condition = $engine->process();

if ($condition !== null && $condition->getValue() === true) {
$route->branch($branchRule->getTarget());

break 2;
}
}

$route->next();
}

Expand Down Expand Up @@ -3053,7 +3098,7 @@ public function isNextRouteItemPredictible(): bool
}

// Case 4. The next item has preconditions.
if ($this->mustApplyPreConditions(true) && count($this->getRoute()->getNext()->getPreConditions()) > 0) {
if ($this->mustApplyPreConditions(true)) {
return false;
}

Expand Down Expand Up @@ -3133,7 +3178,24 @@ protected function mustApplyBranchRules(): bool
*/
protected function mustApplyPreConditions($nextRouteItem = false): bool
{
$routeItem = ($nextRouteItem === false) ? $this->getCurrentRouteItem() : $this->getRoute()->getNext();
return ($routeItem->getTestPart()->getNavigationMode() === NavigationMode::LINEAR || $this->mustForcePreconditions() === true);
if ($this->mustForcePreconditions()) {
return true;
}

$routeItem = $nextRouteItem === false ? $this->getCurrentRouteItem() : $this->getRoute()->getNext();

if (!$routeItem instanceof RouteItem) {
return false;
}

$testPart = $routeItem->getTestPart();
$navigationMode = $testPart->getNavigationMode();

if ($navigationMode === NavigationMode::LINEAR) {
return $routeItem->getEffectivePreConditions()->count() > 0;
}

// Now NonLinear Test part pre-conditions must be considered
return $navigationMode === NavigationMode::NONLINEAR && $testPart->getPreConditions()->count() > 0;
}
}
26 changes: 10 additions & 16 deletions src/qtism/runtime/tests/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -1117,10 +1117,11 @@ public function branch($identifier): void

if ($targetRouteItems[$occurence]->getTestPart() !== $this->current()->getTestPart()) {
// From IMS QTI:
// In case of an item or section, the target must refer to an item or section
// in the same testPart [...]
$msg = 'Branchings to items outside of the current testPart is forbidden by the QTI 2.1 specification.';
throw new OutOfBoundsException($msg);
// In the case of an item or section, the target must refer to an item or section in the same test-part
// that has not yet been presented.
$this->next();

return;
}

$this->setPosition($this->getRouteItemPosition($targetRouteItems[$occurence]));
Expand All @@ -1133,10 +1134,11 @@ public function branch($identifier): void
if (isset($assessmentSectionIdentifierMap[$id])) {
if ($assessmentSectionIdentifierMap[$id][0]->getTestPart() !== $this->current()->getTestPart()) {
// From IMS QTI:
// In case of an item or section, the target must refer to an item or section
// in the same testPart [...]
$msg = 'Branchings to assessmentSections outside of the current testPart is forbidden by the QTI 2.1 specification.';
throw new OutOfBoundsException($msg);
// In the case of an item or section, the target must refer to an item or section in the same test-part
// that has not yet been presented.
$this->next();

return;
}

// We branch to the first RouteItem belonging to the section.
Expand All @@ -1148,14 +1150,6 @@ public function branch($identifier): void
// Check for a testPart.
$testPartIdentifierMap = $this->getTestPartIdentifierMap();
if (isset($testPartIdentifierMap[$id])) {
// We branch to the first RouteItem belonging to the testPart.
if ($testPartIdentifierMap[$id][0]->getTestPart() === $this->current()->getTestPart()) {
// From IMS QTI:
// For testParts, the target must refer to another testPart.
$msg = 'Cannot branch to the same testPart.';
throw new OutOfBoundsException($msg);
}

// We branch to the first RouteItem belonging to the testPart.
$this->setPosition($this->getRouteItemPosition($testPartIdentifierMap[$id][0]));

Expand Down
Loading

0 comments on commit 6e7e419

Please sign in to comment.