Working With Activities, Exercises, ITQs and SAQs#

When creating active teaching and learning materials, an important learning design approach for prompting engagement is the inclusion of activities, where a call to action is provided and a learner is encouraged to perform a particular task or check their understanding in a particular way.

A large number of potentially reusable activities are defined in a structured way in OU-XML documents. If we are to reuse such activities, whether directly, with modification, or simply as a source of inspiration, we need to be able to browse or discover them in some way.

Currently, discovery is likely to arise from an academic remembering a particular activity from a particular module then sarching the VLE for that module, and searching within the VLE for the activity they remember. How much easier it would be if they could simply search over all activities, perhaps filtering down by module code, and then previewing the results in a meaningful way?

The <Activity>, <Exercise>, <ITQ> and <SAQ> elements all have a similar internal structure and only differ in the parent tag [example docs].

Each element must include a <Question> and may include a <Heading> and a <Timing> element; various different response elements may be provided (one or more of <MediaContent>, <Interaction>, <Answer>, <Discussion>) either as a single response, or within a <Part> tag inside a <Multipart> response element..

For example, at its simplest, an ITQ might take the following form:

<ITQ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <Question>
        <Paragraph>In session 1, you met charged versions of atoms; what are they called? </Paragraph>
    </Question>
    <Answer>
        <Paragraph>These are ions (positively charged ions are called cations and negatively charged ions are called anions). </Paragraph>
    </Answer>
</ITQ>

ITQs may also be defined as multipart questions.

Work in Progress; the enclosure within an activity element is not currently handled, although all media elements are extracted, irrespective of their context, in the generic section on extracting media items from OU-XML. In the Activity context, it would probably make sense to use a foreign key relationship to provide a reference into the media table from an activities table.

Preparing the Ground#

As ever, we need to set up a database connection:

from sqlite_utils import Database
import pandas as pd

# Open database connection
xml_dbname = "all_openlean_xml.db"
xml_db = Database(xml_dbname)

activity_dbname = "openlean_assets.db"
db = Database(activity_dbname)

And get a sample XML file, selecting one that we know contains structurally marked up glossary items:

from lxml import etree
import pandas as pd
from xml_utils import unpack, flatten

# Grab an OU-XML file that is known to contain activity items
activity_example_raw = pd.read_sql("SELECT xml FROM xml WHERE name='Accessibility of eLearning'",
                                   con=xml_db.conn).loc[0, "xml"]

# Parse the XML into an xml object
root = etree.fromstring(activity_example_raw)

Decomposing Activities Into Component Parts#

Activity-like objects are made up of several “simple” and more structurd componenents. All activity types must define a <Question> component, but there is some freedom in the choice of “response” element. In addition, multipart responses may be defined.

The strategy followed below is to split out the “simple” <Answer> and <Discussion> elements ito their own columns. For <Multipart> responses, each <Part> has its own entry in the activities table, with a multipart flag set.

TO DO: a “part-sort-order” number should also be included in the table.

The more complex <MediaContent> and <Interaction> elements are not currently handled in a meanigful way in the activity context. (Media items are handled separately in their own mined table, and a foreigh key reference should be made into that table. The interaction element is currently included in the activities table as stringified XML.)

TO DO: handle interaction

TO DO: handle media item reference.

import secrets
        
