跳转至

元素开发者指南

有关示例元素,请参阅 elements/

元素代码使用 prairielearn 模块 来实现解析属性和计算分数等常见功能。

Info

在决定元素使用的属性时,请避免使用通常在 HTML 元素中解释为布尔属性的属性名称。这些属性名称可能会被错误地解释,并且它们的值可能会转换为与您的原始意图不同的含义。特别是,您应该避免以下属性:checkedcompactdeclaredeferdisabledismapmultiplenohrefnoresizenoshadenowrapreadonlyselected

此外,建议您避免使用在 HTML 中具有特殊含义的属性名称(例如 onclickstyle),因为它们可能会被 IDE 误解。

元素的剖析

ZJUI-Learn 服务器当前版本中可用的系统范围元素 位于 [ZJUI-Learn directory]/elements 与元素名称对应的文件夹内。 您还可以在目录中包含特定于课程的元素 课程存储库的根目录,例如 [course directory]/elements。 见exampleCourse/elements 举一个真实的例子。

按照惯例, 所有元素文件的命名与其所属的元素相同。那个目录 应包含一个 info.json 文件,其中包含有关元素的元数据,包括 哪个文件是元素控制器以及元素的任何依赖项。有关详细信息,请参阅有关依赖项的部分

每个元件应有一个 .py 控制器,其中包含列出的功能 在下一节中。该控制器负责渲染元素, 解析学生提交的内容,并可选择对提交内容进行评分。

作为一个简单的示例,元素 pl-my-element 将具有以下文件 结构:

