protected function buildResponseProcessing($validation, $isCaseSensitive = true)
 {
     // Guess question type
     $validationClazz = new \ReflectionClass($validation);
     $questionType = str_replace('_validation', '', $validationClazz->getShortName());
     if (in_array($questionType, Constants::$questionTypesWithMappingSupport)) {
         $responseProcessing = new ResponseProcessing();
         $responseProcessing->setTemplate(Constants::RESPONSE_PROCESSING_TEMPLATE_MAP_RESPONSE);
         return $responseProcessing;
     }
     if ($validation->get_valid_response()->get_score() != 1) {
         $validation->get_valid_response()->set_score(1);
         LogService::log('Only support mapping to `matchCorrect` template, thus validation score is changed to 1 and since mapped to QTI pre-defined `match_correct.xml` template');
     }
     // Warn and remove `alt_responses` because couldn't support responseDeclaration with multiple valid answers
     if (!empty($validation->get_alt_responses())) {
         $validation->set_alt_responses([]);
         LogService::log('Does not support multiple validation responses for `responseDeclaration`, only use `valid_response`, ignoring `alt_responses`');
     }
     // Warn since we only support match_correct, can't support `$isCaseSensitive`
     if ($isCaseSensitive == false) {
         LogService::log('Only support mapping to `matchCorrect` template, thus case sensitivity is ignored');
     }
     $responseProcessing = new ResponseProcessing();
     $responseProcessing->setTemplate(Constants::RESPONSE_PROCESSING_TEMPLATE_MATCH_CORRECT);
     return $responseProcessing;
 }
 public function getQuestionType()
 {
     /* @var QtiMatchInteraction $interaction */
     $interaction = $this->interaction;
     if ($interaction->mustShuffle()) {
         LogService::log('Shuffle attribute is not supported');
     }
     $simpleMatchSetCollection = $interaction->getSimpleMatchSets();
     $stems = $this->buildOptions($simpleMatchSetCollection[0], $this->stemMapping);
     $options = $this->buildOptions($simpleMatchSetCollection[1], $this->optionsMapping);
     // Build validation
     $validationBuilder = new MatchInteractionValidationBuilder($this->stemMapping, $this->optionsMapping, $this->responseDeclaration);
     $validation = $validationBuilder->buildValidation($this->responseProcessingTemplate);
     if ($interaction->getMaxAssociations() !== count($stems)) {
         LogService::log('Max Association number not equals to number of stems is not supported');
     }
     $uiStyle = new choicematrix_ui_style();
     $uiStyle->set_type('table');
     $isMultipleResponse = $this->isMultipleResponse($interaction);
     $question = new choicematrix('choicematrix', $options, $stems);
     $question->set_multiple_responses($isMultipleResponse);
     $question->set_stimulus($this->getPrompt());
     $question->set_ui_style($uiStyle);
     if ($validation) {
         $question->set_validation($validation);
     }
     return $question;
 }
 public function getQuestionType()
 {
     /* @var QtiExtendedTextInteraction $interaction */
     $interaction = $this->interaction;
     $longtext = new longtext('longtext');
     LogService::log('No validation mapping supported for this interaction. Ignoring any ' . '<responseProcessing> and <responseDeclaration> if any');
     if (!empty($interaction->getPrompt())) {
         $promptContent = $interaction->getPrompt()->getContent();
         $longtext->set_stimulus(QtiMarshallerUtil::marshallCollection($promptContent));
     }
     if ($interaction->getPlaceholderText()) {
         $longtext->set_placeholder($interaction->getPlaceholderText());
     }
     /** As per QTI spec
      *  When multiple strings are accepted, expectedLength applies to each string.
      *  `expectedLength` works as a only as a 'hint' to student so we do not want to force a hard limit
      */
     if ($interaction->getExpectedLength() > 0) {
         $maxStrings = $interaction->getMaxStrings() > 0 ? $interaction->getMaxStrings() : 1;
         $expectedLength = $interaction->getExpectedLength() / 5;
         $longtext->set_max_length($maxStrings * $expectedLength);
         $longtext->set_submit_over_limit(true);
     }
     return $longtext;
 }