def parse_activity_item(activity, _path=None, key=""):
    """Parse activity element.
       The key is a unit id that lets us create a unique interaction table FK."""
    def _delist(x):
        return x[0] if x else x
    def _tidy(x):
        return unpack(x).decode().strip() if len(x) else None
    
    if _path is None:
        tree = etree.ElementTree(activity)
        _path = tree.getpath(activity)

    key = key if key else secrets.token_hex(16)
        
    # Need to consider Multipart
    _flat = flatten(activity)
    
    if _flat:
        a_multipart=[]
        a_heading = activity.xpath("Heading")
        a_heading = flatten(a_heading[0]) if a_heading else None
        a_timing = activity.xpath("Timing")
        a_timing = flatten(a_timing[0]) if a_timing else None
        # Should we perhaps parse the question to markdown?
        a_question = _tidy(_delist(activity.xpath("Question")))
        a_answer = _tidy(_delist(activity.xpath("Answer")))
        
        """
        # The interaction type is a complex element that should be included
        # in an interaction table with a shared reference id generated from
        # the unit id and the path to the element
        _interaction = _tidy(_delist(activity.xpath("Interaction")))
        if _interaction:
            interaction_id = create_id( (key, _path) )
            a_interaction = interaction_id
        else:
            a_interaction = None
        """
        a_discussion = _tidy(_delist(activity.xpath("Discussion")))
        
        # TO DO - for now, explicitly capture the interaction
        a_interaction = _tidy(_delist(activity.xpath("Interaction")))
        
        # TO DO: activity.xpath("MediaElement") as a FK relation?
        
        if activity.xpath("Multipart"):
            for p in activity.xpath("Multipart/Part"):
                a_multipart.extend(parse_activity_item(p))

        if not a_multipart:
            # Also return the multipart status and the multipart name and timing
            return [(a_heading, a_timing, a_question, a_interaction, a_answer,
                     a_discussion, _path,
                    True if a_multipart else False, _path)]
        else:
            return [(m[0], m[1], m[2], m[3], m[4], m[5], m[6],
                    True if a_multipart else False, _path) for m in a_multipart if m]

    return []

Let’s see how that works:

parse_activity_item( root.xpath("//Activity")[0])
[('Activity 1',
  None,
  '<Question xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><NumberedList><ListItem>Identify some eLearning materials you are familiar with. You may have written them, taught them, or even studied them (you can think about this course if you have no examples of your own).</ListItem><ListItem>Identify the different elements of the materials &#8211; there will almost certainly be text, but are there images, videos, forms or boxes to enter text, drag-and-drop exercises, quizzes etc.? List all of the different types of element.</ListItem><ListItem>For each item on the list try to identify where students with particular disabilities might experience difficulties, and try to suggest possible ways (if you can think of any) that these barriers may be removed. It does not matter if your list is incomplete, or if you cannot think of solutions to some of your identified issues. We will revisit this task later, after you have worked through more of the course materials.</ListItem></NumberedList><Paragraph>Use the box below to record your thoughts.</Paragraph></Question>',
  '<Interaction xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><FreeResponse size="paragraph" id="fr_3"/></Interaction>',
  None,
  None,
  '/Item/Unit/Session[1]/Section[6]/Activity',
  False,
  '/Item/Unit/Session[1]/Section[6]/Activity')]

TO DO - the <Interaction> element takes several forms: <SingleChoice>, <Matching>, <FreeResponse>, <VoiceRecorder>, <MultipleChoice>.

As such, it makes sense to record interaction components in a table of their own, referenced to the parent activity element etc. TO DO

We can also create a table that splits the activity elements out into component parts:

all_activities_tbl = db["activities"]
all_activities_tbl.drop(ignore=True)
all_activities_tbl.create({
    "type": str,
    "heading": str,
    "timing": str,
    "question": str,
    "answer": int,
    "discussion": str,
    "interaction": str,
    "multipart": bool,
    "id": str, # id of unit
    "_path": str
})
# For the pk, we need to be able to account for multipart elements
# Currently, each part in multipart element is an entry in this table

# Enable full text search
# This creates an extra virtual table (media_fts) to support the full text search
db[f"{all_activities_tbl.name}_fts"].drop(ignore=True)
db[all_activities_tbl.name].enable_fts(["heading", "question", "interaction",
                                                 "answer", "discussion", "id"],
                                                create_triggers=True)
<Table activities (type, heading, timing, question, answer, discussion, interaction, multipart, id, _path)>

Create a simple function to grab all the activities associated with a particular unit:

