diff --git a/qtism/data/AssessmentSectionCollection.php b/qtism/data/AssessmentSectionCollection.php index 34fa43c32..9cb4167a2 100644 --- a/qtism/data/AssessmentSectionCollection.php +++ b/qtism/data/AssessmentSectionCollection.php @@ -32,7 +32,7 @@ * @author Jérôme Bogaerts * */ -class AssessmentSectionCollection extends QtiIdentifiableCollection { +class AssessmentSectionCollection extends SectionPartCollection { /** * Check if $value is an AssessmentSection object. diff --git a/qtism/data/QtiComponentIterator.php b/qtism/data/QtiComponentIterator.php index 05faf6006..b27f0fa90 100644 --- a/qtism/data/QtiComponentIterator.php +++ b/qtism/data/QtiComponentIterator.php @@ -299,7 +299,11 @@ public function rewind() { $root = $this->getRootComponent(); $this->pushOnTrail($root, $root->getComponents()); + $hasTrail = false; + $foundClass = false; + while(count($this->getTrail()) > 0) { + $hasTrail = true; $trailEntry = $this->popFromTrail(); $this->setValid(true); @@ -309,11 +313,12 @@ public function rewind() { $this->pushOnTrail($this->getCurrentComponent(), $this->getCurrentComponent()->getComponents()); if (empty($classes) === true || in_array($this->getCurrentComponent()->getQtiClassName(), $classes) === true) { + $foundClass = true; break; } } - if (count($this->getTrail()) === 0) { + if (count($this->getTrail()) === 0 && (($hasTrail && !$foundClass) || (!$hasTrail))) { $this->setValid(false); $this->setCurrentComponent(null); $this->setCurrentContainer(null); diff --git a/qtism/data/TestPart.php b/qtism/data/TestPart.php index 26a37518f..3b18b5e83 100644 --- a/qtism/data/TestPart.php +++ b/qtism/data/TestPart.php @@ -115,7 +115,7 @@ class TestPart extends QtiComponent implements QtiIdentifiable { * * The items contained in each testPart are arranged into sections and sub-sections. * - * @var AssessmentSectionCollection + * @var SectionPartCollection * @qtism-bean-property */ private $assessmentSections; @@ -141,12 +141,12 @@ class TestPart extends QtiComponent implements QtiIdentifiable { * Create a new instance of TestPart. * * @param string $identifier A QTI Identifier; - * @param AssessmentSectionCollection $assessmentSections A collection of AssessmentSection objects. + * @param @param SectionPartCollection $assessmentSections A collection of AssessmentSection or AssessmentSectionRef objects objects. * @param int $navigationMode A value of the NavigationMode enumeration. * @param int $submissionMode A value of the SubmissionMode enumeration. * @throws InvalidArgumentException If an argument has the wrong type or format. */ - public function __construct($identifier, AssessmentSectionCollection $assessmentSections, $navigationMode = NavigationMode::LINEAR, $submissionMode = SubmissionMode::INDIVIDUAL) { + public function __construct($identifier, SectionPartCollection $assessmentSections, $navigationMode = NavigationMode::LINEAR, $submissionMode = SubmissionMode::INDIVIDUAL) { $this->setObservers(new SplObjectStorage()); $this->setIdentifier($identifier); @@ -329,22 +329,30 @@ public function hasTimeLimits() { } /** - * Set the AssessmentSection that are part of this Test Part. + * Get the AssessmentSections and/or AssessmentSectionRefs that are part of this Test Part. * - * @return AssessmentSectionCollection A collection of AssessmentSection object. + * @return \qtism\data\SectionPartCollection A collection of AssessmentSection and/or AssessmentSectionRef objects. */ public function getAssessmentSections() { return $this->assessmentSections; } /** - * Set the AssessmentSection that are part of this Test Part. + * Set the AssessmentSections and/or AssessmentSectionRefs that are part of this Test Part. * - * @param AssessmentSectionCollection $assessmentSections A collection of AssessmentSection objects. - * @throws InvalidArgumentException If $assessmentSections is an empty collection. + * @param SectionPartCollection $assessmentSections A collection of AssessmentSection and/or AssessmentSectionRef objects. + * @throws \InvalidArgumentException If $assessmentSections is an empty collection or contains something else than AssessmentSection and/or AssessmentSectionRef objects. */ - public function setAssessmentSections(AssessmentSectionCollection $assessmentSections) { + public function setAssessmentSections(SectionPartCollection $assessmentSections) { 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); + } + } + $this->assessmentSections = $assessmentSections; } else { diff --git a/qtism/data/storage/xml/XmlDocument.php b/qtism/data/storage/xml/XmlDocument.php index 61ebf98c6..42ac7fd21 100644 --- a/qtism/data/storage/xml/XmlDocument.php +++ b/qtism/data/storage/xml/XmlDocument.php @@ -30,6 +30,8 @@ use qtism\data\QtiDocument; use qtism\data\storage\xml\marshalling\MarshallerFactory; use qtism\data\AssessmentTest; +use qtism\data\AssessmentSectionRef; +use qtism\data\TestPart; use qtism\data\content\Flow; use qtism\data\storage\xml\marshalling\Marshaller; use qtism\data\storage\xml\marshalling\UnmarshallingException; @@ -354,6 +356,54 @@ public function xInclude($validate = false) { throw new LogicException($msg); } } + + public function includeAssessmentSectionRefs($validate = false) + { + if (($root = $this->getDocumentComponent()) !== null) { + + $baseUri = str_replace('\\', '/', $this->getDomDocument()->documentElement->baseURI); + $pathinfo = pathinfo($baseUri); + $basePath = $pathinfo['dirname']; + + $count = count($root->getComponentsByClassName('assessmentSectionRef')); + while ($count > 0) { + $iterator = new QtiComponentIterator($root, array('assessmentSectionRef')); + foreach ($iterator as $assessmentSectionRef) { + $parent = $iterator->parent(); + $href = $assessmentSectionRef->getHref(); + + if (Url::isRelative($href) === true) { + $href = Url::rtrim($basePath) . '/' . Url::ltrim($href); + + $doc = new XmlDocument(); + $doc->load($href, $validate); + $sectionRoot = $doc->getDocumentComponent(); + + foreach ($sectionRoot->getComponentsByClassName(array('assessmentSectionRef', 'assessmentItemRef')) as $sectionPart) { + $newBasePath = Url::ltrim(str_replace($basePath, '', $href)); + $pathinfo = pathinfo($newBasePath); + $newHref = $pathinfo['dirname'] . '/' . Url::ltrim($sectionPart->getHref()); + $sectionPart->setHref($newHref); + } + + if ($parent instanceof TestPart) { + $collection = $parent->getAssessmentSections(); + } else { + $collection = $parent->getSectionParts(); + } + + $collection->detach($assessmentSectionRef); + $collection->attach($sectionRoot); + } + } + + $count = count($root->getComponentsByClassName('assessmentSectionRef')); + } + } else { + $msg = "Cannot resolve assessmentSectionRefs before loading any file."; + throw new LogicException($msg); + } + } /** * Decorate the root element of the XmlAssessmentDocument with the appropriate diff --git a/qtism/data/storage/xml/marshalling/TestPartMarshaller.php b/qtism/data/storage/xml/marshalling/TestPartMarshaller.php index 98547e1af..81d793fe6 100644 --- a/qtism/data/storage/xml/marshalling/TestPartMarshaller.php +++ b/qtism/data/storage/xml/marshalling/TestPartMarshaller.php @@ -28,7 +28,7 @@ use qtism\data\TestPart; use qtism\data\TestFeedbackCollection; use qtism\data\ItemSessionControl; -use qtism\data\AssessmentSectionCollection; +use qtism\data\SectionPartCollection; use qtism\data\rules\PreConditionCollection; use qtism\data\rules\BranchRuleCollection; use qtism\data\TimeLimits; @@ -97,8 +97,8 @@ protected function unmarshall(DOMElement $element) { // We do not use the regular DOMElement::getElementsByTagName method // because it is recursive. We only want the first level elements with // tagname = 'assessmentSection'. - $assessmentSectionElts = self::getChildElementsByTagName($element, 'assessmentSection'); - $assessmentSections = new AssessmentSectionCollection(); + $assessmentSectionElts = self::getChildElementsByTagName($element, array('assessmentSection', 'assessmentSectionRef')); + $assessmentSections = new SectionPartCollection(); foreach ($assessmentSectionElts as $sectElt) { $marshaller = $this->getMarshallerFactory()->createMarshaller($sectElt); $assessmentSections[] = $marshaller->unmarshall($sectElt); diff --git a/test/qtism/data/QtiComponentIteratorTest.php b/test/qtism/data/QtiComponentIteratorTest.php index daa2ca8f0..016cbdbd6 100644 --- a/test/qtism/data/QtiComponentIteratorTest.php +++ b/test/qtism/data/QtiComponentIteratorTest.php @@ -77,4 +77,17 @@ public function testClassSelection() { $this->assertEquals(7, $i); } -} \ No newline at end of file + + public function testOneChildComponents() { + $baseValues = new ExpressionCollection(); + $baseValues[] = new BaseValue(BaseType::FLOAT, 0.5); + $sum = new Sum($baseValues); + $iterator = new QtiComponentIterator($sum); + + $iterations = 0; + foreach ($iterator as $k => $i) { + $iterations++; + } + $this->assertEquals(1, $iterations); + } +} diff --git a/test/qtism/data/storage/xml/XmlAssessmentTestDocumentTest.php b/test/qtism/data/storage/xml/XmlAssessmentTestDocumentTest.php index a63e7eaff..87aa53d51 100644 --- a/test/qtism/data/storage/xml/XmlAssessmentTestDocumentTest.php +++ b/test/qtism/data/storage/xml/XmlAssessmentTestDocumentTest.php @@ -90,8 +90,49 @@ public function testItemSessionControls() { $this->assertInstanceOf('qtism\\data\\TestPart', $p02); $this->assertEquals(4, $p02->getItemSessionControl()->getMaxAttempts()); } + + public function testAssessmentSectionRefsInTestParts() { + $doc = new XmlDocument(); + $doc->load(self::samplesDir() . 'custom/tests/nested_assessment_section_refs/test_definition/test.xml', true); + + $testParts = $doc->getDocumentComponent()->getTestParts(); + $this->assertTrue(isset($testParts['T01'])); + + $sectionParts = $testParts['T01']->getAssessmentSections(); + $this->assertTrue(isset($sectionParts['SR01'])); + $this->assertInstanceOf('qtism\\data\\AssessmentSectionRef', $sectionParts['SR01']); + } + + public function testIncludeAssessmentSectionRefsInTestParts() { + $doc = new XmlDocument(); + $doc->load(self::samplesDir() . 'custom/tests/nested_assessment_section_refs/test_definition/test.xml', true); + $doc->includeAssessmentSectionRefs(); + + $root = $doc->getDocumentComponent(); + + $testParts = $root->getTestParts(); + $this->assertTrue(isset($testParts['T01'])); + + // Check that assessmentSectionRef 'SR01' has been resolved. + $sectionParts = $testParts['T01']->getAssessmentSections(); + + $this->assertTrue(isset($sectionParts['S01'])); + $this->assertFalse(isset($sectionParts['SR01'])); + $this->assertTrue(isset($sectionParts['S01']->getSectionParts()['S02'])); + + // Check that the final assessmentSection contains the assessmentItemRefs. + $assessmentItemRefs = $sectionParts['S01']->getSectionParts()['S02']->getSectionParts(); + $this->assertEquals(3, count($assessmentItemRefs)); + + $this->assertInstanceOf('qtism\\data\\AssessmentItemRef', $assessmentItemRefs['Q01']); + $this->assertEquals('../sections/../sections/../items/question1.xml', $assessmentItemRefs['Q01']->getHref()); + $this->assertInstanceOf('qtism\\data\\AssessmentItemRef', $assessmentItemRefs['Q02']); + $this->assertEquals('../sections/../sections/../items/question2.xml', $assessmentItemRefs['Q02']->getHref()); + $this->assertInstanceOf('qtism\\data\\AssessmentItemRef', $assessmentItemRefs['Q03']); + $this->assertEquals('../sections/../sections/../items/question3.xml', $assessmentItemRefs['Q03']->getHref()); + } private static function decorateUri($uri) { return dirname(__FILE__) . '/../../../../samples/ims/tests/' . $uri; } -} \ No newline at end of file +} diff --git a/test/samples/custom/tests/nested_assessment_section_refs/items/question1.xml b/test/samples/custom/tests/nested_assessment_section_refs/items/question1.xml new file mode 100644 index 000000000..de59124d9 --- /dev/null +++ b/test/samples/custom/tests/nested_assessment_section_refs/items/question1.xml @@ -0,0 +1,21 @@ + + + + + ChoiceA + + + + + + What is the correct response? + Choice A + Choice B + Choice C + + + + diff --git a/test/samples/custom/tests/nested_assessment_section_refs/items/question2.xml b/test/samples/custom/tests/nested_assessment_section_refs/items/question2.xml new file mode 100644 index 000000000..f9c9897d1 --- /dev/null +++ b/test/samples/custom/tests/nested_assessment_section_refs/items/question2.xml @@ -0,0 +1,21 @@ + + + + + ChoiceB + + + + + + What is the correct response? + Choice A + Choice B + Choice C + + + + diff --git a/test/samples/custom/tests/nested_assessment_section_refs/items/question3.xml b/test/samples/custom/tests/nested_assessment_section_refs/items/question3.xml new file mode 100644 index 000000000..d00a6fc22 --- /dev/null +++ b/test/samples/custom/tests/nested_assessment_section_refs/items/question3.xml @@ -0,0 +1,21 @@ + + + + + ChoiceC + + + + + + What is the correct response? + Choice A + Choice B + Choice C + + + + diff --git a/test/samples/custom/tests/nested_assessment_section_refs/sections/section_ref1.xml b/test/samples/custom/tests/nested_assessment_section_refs/sections/section_ref1.xml new file mode 100644 index 000000000..15dd373f1 --- /dev/null +++ b/test/samples/custom/tests/nested_assessment_section_refs/sections/section_ref1.xml @@ -0,0 +1,4 @@ + + + + diff --git a/test/samples/custom/tests/nested_assessment_section_refs/sections/section_ref2.xml b/test/samples/custom/tests/nested_assessment_section_refs/sections/section_ref2.xml new file mode 100644 index 000000000..4905ff333 --- /dev/null +++ b/test/samples/custom/tests/nested_assessment_section_refs/sections/section_ref2.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/samples/custom/tests/nested_assessment_section_refs/test_definition/test.xml b/test/samples/custom/tests/nested_assessment_section_refs/test_definition/test.xml new file mode 100644 index 000000000..5a55f8cad --- /dev/null +++ b/test/samples/custom/tests/nested_assessment_section_refs/test_definition/test.xml @@ -0,0 +1,6 @@ + + + + + +