pl-my-element
+-- info.json
|-- pl-my-element.py
|-- pl-my-element.mustache
|-- pl-my-element.js
`-- pl-my-element.css

以及 info.json ,其内容如下:

info.json
{
  "controller": "pl-my-element.py",
  "dependencies": {
    "elementScripts": ["pl-my-element.js"],
    "elementStyles": ["pl-my-element.css"]
  }
}

元素功能

所有元素函数均具有以下形式:

def fcn(element_html, data):

请注意,并非所有函数都具有相同的返回类型。论据是:

论证 类型 描述
element_html 字符串 元素的模板 HTML。
data 字典 问题的可变数据,可以修改和返回。

data 字典具有以下可能的键(并非所有键都会出现在所有元素函数中):

关键 类型 描述
data["ai_grading"] 布尔 问题是否正在渲染以进行 AI 评分。
data["correct_answer_shown"] 布尔 正确答案当前是否可见(即,呈现答案面板;在呈现其他面板时使用)
data["correct_answers"] 字典 变体的真实答案(如果有)。
data["editable"] 布尔 问题当前是否处于可编辑状态。
data["extensions"] 字典 可由此元素加载的扩展列表。有关更多信息,请参阅元素扩展 文档。
data["feedback"] 字典 对学生提交的答案的任何反馈。
data["format_errors"] 字典 解析学生输入时遇到的任何错误。
data["gradable"] 布尔 提交的内容是否可以评分。如果存在格式错误,则自动设置为 false
data["manual_grading"] 布尔 是否应显示手动评分内容。这是手动评分视图中的 true,也适用于 AI 评分渲染时的问答面板。
data["num_valid_submissions"] 整数 学生针对当前变体提交的有效(不包含格式错误)的数量。
data["options"] 字典 与问题相关的任何选项,例如访问客户端文件
data["panel"] 字符串 正在渲染哪个面板(questionsubmissionanswer)。
data["params"] 字典 描述问题变体的参数。
data["preferences"] 字典 当前评估上下文的只读问题首选项。值来自问题的默认值与任何评估覆盖的合并。
data["partial_scores"] 字典 问题中各个变量的部分得分。
data["raw_submitted_answers"] 字典 解析前学生提交的答案。
data["score"] 浮动 该问题的最终总分。
data["submitted_answers"] 字典 学生提交的答案(解析后)。
data["variant_seed"] 整数 该问题变体的随机种子。

为了使多个元素可以同时存在于一个问题中,约定是每个元素实例都与一个或多个变量相关联。这些变量是数据元素字典中的键。例如,如果有变量 xy 那么我们可能有:

data["correct_answers"]["x"] = 4
data["correct_answers"]["y"] = 7
data["submitted_answers"]["x"] = 4
data["submitted_answers"]["y"] = 12

这种结构(其中字典以变量作为键)用于 data 中的所有字典。

元素函数为:

功能 返回对象 可修改的 data 按键 不可修改的 data 按键 描述
prepare() None correct_answersparams extensionsoptionspreferencesvariant_seed 验证并准备元素的初始数据。在问题的 generate() 函数执行后执行,并且可以访问该函数创建的数据。
render() str (html) correct_answer_showncorrect_answerseditableextensionsfeedbackformat_errorsmanual_gradingnum_valid_submissionsoptionspanelparamspartial_scorespreferencesraw_submitted_answersscoresubmitted_answersvariant_seed 渲染一个面板的 HTML 并将其作为字符串返回。
parse() None correct_answersfeedbackformat_errorsparamssubmitted_answers extensionsoptionspreferencesraw_submitted_answersvariant_seed 解析学生输入的data["submitted_answers"][var]数据,修改该变量。
grade() None correct_answersfeedbackformat_errorsparamspartial_scoresscoresubmitted_answers extensionsoptionspreferencesraw_submitted_answersvariant_seed data["submitted_answers"][var] 来确定分数。将分数和任何反馈分别存储在 data["partial_scores"][var]["score"]data["partial_scores"][var]["feedback"] 中。 注意: 避免修改 data["feedback"] 字典,因为这意味着由自定义问题使用。
test() None format_errorspartial_scoresraw_submitted_answersscore extensionsgradablepreferencestest_type 为此元素创建测试提交,在从“设置”面板运行测试时使用。应在 data["raw_submitted_answers"][var] 中设置一个值,并在 data["partial_scores"][var] 中设置预期分数(如果是 invalid,则为 data["format_errors"][var])。要测试的输入类型在 data["test_type"] 中给出,可以是 correctincorrectinvalid 之一。

Note

与问题 server.py 文件不同,元素控制器没有 generate() 功能。问题的server.py generate()函数负责生成随机参数和正确答案。元素应使用 prepare() 进行依赖于这些参数的任何初始化。

上表描述了每个函数的用途以及 data 中允许修改的值。对 data 中的值的任何允许的更改都将保留到数据库中。 data 中不允许添加或删除密钥的功能。

上述所有函数在 问题代码 中都有等效项(即问题自己的 server.py 文件)。当在元素和问题中都声明函数时,对于问题中的每个元素,始终首先执行元素函数,按照元素在问题中出现的顺序,然后执行问题函数。这允许问题代码在必要时覆盖或修改元素的行为。

元素依赖性

您的元素可能依赖于某些客户端资产,例如脚本或样式表。为了保持 HTML、CSS 和 JS 的清晰分离,您可以将这些依赖项放置在其他文件中。如果您依赖于 lodashd3 等库,您还可以链接到包含这些库的节点模块。 ZJUI-Learn 将编译页面上所有元素所需的所有依赖项的列表,删除重复的依赖项,并确保它们加载到页面上。

依赖项列在元素的 info.json 中。您可以为您的元素配置它们,如下所示:

info.json
{
  "controller": "pl-my-element.py",
  "dependencies": {
    "nodeModulesScripts": ["three/build/three.min.js"],
    "elementScripts": ["pl-my-element.js"],
    "elementStyles": ["pl-my-element.css"],
    "clientFilesCourseStyles": ["courseStylesheet1.css", "courseStylesheet2.css"]
  }
}

下表总结了当前可用的不同类型的依赖属性:

物业 描述
nodeModulesStyles 该元素所需的样式,相对于 [ZJUI-Learn directory]/node_modules
nodeModulesScripts 该元素所需的脚本,相对于 [ZJUI-Learn directory]/node_modules
elementStyles 该元素所需的样式相对于元素的目录,即 [ZJUI-Learn directory]/elements/this-element-name[course directory]/elements/this-element-name
elementScripts 该元素所需的脚本相对于元素的目录,即 [ZJUI-Learn directory]/elements/this-element-name[course directory]/elements/this-element-name
clientFilesCourseStyles 该元素所需的样式相对于[course directory]/clientFilesCourse(注意:此属性仅适用于特定课程目录中托管的元素,不适用于系统范围的 ZJUI-Learn 元素。)
clientFilesCourseScripts 该元素所需的脚本相对于 [course directory]/clientFilesCourse(注意:此属性仅适用于特定课程目录中托管的元素,不适用于系统范围的 ZJUI-Learn 元素。)

coreScriptscoreStyles 属性用于旧元素和问题,但已弃用,不应在新对象中使用。它列出了该元素所需的脚本和样式,分别相对于 [ZJUI-Learn directory]/public/javascripts[ZJUI-Learn directory]/public/stylesheets[ZJUI-Learn directory]/public/javascripts中的脚本主要用于与遗留元素和问题的兼容性,而[ZJUI-Learn directory]/public/stylesheets中的样式是为特定页面而不是单个元素使用的样式保留的。

除了静态依赖关系之外,元素还可以声明动态依赖关系,对应于仅在认为必要时才加载的脚本。例如,如果一个元素可以使用 d3 库,但仅在某些情况下,它可以声明对 d3 的依赖关系:

info.json
{
  "controller": "pl-my-element.py",
  "dependencies": {
    "elementScripts": ["pl-my-element.js"]
  },
  "dynamicDependencies": {
    "nodeModulesScripts": { "d3": "d3/dist/d3.min.js" }
  }
}

然后,元素自己的脚本(例如,pl-my-element.js)可以仅在认为必要时动态导入 d3 库:

if (options.use_d3) {
  import('d3').then((module) => {
    // use d3 here
  });
}

动态依赖关系是使用导入映射 实现的,它允许元素脚本中的 import 调用通过 info.json 文件中定义的名称而不是完整的 URL 来引用模块。这也可以用作 ESM 模块静态依赖关系的替代方案,因为它允许使用 ESM 语法导入模块。

动态依赖关系可能指向:

物业 描述
nodeModulesScripts 该元素所需的脚本,相对于 [ZJUI-Learn directory]/node_modules
elementScripts 该元素所需的脚本相对于元素的目录,即 [ZJUI-Learn directory]/elements/this-element-name[course directory]/elements/this-element-name
clientFilesCourseScripts 该元素所需的脚本相对于 [course directory]/clientFilesCourse(注意:此属性仅适用于特定课程目录中托管的元素,不适用于系统范围的 ZJUI-Learn 元素。)

请注意,动态依赖项中使用的密钥将在问题中可用的所有元素之间共享。例如,如果问题中的两个元素都声明对 d3 的动态依赖项,则 d3 库只会加载一次,即使两个元素都使用它。因此,在定义动态依赖项的键时,使用以下约定非常重要:

  • 对于节点模块:使用 package.json 文件中定义的模块名称。这将允许使用同一模块的多个元素共享相同的依赖关系,而无需加载该模块两次。也就是说,对于课程元素,如果可能的话,您应该避免直接使用节点模块依赖项,如下所述
  • 对于元素脚本:使用元素的名称,后跟斜杠,然后是脚本的名称。例如,如果元素名为 pl-my-element,脚本名为 my-element.js,则键应为 pl-my-element/my-element.js
  • 对于 clientFilesCourse 脚本:使用任何与上述命名不冲突的特定于课程的约定。

您还可以在架构参考中找到有关依赖关系类型的更多详细信息:

在元素代码中使用节点依赖关系

请注意,仅 ZJUI-Learn 本身所依赖的依赖项支持使用节点模块(nodeModulesScriptsnodeModulesStyles)。这些依赖项可以在 ZJUI-Learn 存储库的 apps/prairielearn/package.json 文件的 dependencies 部分中找到。

Warning

虽然支持在课程元素中使用节点模块依赖项,但应尽可能避免。特别要注意的是,节点模块可能会在没有警告的情况下更新,这在某些情况下可能会破坏您的元素。另请注意,虽然传递依赖项(即依赖项的依赖项)在某些情况下可能有效,但不能保证它们将来继续工作,因为依赖项更新或 ZJUI-Learn 配置中的更新可能会更改传递依赖项的可用性。

如果您的代码依赖于节点模块,建议的操作过程是将模块复制到元素目录或 clientFilesCourse 中,然后从那里链接到该模块。这样,您就可以控制模块何时更新,并且可以确保更新不会破坏您的元素。

要将节点模块复制到元素目录中,请首先获取该模块的适当包(例如,从模块的主页、CDN 或自行构建),然后将其放入元素目录中。例如,如果您想使用 moment 库,您可以从模块网页 (https://momentjs.com/) 或 CDN(例如 https://cdn.jsdelivr.net/npm/moment@2.30.1/dist/moment.min.js)获取捆绑包,并将其作为 moment.min.js 放置在您的元素目录中。然后,您可以在 info.json 中链接到该文件,如下所示:

info.json
{
  "controller": "pl-my-element.py",
  "dependencies": {
    "elementScripts": ["moment.min.js"]
  }
}

或者,如果您使用的模块将由多个元素使用,或者直接由问题使用,那么最好将该模块放置在 clientFilesCourse 中,而不是将其复制到每个元素目录中。在这种情况下,您可以从 info.json 链接到模块,如下所示:

info.json
{
  "controller": "pl-my-element.py",
  "dependencies": {
    "clientFilesCourseScripts": ["moment.min.js"]
  }
}

对于动态依赖关系,同样的建议也适用。在这种情况下,建议您使用不太可能与其他动态依赖项冲突的密钥,例如 pl-my-element/moment 用于 pl-my-element 使用的 moment 依赖项,或 course-specific-name/moment 用于课程中多个元素使用的 moment 依赖项。在这种情况下,请确保您的元素脚本使用 info.json 文件中定义的相同密钥导入模块,例如元素脚本依赖项的 import('pl-my-element/moment')。例如,如果将 moment 库作为 moment.min.js 复制到元素目录中,则可以定义对此文件的动态依赖项,如下所示:

info.json
{
  "controller": "pl-my-element.py",
  "dependencies": {
    "elementScripts": ["pl-my-element.js"]
  },
  "dynamicDependencies": {
    "elementScripts": { "pl-my-element/moment": "moment.min.js" }
  }
}

Note

请注意,通过使用上述方法,您有责任确保该模块是最新的,包含最新的更改和安全更新。如果您选择将节点模块复制到元素目录中,则可以选择在文件名中包含版本号(例如,moment-2.30.1.min.js),以便更轻松地跟踪您正在使用的版本并在必要时更新它。