def get_activity_items(root, typ="Activity", _id=""):
    """Extract activity items from an OU-XML XML object."""
    activities = root.xpath(f'//{typ}')
    
    activity_list_raw = []
    activity_list = []
    for activity in activities:
            
        # Get path to activity within unit
        tree = etree.ElementTree(activity)
        _path = tree.getpath(activity)
    
        activity_list.extend(parse_activity_item(activity, _path))
        activity_list_raw.append( {"activity": unpack(activity), "id": _id,
                                   "type":typ.lower(), "_path": _path} )

    return activity_list_raw, activity_list

Create a function to scan the OpenLearn units for various type of activity:

def add_activities_to_db(typ="Activity"):
    """Add activity type elements to the database."""
    for row in xml_db.query("""SELECT * FROM xml;"""):
        _root = etree.fromstring(row["xml"])
        raw_activity_items, activity_items = get_activity_items(_root,
                                                                typ=typ,
                                                                _id=row["id"])

        # From the list of activity items,
        # create a list of dict items we can add to the database
        activity_item_dicts = [{"heading": a[0], "timing": a[1] , "question": a[2],
                                "interaction": a[3], "answer": a[4],
                                "discussion": a[5], "_path": a[6],
                                "type": typ.lower(),
                                "id": row["id"]} for a in activity_items if a[2] ]

        # Add items to the database
        db[all_activities_tbl.name].insert_all(activity_item_dicts)

We can now parse the documents for Activity type elements:

add_activities_to_db(typ="Activity")

How does that look?

pd.read_sql("SELECT * FROM activities LIMIT 3", con=db.conn)
type heading timing question answer discussion interaction multipart id _path
0 activity Activity 1 Looking at different technologies Allow approximately 15 minutes <Question xmlns:xsi="http://www.w3.org/2001/XM... None <Discussion xmlns:xsi="http://www.w3.org/2001/... <Interaction xmlns:xsi="http://www.w3.org/2001... None 1f194525072f4358f7639c471ee5289665d50a3f /Item/Unit/Session[1]/Activity
1 activity Activity 2 Types of body language Allow approximately 20 minutes <Question xmlns:xsi="http://www.w3.org/2001/XM... None <Discussion xmlns:xsi="http://www.w3.org/2001/... None None 1f194525072f4358f7639c471ee5289665d50a3f /Item/Unit/Session[5]/Activity
2 activity Activity 3 Translating emoji language Allow approximately 15 minutes <Question xmlns:xsi="http://www.w3.org/2001/XM... None <Discussion xmlns:xsi="http://www.w3.org/2001/... None None 1f194525072f4358f7639c471ee5289665d50a3f /Item/Unit/Session[6]/Activity

And for the raw table?

pd.read_sql("SELECT * FROM activities_raw LIMIT 3", con=db.conn)
type activity id _path
0 activity b'<Activity xmlns:xsi="http://www.w3.org/2001/... 1f194525072f4358f7639c471ee5289665d50a3f /Item/Unit/Session[1]/Activity
1 activity b'<Activity xmlns:xsi="http://www.w3.org/2001/... 1f194525072f4358f7639c471ee5289665d50a3f /Item/Unit/Session[5]/Activity
2 activity b'<Activity xmlns:xsi="http://www.w3.org/2001/... 1f194525072f4358f7639c471ee5289665d50a3f /Item/Unit/Session[6]/Activity

Parsing Other Activity Types#

An ITQ element can be parsed in the same way as <Activity> element, as previously described:

add_activities_to_db(typ="ITQ")