示例#4
0
 public function parse($xmlString, $validate = true)
 {
     // TODO: Remove this, and move it higher up
     LogService::flush();
     $xmlDocument = new XmlDocument();
     if ($validate === false) {
         LogService::log('QTI pre-validation is turned off, some invalid attributes might be stripped from XML content upon conversion');
     }
     $xmlDocument->loadFromString($xmlString, $validate);
     /** @var AssessmentItem $assessmentItem */
     $assessmentTest = $xmlDocument->getDocumentComponent();
     if (!$assessmentTest instanceof AssessmentTest) {
         throw new MappingException('XML is not a valid <assessmentItem> document');
     }
     // Ignore `testPart` and `assessmentSection`. Grab every item references and merge in array
     $itemReferences = [];
     foreach ($assessmentTest->getComponentsByClassName('assessmentItemRef', true) as $assessmentItemRef) {
         $itemReferences[] = $assessmentItemRef->getIdentifier();
     }
     LogService::log('Support for mapping is very limited. Elements such `testPart`, `assessmentSections`, `seclection`, `rubricBlock`, ' . 'etc are ignored. Please see developer docs for more details');
     $data = new activity_data();
     $data->set_items($itemReferences);
     $activity = new activity($assessmentTest->getIdentifier(), $data);
     // Flush out all the error messages stored in this static class, also ensure they are unique
     $messages = array_values(array_unique(LogService::flush()));
     return [$activity, $messages];
 }
 public function buildValidation(ResponseProcessingTemplate $responseProcessingTemplate)
 {
     try {
         switch ($responseProcessingTemplate->getTemplate()) {
             case ResponseProcessingTemplate::MATCH_CORRECT:
                 return $this->getMatchCorrectTemplateValidation();
             case ResponseProcessingTemplate::MAP_RESPONSE:
             case ResponseProcessingTemplate::CC2_MAP_RESPONSE:
                 return $this->getMapResponseTemplateValidation();
             case ResponseProcessingTemplate::NONE:
                 if (!empty($this->responseDeclaration)) {
                     // If the response processing template is not set, simply check whether `mapping` or `correctResponse` exists and
                     // simply use `em
                     if (!empty($this->responseDeclaration->getMapping()) && $this->responseDeclaration->getMapping()->getMapEntries()->count() > 0) {
                         LogService::log('Response processing is not set, the `validation` object is assumed to be mapped based on `mapping` map entries elements');
                         return $this->getMapResponseTemplateValidation();
                     }
                     if (!empty($this->responseDeclaration->getCorrectResponse()) && $this->responseDeclaration->getCorrectResponse()->getValues()->count() > 0) {
                         LogService::log('Response processing is not set, the `validation` object is assumed to be mapped based on `correctResponse` values elements');
                         return $this->getMatchCorrectTemplateValidation();
                     }
                 }
                 return $this->getNoTemplateResponsesValidation();
             default:
                 LogService::log('Unrecognised response processing template. Validation is not available');
         }
     } catch (MappingException $e) {
         LogService::log('Validation is not available. Critical error: ' . $e->getMessage());
     }
     return null;
 }
 public function buildItemBody(array $interactions, $content = '')
 {
     // Try to build the <itemBody> according to items` content if exists
     if (empty($content)) {
         return $this->buildItemBodySimple($interactions);
     }
     try {
         return $this->buildItemBodyWithItemContent($interactions, $content);
         // If anything fails, <itemBody> can't be mapped due to whatever reasons
         // Probably simply due to its being wrapped in a tag which only accept inline content
         // Simply build it without considering items` content and put the content on the top
     } catch (\Exception $e) {
         $itemBody = $this->buildItemBodySimple($interactions);
         $itemBodyContent = new BlockCollection();
         // Build the div bundle that contains all the item`s content
         // minus those questions and features `span`
         $html = new SimpleHtmlDom();
         $html->load($content);
         foreach ($html->find('span.learnosity-response') as &$span) {
             $span->outertext = '';
         }
         $div = new Div();
         $contentCollection = QtiMarshallerUtil::unmarshallElement($html->save());
         $div->setContent(ContentCollectionBuilder::buildFlowCollectionContent($contentCollection));
         $itemBodyContent->attach($div);
         $itemBodyContent->merge($itemBody->getComponents());
         $itemBody->setContent($itemBodyContent);
         LogService::log('Interactions are failed to be mapped with `item` content: ' . $e->getMessage() . '. Thus, interactions are separated from its actual `item` content and appended in the bottom');
         return $itemBody;
     }
 }
 public static function build($questionType, $scoringType, array $responses)
 {
     // Validate the `responses` array
     foreach ($responses as $response) {
         if (!$response instanceof ValidResponse) {
             throw new \Exception('Invalid `responses` array. Fail to build validation');
         }
     }
     // Filter out negative values
     $responses = array_filter($responses, function ($response) {
         /** @var ValidResponse $response */
         $isPositiveValue = floatval($response->getScore()) >= 0;
         if (!$isPositiveValue) {
             LogService::log('Ignored validation mapping with negative score');
         }
         return $isPositiveValue;
     });
     // Sort by score value, as the one with highest score would be used for `valid_response` object
     self::susort($responses, function ($a, $b) {
         /**
          * @var ValidResponse $a
          * @var ValidResponse $b
          */
         if ($a->getScore() == $b->getScore()) {
             return 0;
         }
         return $a->getScore() > $b->getScore() ? -1 : 1;
     });
     // Build `valid_response` and its `alt_responses`
     $validResponse = null;
     $altResponses = [];
     foreach ($responses as $response) {
         /** @var ValidResponse $response */
         if (!$validResponse) {
             $validResponseRef = new \ReflectionClass(self::BASE_NS . $questionType . '_validation_valid_response');
             $validResponse = $validResponseRef->newInstance();
             $validResponse->set_score($response->getScore());
             $validResponse->set_value($response->getValue());
         } else {
             $altResponseItemRef = new \ReflectionClass(self::BASE_NS . $questionType . '_validation_alt_responses_item');
             $altResponseItem = $altResponseItemRef->newInstance();
             $altResponseItem->set_score($response->getScore());
             $altResponseItem->set_value($response->getValue());
             $altResponses[] = $altResponseItem;
         }
     }
     // Build dah` validation object
     $validationRef = new \ReflectionClass(self::BASE_NS . $questionType . '_validation');
     $validation = $validationRef->newInstance();
     $validation->set_scoring_type($scoringType);
     if (!empty($validResponse)) {
         $validation->set_valid_response($validResponse);
     }
     if (!empty($altResponses)) {
         $validation->set_alt_responses($altResponses);
     }
     return $validation;
 }
 private function validateInteraction(InlineChoiceInteraction $interaction)
 {
     if (!empty($interaction->mustShuffle())) {
         LogService::log('The attribute `shuffle` is not supported, thus is ignored');
     }
     if (!empty($interaction->isRequired())) {
         LogService::log('The attribute `required` is not supported, thus is ignored');
     }
     return $interaction;
 }
 public function convert(Manifest $manifest, array $rules = [])
 {
     $activityReference = $manifest->getIdentifier();
     $tagsWriter = new TagsWriter();
     // Does not handle submanifest tyvm!
     if (!empty($manifest->getManifest())) {
         LogService::log('Does not handle sub-manifest element thus it is ignored');
     }
     // Atm, we only have tags rules and this need to be validated and user shall be provided with a nice error message
     // TODO: Validation need to be done in future
     $tagRules = isset($rules['tags']) ? $rules['tags'] : [];
     // Let's map package metadatas as activity tags
     // We can write custom replacer or remover to fix the messy `identifier:catalog:` afterwards
     $activityTags = [];
     $metadatas = $manifest->getMetadata();
     if (!empty($metadatas)) {
         $tags = $tagsWriter->convert($metadatas, $tagRules);
         if (!empty($tags)) {
             $activityTags = ['reference' => $activityReference, 'tags' => $tags];
         }
     }
     $itemReferences = [];
     $itemsTags = [];
     // Build item reference and item tags JSON
     $organisations = $manifest->getOrganizations();
     if (!empty($organisations)) {
         foreach ($organisations as $organisation) {
             foreach ($organisation->getItems() as $item) {
                 $itemReferences[] = $item->getIdentifier();
             }
         }
     }
     // Build item reference and item tags JSON
     $resources = $manifest->getResources();
     if (!empty($resources)) {
         foreach ($resources as $resource) {
             // Just add `item` resource as items, and leave css and any other resources alone
             if (StringUtil::startsWith($resource->getType(), 'imsqti_item')) {
                 /** @var Resource $resource */
                 $itemReference = $resource->getIdentifier();
                 $itemReferences[] = $itemReference;
                 $tags = $tagsWriter->convert($resource->getMetadata(), $tagRules);
                 if (!empty($tags)) {
                     $itemsTags[] = ['reference' => $itemReference, 'tags' => $tags];
                 }
             }
         }
     }
     // Build activity JSON
     $activity = ['reference' => $activityReference, 'data' => ['items' => array_values(array_unique($itemReferences))], 'status' => 'published'];
     // Obvious here that these `items` hasn't and wouldn't be validated against
     // Should do it later by the function that calls this
     return [$activity, $activityTags, $itemsTags];
 }
 private function validate(QtiOrderInteraction $interaction)
 {
     if ($interaction->mustShuffle()) {
         LogService::log('Attribute `shuffle` is not supported');
     }
     foreach ($interaction->getSimpleChoices() as $simpleChoice) {
         /** @var SimpleChoice $simpleChoice */
         if ($simpleChoice->isFixed()) {
             LogService::log('Attribute `fixed` for ' . $simpleChoice->getIdentifier() . ' is not supported');
         }
     }
     return true;
 }
 public function map($itemReference, ItemBody $itemBody, QtiComponentCollection $interactionComponents, QtiComponentCollection $responseDeclarations = null, ResponseProcessingTemplate $responseProcessingTemplate = null)
 {
     $this->itemReference = $itemReference;
     $questionsXmls = [];
     $responseDeclarationsMap = [];
     if ($responseDeclarations) {
         /** @var ResponseDeclaration $responseDeclaration */
         foreach ($responseDeclarations as $responseDeclaration) {
             $responseDeclarationsMap[$responseDeclaration->getIdentifier()] = $responseDeclaration;
         }
     }
     foreach ($interactionComponents as $component) {
         /* @var $component Interaction */
         $questionReference = $this->itemReference . '_' . $component->getResponseIdentifier();
         // Process <responseDeclaration>
         $responseDeclaration = isset($responseDeclarationsMap[$component->getResponseIdentifier()]) ? $responseDeclarationsMap[$component->getResponseIdentifier()] : null;
         $mapper = $this->getMapperInstance($component->getQtiClassName(), [$component, $responseDeclaration, $responseProcessingTemplate]);
         $question = $mapper->getQuestionType();
         $this->questions[$questionReference] = new Question($question->get_type(), $questionReference, $question);
         $questionsXmls[$questionReference] = ['qtiClassName' => $component->getQtiClassName(), 'responseIdentifier' => $component->getResponseIdentifier()];
     }
     // Build item's HTML content
     $extraContentHtml = new SimpleHtmlDom();
     if (!$extraContentHtml->load(QtiMarshallerUtil::marshallCollection($itemBody->getComponents()), false)) {
         throw new \Exception('Issues with the content for itemBody, it might not be valid');
     }
     foreach ($questionsXmls as $questionReference => $interactionData) {
         // Append this question span to our `item` content as it is
         $this->content .= '<span class="learnosity-response question-' . $questionReference . '"></span>';
         // Clean up interaction HTML content
         $qtiClassName = $interactionData['qtiClassName'];
         $responseIdentifier = $interactionData['responseIdentifier'];
         $toFind = $qtiClassName . '[responseIdentifier="' . $responseIdentifier . '"]';
         foreach ($extraContentHtml->find($toFind) as &$tag) {
             $tag->outertext = '';
         }
     }
     $extraContent = $extraContentHtml->save();
     // Making assumption question always has stimulus `right`?
     // So, prepend the extra content on the stimulus on the first question
     if (!empty(trim($extraContent))) {
         $firstQuestionReference = key($this->questions);
         $newStimulus = $extraContent . $this->questions[$firstQuestionReference]->get_data()->get_stimulus();
         $this->questions[$firstQuestionReference]->get_data()->set_stimulus($newStimulus);
         LogService::log('Extra <itemBody> content is prepended to question stimulus and please verify as this `might` break item content structure');
     }
     return true;
 }
 protected function getMatchCorrectTemplateValidation()
 {
     // TODO: Validate against mismatch possible responses and correct response
     // Build the `value` object on `valid_response`
     $values = [];
     foreach ($this->responseDeclaration->getCorrectResponse()->getValues() as $v) {
         /** @var Value $v */
         $value = $v->getValue();
         if (!isset($this->orderMapping[$value])) {
             LogService::log('Cannot locate ' . $value . ' in responseDeclaration');
             continue;
         }
         $values[] = $this->orderMapping[$value];
     }
     return ValidationBuilder::build('orderlist', 'exactMatch', [new ValidResponse(1, $values)]);
 }
