Nodes

Kolibri channels are tree-like structures that consist of different types of topic nodes (folders) and various content nodes (document, audio, video, html, exercise). The module ricecooker.classes.nodes defines helper classes to represent each of these supported content types and provide validation logic to check channel content is valid before uploading it to Kolibri Studio.

The purpose of the Node classes is to represent the channel tree structure and store metadata necessary for each type of content item, while the actual content data is stored in file objects (defined in ricecooker.classes.files) and exercise questions object (defined in ricecooker.classes.questions) which are created separately.

Overview

The following diagram lists all the node classes defined in ricecooker.classes.nodes and shows the associated file and question classes that content nodes can contain.

      ricecooker.classes.nodes
      |
      |                                               ricecooker.classes.files
class Node(object)                                    |
    class ChannelNode(Node)                           |
    class TreeNode(Node)                              |
        class TopicNode(TreeNode)                     |
        class ContentNode(TreeNode)                   |
            class AudioNode(ContentNode)     files = [AudioFile]
            class DocumentNode(ContentNode)  files = [DocumentFile, EPubFile]
            class HTML5AppNode(ContentNode)  files = [HTMLZipFile]
            class H5PAppNode(ContentNode)    files = [H5PFile]
            class SlideshowNode(ContentNode) files = [SlideImageFile]
            class VideoNode(ContentNode)     files = [VideoFile, WebVideoFile, YouTubeVideoFile,
                                                      SubtitleFile, YouTubeSubtitleFile]
            class ExerciseNode(ContentNode)  questions = [SingleSelectQuestion,
                                                          MultipleSelectQuestion,
                                                          InputQuestion,
                                                          PerseusQuestion]
                                                          |
                                                          |
                                                          ricecooker.classes.questions

In the remainder of this document we’ll describe in full detail the metadata that is needed to specify different content nodes.

For more info about file objects see page files and to learn about the different exercise questions see the page exercises.

Content node metadata

Each node has the following attributes:

  • source_id (str): content’s original id

  • title (str): content’s title

  • license (str or License): content’s license id or object

  • language (str or lang_obj): language for the content node

  • description (str): description of content (optional)

  • author (str): who created the content (optional)

  • aggregator (str): website or org hosting the content collection but not necessarily the creator or copyright holder (optional)

  • provider (str): organization that commissioned or is distributing the content (optional)

  • role (str): set to roles.COACH for teacher-facing materials (default roles.LEARNER)

  • thumbnail (str or ThumbnailFile): path to thumbnail or file object (optional)

  • derive_thumbnail (bool): set to True to generate thumbnail from contents (optional)

  • files ([FileObject]): list of file objects for node (optional)

  • extra_fields (dict): any additional data needed for node (optional)

  • domain_ns (uuid): who is providing the content (e.g. learningequality.org) (optional)

IMPORTANT: nodes representing distinct pieces of content MUST have distinct source_ids. Each node has a content_id (computed as a function of the source_domain and the node’s source_id) that uniquely identifies a piece of content within Kolibri for progress tracking purposes. For example, if the same video occurs in multiple places in the tree, you would use the same source_id for those nodes – but content nodes that aren’t for that video need to have different source_ids.

Usability guidelines

  • Thumbnails: 16:9 aspect ratio ideally (e.g. 400x225 pixels)

  • Titles: Aim for titles that make content items reusable independently of their containing folder, since curators could copy content items to other topics or channels. e.g. title for pdf doc “{lesson_name} - instructions.pdf” is better than just “Instructions.pdf” since that PDF could show up somewhere else.

  • Descriptions: aim for about 400 characters (about 3-4 sentences)

  • Licenses: Any non-public domain license must have a copyright holder, and any special permissions licenses must have a license description.

Licenses

All content nodes within Kolibri and Kolibri Studio must have a license. The file le_utils/constants/licenses.py contains the constants used to identify the license types. These constants are meant to be used in conjunction with the helper method ricecooker.classes.licenses.get_license to create Licence objects.

To initialize a license object, you must specify the license type and the copyright_holder (str) which identifies a person or an organization. For example:

from ricecooker.classes.licenses import get_license
from le_utils.constants import licenses

license_obj = get_license(licenses.CC_BY, copyright_holder="Khan Academy")

Note: The copyright_holder field is required for all License types except for the public domain license for which copyright_holder can be None. Everyone owns the stuff in the public domain.

Languages