pd.read_sql("SELECT * FROM activities WHERE type='itq' LIMIT 3", con=db.conn)
type heading timing question answer discussion interaction multipart id _path
0 itq None None <Question xmlns:xsi="http://www.w3.org/2001/XM... <Answer xmlns:xsi="http://www.w3.org/2001/XMLS... None None None 6bff78840be5165329dda278418bbbd54c909047 /Item/Unit/Session[1]/Section[2]/ITQ
1 itq None None <Question xmlns:xsi="http://www.w3.org/2001/XM... <Answer xmlns:xsi="http://www.w3.org/2001/XMLS... None None None 6bff78840be5165329dda278418bbbd54c909047 /Item/Unit/Session[1]/Section[3]/SubSection[1]...
2 itq None None <Question xmlns:xsi="http://www.w3.org/2001/XM... <Answer xmlns:xsi="http://www.w3.org/2001/XMLS... None None None 6bff78840be5165329dda278418bbbd54c909047 /Item/Unit/Session[1]/Section[3]/SubSection[6]...

How about exercises?

add_activities_to_db(typ="Exercise")

pd.read_sql("SELECT * FROM activities WHERE type='exercise' LIMIT 3", con=db.conn)
type heading timing question answer discussion interaction multipart id _path

Or SAQs?

add_activities_to_db(typ="SAQ")

pd.read_sql("SELECT * FROM activities WHERE type='saq' LIMIT 3", con=db.conn)
type heading timing question answer discussion interaction multipart id _path
0 saq None None <Question xmlns:xsi="http://www.w3.org/2001/XM... <Answer xmlns:xsi="http://www.w3.org/2001/XMLS... None None None 6c5ba9c60fa29f546a71ba94565a6f62f1eae0db /Item/Unit[2]/Session[2]/Section/SAQ
1 saq None None <Question xmlns:xsi="http://www.w3.org/2001/XM... None <Discussion xmlns:xsi="http://www.w3.org/2001/... None None 6c5ba9c60fa29f546a71ba94565a6f62f1eae0db /Item/Unit[2]/Session[3]/Section[2]/SAQ[1]
2 saq None None <Question xmlns:xsi="http://www.w3.org/2001/XM... <Answer xmlns:xsi="http://www.w3.org/2001/XMLS... None None None 6c5ba9c60fa29f546a71ba94565a6f62f1eae0db /Item/Unit[2]/Session[3]/Section[2]/SAQ[2]

We should also be able to run full-text search questions over all the activity types:

fts(db, "activities", "chemical equation")
heading question interaction answer discussion id
0 Part 1 <Question>\n <P... <Interaction>\n ... <Answer>\n <Equ... None 904a100e4d41cf1a696b547eec1b2f625fc5bd78
1 Question 9 <Question xmlns:xsi="http://www.w3.org/2001/XM... None <Answer xmlns:xsi="http://www.w3.org/2001/XMLS... None f36ae6f445e1c39925fa84d3e188af9ed7c15fdc
2 None <Question xmlns:xsi="http://www.w3.org/2001/XM... None <Answer xmlns:xsi="http://www.w3.org/2001/XMLS... None 884164a46f4066c6b26894c812484c74ab2e8531
3 None <Question xmlns:xsi="http://www.w3.org/2001/XM... None <Answer xmlns:xsi="http://www.w3.org/2001/XMLS... None 884164a46f4066c6b26894c812484c74ab2e8531
4 None <Question xmlns:xsi="http://www.w3.org/2001/XM... None <Answer xmlns:xsi="http://www.w3.org/2001/XMLS... None 884164a46f4066c6b26894c812484c74ab2e8531
5 None <Question xmlns:xsi="http://www.w3.org/2001/XM... None <Answer xmlns:xsi="http://www.w3.org/2001/XMLS... None 884164a46f4066c6b26894c812484c74ab2e8531
6 None <Question xmlns:xsi="http://www.w3.org/2001/XM... None <Answer xmlns:xsi="http://www.w3.org/2001/XMLS... None 884164a46f4066c6b26894c812484c74ab2e8531
7 SAQ 5 <Question xmlns:xsi="http://www.w3.org/2001/XM... None <Answer xmlns:xsi="http://www.w3.org/2001/XMLS... None a82aa8fe5c90c02095eefb3e7d998efbbc1949c8

We really need a better way to render these results…

Parsing the activity into markdown would be one way…