示例#13
0
 public function convert(BaseQuestionType $questionType, $interactionIdentifier, $interactionLabel)
 {
     /** @var mcq $question */
     $question = $questionType;
     // Build <choiceInteraction>
     $valueIdentifierMap = [];
     $simpleChoiceCollection = new SimpleChoiceCollection();
     foreach ($question->get_options() as $index => $option) {
         /** @var mcq_options_item $option */
         $choiceContent = new FlowStaticCollection();
         foreach (QtiMarshallerUtil::unmarshallElement($option->get_label()) as $component) {
             $choiceContent->attach($component);
         }
         // Use option['value'] as choice `identifier` if it has the correct format,
         // Otherwise, generate a valid using index such `CHOICE_1`, `CHOICE_2`, etc
         $originalOptionValue = $option->get_value();
         $choiceIdentifier = Format::isIdentifier($originalOptionValue, false) ? $originalOptionValue : 'CHOICE_' . $index;
         // Store this reference in a map
         $valueIdentifierMap[$originalOptionValue] = $choiceIdentifier;
         $choice = new SimpleChoice($choiceIdentifier);
         $choice->setContent($choiceContent);
         $simpleChoiceCollection->attach($choice);
     }
     // Build final interaction and its corresponding <responseDeclaration>, and its <responseProcessingTemplate>
     $interaction = new ChoiceInteraction($interactionIdentifier, $simpleChoiceCollection);
     $interaction->setLabel($interactionLabel);
     $interaction->setMinChoices(1);
     $interaction->setMaxChoices($question->get_multiple_responses() ? $simpleChoiceCollection->count() : 1);
     // Build the prompt
     $interaction->setPrompt($this->convertStimulusForPrompt($question->get_stimulus()));
     // Set shuffle options
     $interaction->setShuffle($question->get_shuffle_options() ? true : false);
     // Set the layout
     if ($question->get_ui_style() instanceof mcq_ui_style && $question->get_ui_style()->get_type() === 'horizontal' && intval($question->get_ui_style()->get_columns()) === count($question->get_options())) {
         $interaction->setOrientation(Orientation::HORIZONTAL);
     } else {
         $interaction->setOrientation(Orientation::VERTICAL);
         LogService::log('ui_style` is ignored and `choiceInteraction` is assumed and set as `vertical`');
     }
     if (empty($question->get_validation())) {
         return [$interaction, null, null];
     }
     $builder = new McqValidationBuilder($question->get_multiple_responses(), $valueIdentifierMap);
     list($responseDeclaration, $responseProcessing) = $builder->buildValidation($interactionIdentifier, $question->get_validation());
     return [$interaction, $responseDeclaration, $responseProcessing];
 }
 public function getQuestionType()
 {
     /* @var QtiChoiceInteraction $interaction */
     $interaction = $this->interaction;
     $options = $this->buildOptions($interaction->getSimpleChoices());
     $mcq = new mcq('mcq', $options);
     // Support for @shuffle
     $mustShuffle = $interaction->mustShuffle();
     if ($mustShuffle) {
         $mcq->set_shuffle_options($mustShuffle);
         LogService::log('Set shuffle choices as true, however `fixed` attribute would be ignored since we don\'t support partial shuffle');
     }
     // Support for @orientation ('vertical' or 'horizontal')
     $uiStyle = new mcq_ui_style();
     if ($interaction->getOrientation() === Orientation::HORIZONTAL) {
         $uiStyle->set_type('horizontal');
         $uiStyle->set_columns(count($options));
         $mcq->set_ui_style($uiStyle);
     }
     // Support mapping for <prompt>
     if ($interaction->getPrompt() instanceof Prompt) {
         $promptContent = $interaction->getPrompt()->getContent();
         $mcq->set_stimulus(QtiMarshallerUtil::marshallCollection($promptContent));
     }
     // Partial support for @maxChoices
     // @maxChoices of 0 or more than 1 would then map to choicematrix
     $maxChoices = $interaction->getMaxChoices();
     if ($maxChoices !== 1) {
         if ($maxChoices !== 0 && $maxChoices !== count($options)) {
             // We do not support specifying amount of choices
             LogService::log("Allowing multiple responses of max " . count($options) . " options, however " . "maxChoices of {$maxChoices} would be ignored since we can't support exact number");
         }
         $mcq->set_multiple_responses(true);
     }
     // Ignoring @minChoices
     if (!empty($interaction->getMinChoices())) {
         LogService::log('Attribute minChoices is not supported. Thus, ignored');
     }
     // Build validation
     $validationBuilder = new ChoiceInteractionValidationBuilder($this->responseDeclaration, array_column($options, 'label', 'value'), $maxChoices);
     $validation = $validationBuilder->buildValidation($this->responseProcessingTemplate);
     if (!empty($validation)) {
         $mcq->set_validation($validation);
     }
     return $mcq;
 }
 public function processAssessmentItem(AssessmentItem $assessmentItem)
 {
     // TODO: Yea, we ignore rubric but what happen if the rubric is deep inside nested
     $newCollection = new BlockCollection();
     $itemBodyNew = new ItemBody();
     /** @var QtiComponent $component */
     foreach ($assessmentItem->getItemBody()->getContent() as $key => $component) {
         if (!$component instanceof RubricBlock) {
             $newCollection->attach($component);
         } else {
             LogService::log('Does not support <rubricBlock>. Ignoring <rubricBlock>');
         }
     }
     $itemBodyNew->setContent($newCollection);
     $assessmentItem->setItemBody($itemBodyNew);
     return $assessmentItem;
 }
 protected function getMatchCorrectTemplateValidation()
 {
     $interactionResponses = [];
     foreach ($this->responseDeclarations as $responseIdentifier => $responseDeclaration) {
         /** @var ResponseDeclaration $responseDeclaration */
         if (!empty($responseDeclaration->getCorrectResponse())) {
             $correctResponses = $responseDeclaration->getCorrectResponse()->getValues()->getArrayCopy(true);
             $interactionResponses[] = array_map(function ($value) {
                 /** @var Value $value */
                 return new ValidResponse(1, [$value->getValue()]);
             }, $correctResponses);
         } else {
             LogService::log('Response declaration has no correct response values. Thus, validation ignored');
         }
     }
     $responses = ArrayUtil::cartesianProductForResponses($interactionResponses);
     return ValidationBuilder::build('clozetext', 'exactMatch', $responses);
 }