The Python package le-utils defines the internal language codes used throughout the Kolibri platform (e.g. en, es-MX, and zul). To find the internal language code for a given language, you can locate it in the lookup table, or use one of the language lookup helper functions defined in le_utils.constants.languages:

  • getlang(<code>) --> lang_obj: basic lookup used to ensure <code> is a valid internal language code (otherwise returns None).

  • getlang_by_name(<Language name in English>) --> lang_obj: lookup by name, e.g. French

  • getlang_by_native_name(<Language autonym>) --> lang_obj: lookup by native name, e.g., français

  • getlang_by_alpha2(<two-letter ISO 639-1 code>) --> lang_obj: lookup by standard two-letter code, e.g fr

You can either pass lang_obj as the language attribute when creating nodes, or pass the internal language code (str) obtained from the property lang_obj.code:

from le_utils.constants.languages import getlang_by_native_name

lang_obj = getlang_by_native_name('français')
print(lang_obj        # Language(native_name='Français', primary_code='fr', subcode=None, name='French')
print(lang_obj.code)  # fr

See [languages][./languages.md] to read more about language codes.

Thumbnails

Thumbnails can be passed in as a local filesystem path to an image file (str), a URL (str), or a ThumbnailFile object. The recommended size for thumbnail images is 400px by 225px (aspect ratio 16:9). Use the command line argument --thumbnails to automatically generate thumbnails for all content node that don’t have a thumbnail specified.

Topic nodes

Topic nodes are folder-like containers that are used to organize the channel’s content.

from ricecooker.classes import TopicNode
from le_utils.constants.languages import getlang

topic_node = TopicNode(
    title='The folder name',
    description='A longer description of what the folder contains',
    source_id='<some unique identifier for this folder>',
    language='en',
    thumbnail=None,
    author='',
)

It is highly recommended to find suitable thumbnail images for topic nodes. The presence of thumbnails will make the content more appealing and easier to browse. Set derive_thumbnail=True on a topic node or use the --thumbnails command line argument and Ricecooker will generate thumbnails for topic nodes based on the thumbnails of the content nodes they contain.

Content nodes

The table summarizes summarizes the content node classes, their associated files, and the file formats supported by each file class:

  ricecooker.classes.nodes  ricecooker.classes.files
  |                         |
  AudioNode     --files-->  AudioFile                                   # .mp3
  DocumentNode  --files-->  DocumentFile                                # .pdf
                            EPubFile                                    # .epub
  SlideshowNode --files-->  SlideImageFile                              # .png/.jpg
  HTML5AppNode  --files-->  HTMLZipFile                                 # .zip
  VideoNode     --files-->  VideoFile, WebVideoFile, YouTubeVideoFile,  # .mp4
                            SubtitleFile, YouTubeSubtitleFile           # .vtt

For your copy-paste convenience, here is the sample code for creating a content node (DocumentNode) and an associated (DocumentFile)

content_node = DocumentNode(
      source_id='<some unique identifier within source domain>',
      title='Some Document',
      author='First Last (author\'s name)',
      description='Put node description here',
      language=getlang('en').code,
      license=get_license(licenses.CC_BY, copyright_holder='Copyright holder name'),
      thumbnail='some/local/path/name_thumb.jpg',
      files=[DocumentFile(
                path='some/local/path/name.pdf',
                language=getlang('en').code
             )]
)

Files can be passed in upon initialization as in the above sample, or can be added after initialization using the content_node’s add_files method.

Note you also use URLs for path and thumbnail instead of local filesystem paths, and the files will be downloaded for you automatically.

You can replace DocumentNode and DocumentFile with any of the other combinations of content node and file types.

Specify derive_thumbnail=True and leave thumbnail blank (thumbnail=None) to let Ricecooker automatically generate a thumbnail for the node based on its content. Thumbnail generation is supported for audio, video, PDF, and ePub, and HTML5 files.

Role-based visibility

It is possible to include content nodes in any channel that are only visible to Kolibri coaches. Setting the visibility to “coach-only” is useful for pedagogical guides, answer keys, lesson plan suggestions, and other supporting material intended only for teachers to see but not students. To control content visibility set the role attributes to one of the constants defined in le_utils.constants.roles to define the “minimum role” needed to see the content.

  • if role=roles.LEARNER: visible to learners, coaches, and administrators

  • if role=roles.COACH: visible only to Kolibri coaches and administrators

Exercise nodes

The ExerciseNode class (also subclasses of ContentNode), act as containers for various assessment questions types defined in ricecooker.classes.questions. The question types currently supported are:

  • SingleSelectQuestion: questions that only have one right answer (e.g. radio button questions)

  • MultipleSelectQuestion: questions that have multiple correct answers (e.g. check all that apply)

  • InputQuestion: questions that have as answers simple text or numeric expressions (e.g. fill in the blank)

  • PerseusQuestion: perseus json question (used in Khan Academy chef)

The following code snippet creates an exercise node that contains the three simple question types:

exercise_node = ExerciseNode(
        source_id='<some unique id>',
        title='Basic questions',
        author='LE content team',
        description='Showcase of the simple question type supported by Ricecooker and Studio',
        language=getlang('en').code,
        license=get_license(licenses.PUBLIC_DOMAIN),
        thumbnail=None,
        exercise_data={
            'mastery_model': exercises.M_OF_N,  # \
            'm': 2,                             #   learners must get 2/3 questions correct to complete exercise
            'n': 3,                             # /
            'randomize': True,                  # show questions in random order
        },
        questions=[
            MultipleSelectQuestion(
                id='sampleEX_Q1',
                question = "Which numbers the following numbers are even?",
                correct_answers = ["2", "4",],
                all_answers = ["1", "2", "3", "4", "5"],
                hints=['Even numbers are divisible by 2.'],
            ),
            SingleSelectQuestion(
                id='sampleEX_Q2',
                question = "What is 2 times 3?",
                correct_answer = "6",
                all_answers = ["2", "3", "5", "6"],
                hints=['Multiplication of $a$ by $b$ is like computing the area of a rectangle with length $a$ and width $b$.'],
            ),
            InputQuestion(
                id='sampleEX_Q3',
                question = "Name one of the *factors* of 10.",
                answers = ["1", "2", "5", "10"],
                hints=['The factors of a number are the divisors of the number that leave a whole remainder.'],
            )
        ]
)

Creating a PerseusQuestion requires first obtaining the perseus-format .json file for the question. You can questions using the web interface. Click here to see a samples of questions in the perseus json format.

To following code creates an exercise node with a single perseus question in it:

# LOAD JSON DATA (as string) FOR PERSEUS QUESTIONS
RAW_PERSEUS_JSON_STR = open('ricecooker/examples/data/perseus_graph_question.json', 'r').read()
# or
# import requests
# RAW_PERSEUS_JSON_STR = requests.get('https://github.com/learningequality/sample-channels/blob/master/contentnodes/exercise/perseus_graph_question.json').text
exercise_node2 = ExerciseNode(
        source_id='<another unique id>',
        title='An exercise containing a perseus question',
        author='LE content team',
        description='An example exercise with a Persus question',
        language=getlang('en').code,
        license=get_license(licenses.CC_BY, copyright_holder='Copyright holder name'),
        thumbnail=None,
        exercise_data={
            'mastery_model': exercises.M_OF_N,
            'm': 1,
            'n': 1,
        },
        questions=[
            PerseusQuestion(
                id='ex2bQ4',
                raw_data=RAW_PERSEUS_JSON_STR,
                source_url='https://github.com/learningequality/sample-channels/blob/master/contentnodes/exercise/perseus_graph_question.json'
            ),
        ]
)

The example above uses the JSON from this question, for which you can also a rendered preview here.

SlideshowNode nodes

The SlideshowNode class and the associated SlideImageFile class are used to create powerpoint-like presentations. The following code sample shows how to create a SlideshowNode that contains two slide images:

slideshow_node = SlideshowNode(
      source_id='<some unique identifier within source domain>',
      title='My presentations',
      author='First Last (author\'s name)',
      description='Put slideshow description here',
      language=getlang('en').code,
      license=get_license(licenses.CC_BY, copyright_holder='Copyright holder name'),
      thumbnail='some/local/path/slideshow_thumbnail.jpg',
      files=[
          SlideImageFile(
              path='some/local/path/firstslide.png',
              caption="The caption text to be displayed below the slide image.",
              descriptive_text="Description of the slide for users that cannot see the image",
              language=getlang('en').code,
          ),
          SlideImageFile(
              path='some/local/path/secondslide.jpg',
              caption="The caption for the second slide image.",
              descriptive_text="Alternative text for the second slide image",
              language=getlang('en').code,
          )
      ]
)

Note this is a new feature in Kolibri 0.13 and prior version of Kolibri will not be able to import and view this content kind.