Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: QTI support is improved #4

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions src/cc2olx/qti.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import re
import urllib.parse
import xml.dom.minidom
from collections import OrderedDict
Expand Down Expand Up @@ -184,7 +185,7 @@ def _create_fib_problem(self, problem_data):
# and set the type to case insensitive
problem_content = self.doc.createElement("stringresponse")
problem_content.setAttribute("answer", problem_data["answer"])
problem_content.setAttribute("type", "ci")
problem_content.setAttribute("type", self._build_fib_problem_type(problem_data))

if len(problem_data["answer"]) > max_answer_length:
max_answer_length = len(problem_data["answer"])
Expand All @@ -211,6 +212,18 @@ def _create_fib_problem(self, problem_data):

return problem

@staticmethod
def _build_fib_problem_type(problem_data):
"""
Build `stringresponse` OLX type for a fill in the blank problem.
"""
problem_types = ["ci"]

if problem_data["is_regexp"]:
NiedielnitsevIvan marked this conversation as resolved.
Show resolved Hide resolved
problem_types.append("regexp")

return " ".join(problem_types)

def _create_essay_problem(self, problem_data):
"""
Given parsed essay problem data, returns a openassessment component. If a sample
Expand Down Expand Up @@ -321,7 +334,7 @@ def parse_qti(self):
root = tree.getroot()

# qti xml can contain multiple problems represented by <item/> elements

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment still relevant?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we changed the code to consider all sections instead of just one with root_section identifier. Comment doesn't relate to identifiers, so nothing changed related to the comment, we still can have multiple <item/> elements.

problems = root.findall(".//qti:section[@ident='root_section']/qti:item", self.NS)
problems = root.findall(".//qti:section/qti:item", self.NS)

parsed_problems = []

Expand All @@ -334,7 +347,8 @@ def parse_qti(self):
# when we're getting malformed course (due to a weird Canvas behaviour)
# with equal identifiers. LMS doesn't support blocks with the same identifiers.
data["ident"] = attributes["ident"] + str(i)
data["title"] = attributes["title"]
if title := attributes.get("title"):
data["title"] = title

cc_profile = self._parse_problem_profile(problem)
data["cc_profile"] = cc_profile
Expand Down Expand Up @@ -516,7 +530,7 @@ def _mark_correct_responses(self, resprocessing, responses):
for ans in correct_answers:
responses[ans.text]["correct"] = True

if respcondition.attrib["continue"] == "No":
if respcondition.attrib.get("continue", "No") == "No":
NiedielnitsevIvan marked this conversation as resolved.
Show resolved Hide resolved
break

def _parse_multiple_choice_problem(self, problem):
Expand Down Expand Up @@ -553,16 +567,26 @@ def _parse_fib_problem(self, problem):
data["problem_description"] = presentation.find("qti:material/qti:mattext", self.NS).text

answers = []
patterns = []
for respcondition in resprocessing.findall("qti:respcondition", self.NS):
for varequal in respcondition.findall("qti:conditionvar/qti:varequal", self.NS):
answers.append(varequal.text)

if respcondition.attrib["continue"] == "No":
for varsubstring in respcondition.findall("qti:conditionvar/qti:varsubstring", self.NS):
patterns.append(varsubstring.text)

if respcondition.attrib.get("continue", "No") == "No":
NiedielnitsevIvan marked this conversation as resolved.
Show resolved Hide resolved
break

# Primary answer is the first one, additional answers are what is left
data["answer"] = answers.pop(0)
data["additional_answers"] = answers
data["is_regexp"] = bool(patterns)
if data["is_regexp"]:
data["answer"] = patterns.pop(0)
answers = [re.escape(answer) for answer in answers]
data["additional_answers"] = [*patterns, *answers]
else:
# Primary answer is the first one, additional answers are what is left
data["answer"] = answers.pop(0)
data["additional_answers"] = answers

return data

Expand All @@ -580,7 +604,7 @@ def _parse_essay_problem(self, problem):
data["problem_description"] = presentation.find("qti:material/qti:mattext", self.NS).text

if solution is not None:
sample_solution_selector = "qti:solutionmaterial/qti:material/qti:mattext"
sample_solution_selector = "qti:solutionmaterial//qti:material//qti:mattext"
data["sample_solution"] = solution.find(sample_solution_selector, self.NS).text

if itemfeedback is not None:
Expand Down
51 changes: 47 additions & 4 deletions tests/fixtures_data/imscc_file/resource_4_qti/assessment_qti.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<fieldentry>1</fieldentry>
</qtimetadatafield>
</qtimetadata>
<section ident="root_section">
<section ident="id747a627-25dd-4289-b850-98b134316b50">
<item ident="question_multiple_choice" title="Question">
<itemmetadata>
<qtimetadata>
Expand Down Expand Up @@ -233,6 +233,47 @@
</respcondition>
</resprocessing>
</item>
<item ident="question_fill_in_blank_with_regexp">
<itemmetadata>
<qtimetadata>
<qtimetadatafield>
<fieldlabel>cc_profile</fieldlabel>
<fieldentry>cc.fib.v0p1</fieldentry>
</qtimetadatafield>
<qtimetadatafield>
<fieldlabel>cc_weighting</fieldlabel>
<fieldentry>1</fieldentry>
</qtimetadatafield>
</qtimetadata>
</itemmetadata>
<presentation>
<material>
<mattext texttype="text/html">What's the largest city in Switzerland?</mattext>
</material>
<response_str ident="32693">
<render_fib>
<response_label ident="idfe1116d-9876-4e23-8502-5ddcd5a341fc" />
</render_fib>
</response_str>
</presentation>
<resprocessing>
<outcomes>
<decvar minvalue="0" maxvalue="100" varname="SCORE" vartype="Decimal" />
</outcomes>
<respcondition>
<conditionvar>
<varsubstring respident="32693">Z[uü]rich</varsubstring>
</conditionvar>
<setvar action="Set" varname="SCORE">100</setvar>
</respcondition>
<respcondition continue="Yes">
<conditionvar>
<other />
</conditionvar>
<setvar action="Set" varname="SCORE">0</setvar>
</respcondition>
</resprocessing>
</item>
<item ident="essay_question_id" title="Question">
<itemmetadata>
<qtimetadata>
Expand Down Expand Up @@ -269,9 +310,11 @@
<itemfeedback ident="essay_solution_id">
<solution>
<solutionmaterial>
<material>
<mattext texttype="text/html">Sample Answer</mattext>
</material>
<flow_mat>
<material>
<mattext texttype="text/html">Sample Answer</mattext>
</material>
</flow_mat>
</solutionmaterial>
</solution>
</itemfeedback>
Expand Down
10 changes: 8 additions & 2 deletions tests/fixtures_data/studio_course_xml/course.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,14 @@
<textline size="12"/>
</stringresponse>
</problem>
<problem display_name="QTI" url_name="resource_4_qti">
<stringresponse answer="Z[uü]rich" type="ci regexp">
<p>What's the largest city in Switzerland?</p>
<textline size="19"/>
</stringresponse>
</problem>
<html display_name="QTI" url_name="resource_4_qti"><![CDATA[Sample Answer]]></html>
<openassessment text_response="required" prompts_type="html" display_name="QTI" url_name="essay_question_id4">
<openassessment text_response="required" prompts_type="html" display_name="QTI" url_name="essay_question_id5">
<title>Open Response Assessment</title>
<assessments>
<assessment name="staff-assessment" required="True"/>
Expand Down Expand Up @@ -102,7 +108,7 @@
<feedback_default_text>Feedback prompt default text</feedback_default_text>
</rubric>
</openassessment>
<openassessment text_response="required" prompts_type="html" display_name="QTI" url_name="essay_question_2_id5">
<openassessment text_response="required" prompts_type="html" display_name="QTI" url_name="essay_question_2_id6">
<title>Open Response Assessment</title>
<assessments>
<assessment name="staff-assessment" required="True"/>
Expand Down
Loading