示例#17
0
 public function convert(item $item, array $questions)
 {
     // Make sure we clean up the log
     LogService::flush();
     // Try to build the identifier using item `reference`
     // Otherwise, generate an alternative identifier and store the original reference as `label`
     $itemReference = $item->get_reference();
     $itemIdentifier = Format::isIdentifier($itemReference, false) ? $itemReference : 'ITEM_' . StringUtil::generateRandomString(12);
     if ($itemReference !== $itemIdentifier) {
         LogService::log("The item `reference` ({$itemReference}) is not a valid identifier, thus can not be used for `assessmentItem` identifier. " . "Replaced it with randomly generated `{$itemIdentifier}` and stored the original `reference` as `label` attribute");
     }
     $builder = new AssessmentItemBuilder();
     $assessmentItem = $builder->build($itemIdentifier, $itemReference, $questions, $item->get_content());
     $xml = new XmlDocument();
     $xml->setDocumentComponent($assessmentItem);
     // Flush out all the error messages stored in this static class, also ensure they are unique
     $messages = array_values(array_unique(LogService::flush()));
     return [$xml->saveToString(true), $messages];
 }
 protected function getMapResponseTemplateValidation()
 {
     $interactionResponses = [];
     /** @var ResponseDeclaration $responseDeclaration */
     foreach ($this->responseDeclarations as $responseIdentifier => $responseDeclaration) {
         $responses = [];
         foreach ($responseDeclaration->getMapping()->getMapEntries()->getArrayCopy(true) as $mapEntry) {
             /** @var MapEntry $mapEntry */
             $responses[] = new ValidResponse($mapEntry->getMappedValue(), [$this->possibleResponses[$responseIdentifier][$mapEntry->getMapKey()]]);
             // Find out if one of them is case sensitive
             if (!$mapEntry->isCaseSensitive()) {
                 LogService::log('Could not support `caseSensitive` attribute for this interaction type. This question validation is always case sensitive');
             }
         }
         $interactionResponses[] = $responses;
     }
     $responses = ArrayUtil::cartesianProductForResponses($interactionResponses);
     return ValidationBuilder::build('clozedropdown', 'exactMatch', $responses);
 }
