Element extensions developer guide

Extensions are a way to add custom logic and extra complexity to already existing elements. Each element has the ability to load relevant extensions and as such, extensions should be custom tailored to one specific element.

Extension anatomy

Extensions can be defined in a course only, and are located in [course directory]/elementExtensions. This folder is organized such that elements are contained as subfolders, and extensions are then contained inside the element folders. For example, if you had an extension exampleExtension that extended exampleElement, the folder structure would be: [course directory]/elementExtensions/exampleElement/exampleExtension.

Each extension needs only an info.json containing metadata about the element, and can contain optional information like Python scripts, CSS styles, and clientside JavaScript files.

The info.json file is structurally similar to the element info file and may contain the following fields:

{
  "controller": "Python script",
  "dependencies": {
    "nodeModulesStyles": ["style_file_path"],
    "nodeModulesScripts": ["script_file_path"],
    "clientFilesCourseStyles": ["style_file_path"],
    "clientFilesCourseScripts": ["script_file_path"],
    "extensionStyles": ["style_file_path"],
    "extensionScripts": ["script_file_path"]
  },
  "dynamicDependencies": {
    "nodeModulesScripts": { "module_name": "module_path" },
    "clientFilesCourseScripts": { "file_name": "file_path" },
    "extensionScripts": { "file_name": "file_path" }
  }
}

Python Controller

The main Python controller script has no general structure and is instead defined by the element that is being extended. Any global functions and variables are available for use by the host element.

A host element can call the load_extension() function to load one specific extension, or load_all_extensions() to load everything that is available. These are defined in the freeform prairielearn module.

A two-way flow of logic and information exists between elements and their extensions.

Importing an Extension From an Element

Loading extension Python scripts returns a named tuple of all globally defined functions and variables. Loading all extensions will return a dictionary mapping the extension name to its named tuple. For example, if an extension were to define the following in their controller:

def my_cool_function():
    return "hello, world!"

The host element could then call this by running the following:

import prairielearn as pl

def render(element_html, data):
    extension = pl.load_extension(data, "extension_name")
    contents = extension.my_cool_function()
    return contents

This small example above will render "hello world!" to the question page. Note that when loading all extensions with load_all_extensions(), modules are returned in ascending alphabetical order.

Importing a Host Element From an Extension

Extensions can also import files from their host element with the load_host_script() function. This can be used to obtain helper functions, class definitions, constant variables, etc. If the host element were to contain, for example:

import prairielearn as pl

STATIC_VARIABLE = "hello"

def render(element_html, data):
    extension = pl.load_extension(data, "extension_name")
    contents = extension.my_cool_function()
    return contents

The extension could then access STATIC_VARIABLE by importing the host script:

import prairielearn as pl

host_element = pl.load_host_script("pl-host-element.py")

def my_cool_function():
    return host_element.STATIC_VARIABLE

Extension Dependencies

Similar to how questions and elements may require client-side assets (as described in the element developer guide), extensions may also require client-side JavaScript and CSS. The different properties are summarized here. Note that script dependencies may be set as either static or dynamic dependencies, while styles may only be set as static dependencies.

Property Description
nodeModulesStyles The styles required by this extension, relative to [PrairieLearn directory]/node_modules.
nodeModulesScripts The scripts required by this extension, relative to [PrairieLearn directory]/node_modules.
extensionStyles The styles required by this element relative to the extension's directory, [course directory]/elementExtensions/element-name/extension-name.
extensionScripts The scripts required by this element relative to the extension's directory, [course directory]/elementExtensions/element-name/extension-name.
clientFilesCourseStyles The styles required by this extension relative to [course directory]/clientFilesCourse.
clientFilesCourseScripts The scripts required by this extension relative to [course directory]/clientFilesCourse.

Note that any element extension assets declared in dependencies will always be loaded, regardless of whether their Python controller was loaded or not. As such, it is recommended that, when suitable, extensions make use of dynamicDependencies to load scripts only when necessary, based on the context/usage of the element.

Other Client Files

Other files available to the client may also be loaded, such as images or any downloadable content. These client files should be placed in clientFilesExtension in the extension directory, and the full URL to that folder is given to the host extension in data["options"]["client_files_extensions_url"][extension_name]. If this path is needed by the extension itself, it may be passed as an argument to a defined extension function.