示例#19
0
 private static function populateClassFields($class, $values)
 {
     // And, set values magically using setter methods
     foreach ($values as $key => $value) {
         if (!method_exists($class, "set_{$key}")) {
             LogService::log("Ignoring attribute '{$key}'. Invalid key");
             continue;
         }
         if ($value === null) {
             LogService::log("Ignoring attribute '{$key}'. Invalid key");
             continue;
         }
         $setter = new \ReflectionMethod($class, "set_{$key}");
         $parameters = [];
         foreach ($setter->getParameters() as $parameter) {
             $parameterName = $parameter->getName();
             $parameters[$parameterName] = self::buildField($parameter, $values[$parameterName]);
         }
         $setter->invokeArgs($class, $parameters);
     }
     return $class;
 }
 private function processHtml($content)
 {
     $html = new SimpleHtmlDom();
     $html->load($content);
     // Replace <br> with <br />, <img ....> with <img />, etc
     /** @var array $selfClosingTags ie. `img, br, input, meta, link, hr, base, embed, spacer` */
     $selfClosingTags = implode(array_keys($html->getSelfClosingTags()), ', ');
     foreach ($html->find($selfClosingTags) as &$node) {
         $node->outertext = rtrim($node->outertext, '>') . '/>';
     }
     // Replace these audioplayer and videoplayer feature with <object> nodes
     foreach ($html->find('span.learnosity-feature') as &$node) {
         try {
             // Replace <span..> with <object..>
             $replacement = $this->getFeatureReplacementString($node);
             $node->outertext = $replacement;
         } catch (MappingException $e) {
             LogService::log($e->getMessage() . '. Ignoring mapping feature ' . $node->outertext . '`');
         }
     }
     return $html->save();
 }
 protected function getMapResponseTemplateValidation()
 {
     $validResponses = [];
     foreach ($this->responseDeclaration->getMapping()->getMapEntries() as $mapEntry) {
         /** @var MapEntry $mapEntry */
         if (!isset($this->options[$mapEntry->getMapKey()])) {
             LogService::log('Invalid choice `' . $mapEntry->getMapKey() . '`');
             continue;
         }
         if ($mapEntry->getMappedValue() < 0) {
             LogService::log('Invalid score ` ' . $mapEntry->getMappedValue() . ' `. Negative score is ignored');
             continue;
         }
         $validResponses[] = new ValidResponse($mapEntry->getMappedValue(), [$mapEntry->getMapKey()]);
     }
     // Handle `multiple` cardinality
     if ($this->responseDeclaration->getCardinality() === Cardinality::MULTIPLE) {
         $combinationChoicesCount = $this->maxChoices === 0 ? count($validResponses) : $this->maxChoices;
         $combinationResponses = ArrayUtil::combinations($validResponses, $combinationChoicesCount);
         $validResponses = ArrayUtil::combineValidResponsesWithSummedScore($combinationResponses);
     }
     return ValidationBuilder::build('mcq', 'exactMatch', $validResponses);
 }
示例#22
0
 public static function convertLearnosityToQtiItem(array $data)
 {
     $jsonType = self::guessLearnosityJsonDataType($data);
     // Handle `item` which contains both a single item and one or more questions/features
     if ($jsonType === self::LEARNOSITY_DATA_ITEM) {
         list($xmlString, $messages) = self::convertLearnosityItem($data);
         // Handle if just question
     } else {
         if ($jsonType === self::LEARNOSITY_DATA_QUESTION) {
             list($xmlString, $messages) = self::convertLearnosityQuestion($data);
             // Handle if just question data
         } else {
             if ($jsonType === self::LEARNOSITY_DATA_QUESTION_DATA) {
                 list($xmlString, $messages) = self::convertLearnosityQuestionData($data);
             } else {
                 throw new \Exception('Unknown JSON format');
             }
         }
     }
     // Validate them before proceeding by feeding it back
     try {
         $document = new XmlDocument();
         $document->loadFromString($xmlString);
     } catch (\Exception $e) {
         LogService::log('Unknown error occurred. The QTI XML produced may not be valid');
     }
     return [$xmlString, $messages];
 }
示例#23
0
 private function getResponseProcessingTemplate(ResponseProcessing $responseProcessing = null)
 {
     if ($responseProcessing === null) {
         return ResponseProcessingTemplate::none();
     }
     if ($responseProcessing->getResponseRules()->count()) {
         LogService::log('Does not support custom response processing on <responseProcessing>. Ignoring <responseProcessing>');
         return ResponseProcessingTemplate::unsupported();
     }
     if (!empty($responseProcessing->getTemplateLocation())) {
         LogService::log('Does not support \'templateLocation\' on <responseProcessing>. Ignoring <responseProcessing>');
         return ResponseProcessingTemplate::unsupported();
     }
     if (!empty($responseProcessing->getTemplate())) {
         return ResponseProcessingTemplate::getFromTemplateUrl($responseProcessing->getTemplate());
     }
     return ResponseProcessingTemplate::none();
 }
 private function checkObjectComponents(Object $object, $conversionTo)
 {
     if (!empty($object->getComponents())) {
         LogService::log('Converting <object> element to ' . $conversionTo . '. Any contents within it are removed');
     }
 }
 private function transformCoordinates(QtiCoords $coords, $shape, Object $imageObject)
 {
     $width = $imageObject->getWidth();
     $height = $imageObject->getHeight();
     $coords = explode(',', $coords);
     switch ($shape) {
         // rect: left-x, top-y, right-x, bottom-y.
         case QtiShape::RECT:
             $leftX = round($coords[0] / $width * 100, 2);
             $topY = round($coords[1] / $height * 100, 2);
             $rightX = round($coords[2] / $width * 100, 2);
             $bottomY = round($coords[3] / $height * 100, 2);
             $result = [['x' => $leftX, 'y' => $topY], ['x' => $rightX, 'y' => $topY], ['x' => $rightX, 'y' => $bottomY], ['x' => $leftX, 'y' => $bottomY]];
             return $result;
         case QtiShape::CIRCLE:
             // circle: center-x, center-y, radius. Note. When the radius value is a percentage value, user agents
             // should calculate the final radius value based on the associated object's width and height. The radius should be the smaller value of the two.
             LogService::log('Unable to map QTI `circle` Shape. Mapping it as `rect` instead');
             $radius = $coords[2];
             $leftX = round(($coords[0] - $radius) / $width * 100, 2);
             $topY = round(($coords[1] + $radius) / $height * 100, 2);
             $rightX = round(($coords[0] + $radius) / $width * 100, 2);
             $bottomY = round(($coords[1] - $radius) / $height * 100, 2);
             $result = [['x' => $leftX, 'y' => $topY], ['x' => $rightX, 'y' => $topY], ['x' => $rightX, 'y' => $bottomY], ['x' => $leftX, 'y' => $bottomY]];
             return $result;
         case QtiShape::POLY:
             // poly: x1, y1, x2, y2, ..., xN, yN. The first x and y coordinate pair and the last should be the same to close the polygon.
             // When these coordinate values are not the same, user agents should infer an additional coordinate pair to close the polygon.
             $result = [];
             for ($i = 0; $i < count($coords); $i += 2) {
                 $result[] = ['x' => round($coords[$i] / $width * 100, 2), 'y' => round($coords[$i + 1] / $height * 100, 2)];
             }
             return $result;
         case QtiShape::ELLIPSE:
         default:
             LogService::log('Unsupported QTI Shape mapping conversion. Area mapping conversion is ignored');
             return [];
     }
 }
 private function map(Question $question)
 {
     $type = $question->get_type();
     if (!in_array($type, Constants::$supportedQuestionTypes)) {
         throw new MappingException("Question type `{$type}` not yet supported to be mapped to QTI");
     }
     $clazz = new \ReflectionClass(self::MAPPER_CLASS_BASE . ucfirst($type . 'Mapper'));
     $questionTypeMapper = $clazz->newInstance();
     // Try to use question `reference` as identifier
     // Otherwise, generate an alternative identifier and store the original reference as `label` to be passed in
     $questionReference = $question->get_reference();
     $interactionIdentifier = Format::isIdentifier($questionReference, false) ? $questionReference : strtoupper($type) . '_' . StringUtil::generateRandomString(12);
     if ($interactionIdentifier !== $questionReference) {
         LogService::log("The question `reference` ({$questionReference}) is not a valid identifier. " . "Replaced it with randomly generated `{$interactionIdentifier}` and stored the original `reference` as `label` attribute");
     }
     $result = $questionTypeMapper->convert($question->get_data(), $interactionIdentifier, $questionReference);
     $result[] = $questionTypeMapper->getExtraContent();
     return $result;
 }