元素开发者指南¶
有关示例元素,请参阅 elements/。
元素代码使用 prairielearn 模块 来实现解析属性和计算分数等常见功能。
Info
在决定元素使用的属性时,请避免使用通常在 HTML 元素中解释为布尔属性的属性名称。这些属性名称可能会被错误地解释,并且它们的值可能会转换为与您的原始意图不同的含义。特别是,您应该避免以下属性:checked、compact、declare、defer、disabled、ismap、multiple、nohref、noresize、 noshade、nowrap、readonly、selected。
此外,建议您避免使用在 HTML 中具有特殊含义的属性名称(例如 onclick 或 style),因为它们可能会被 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 ,其内容如下:
{
"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"] |
字符串 | 正在渲染哪个面板(question、submission 或 answer)。 |
data["params"] |
字典 | 描述问题变体的参数。 |
data["preferences"] |
字典 | 当前评估上下文的只读问题首选项。值来自问题的默认值与任何评估覆盖的合并。 |
data["partial_scores"] |
字典 | 问题中各个变量的部分得分。 |
data["raw_submitted_answers"] |
字典 | 解析前学生提交的答案。 |
data["score"] |
浮动 | 该问题的最终总分。 |
data["submitted_answers"] |
字典 | 学生提交的答案(解析后)。 |
data["variant_seed"] |
整数 | 该问题变体的随机种子。 |
为了使多个元素可以同时存在于一个问题中,约定是每个元素实例都与一个或多个变量相关联。这些变量是数据元素字典中的键。例如,如果有变量 x 和 y 那么我们可能有:
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_answers、params |
extensions、options、preferences、variant_seed |
验证并准备元素的初始数据。在问题的 generate() 函数执行后执行,并且可以访问该函数创建的数据。 |
render() |
str (html) |
correct_answer_shown、correct_answers、editable、extensions、feedback、format_errors、manual_grading、num_valid_submissions、options、 panel、params、partial_scores、preferences、raw_submitted_answers、score、submitted_answers、variant_seed |
渲染一个面板的 HTML 并将其作为字符串返回。 | |
parse() |
None |
correct_answers、feedback、format_errors、params、submitted_answers |
extensions、options、preferences、raw_submitted_answers、variant_seed |
解析学生输入的data["submitted_answers"][var]数据,修改该变量。 |
grade() |
None |
correct_answers、feedback、format_errors、params、partial_scores、score、submitted_answers |
extensions、options、preferences、raw_submitted_answers、variant_seed |
用 data["submitted_answers"][var] 来确定分数。将分数和任何反馈分别存储在 data["partial_scores"][var]["score"] 和 data["partial_scores"][var]["feedback"] 中。 注意: 避免修改 data["feedback"] 字典,因为这意味着由自定义问题使用。 |
test() |
None |
format_errors、partial_scores、raw_submitted_answers、score |
extensions、gradable、preferences、test_type |
为此元素创建测试提交,在从“设置”面板运行测试时使用。应在 data["raw_submitted_answers"][var] 中设置一个值,并在 data["partial_scores"][var] 中设置预期分数(如果是 invalid,则为 data["format_errors"][var])。要测试的输入类型在 data["test_type"] 中给出,可以是 correct、incorrect 或 invalid 之一。 |
Note
与问题 server.py 文件不同,元素控制器没有 generate() 功能。问题的server.py generate()函数负责生成随机参数和正确答案。元素应使用 prepare() 进行依赖于这些参数的任何初始化。
上表描述了每个函数的用途以及 data 中允许修改的值。对 data 中的值的任何允许的更改都将保留到数据库中。 data 中不允许添加或删除密钥的功能。
上述所有函数在 问题代码 中都有等效项(即问题自己的 server.py 文件)。当在元素和问题中都声明函数时,对于问题中的每个元素,始终首先执行元素函数,按照元素在问题中出现的顺序,然后执行问题函数。这允许问题代码在必要时覆盖或修改元素的行为。
元素依赖性¶
您的元素可能依赖于某些客户端资产,例如脚本或样式表。为了保持 HTML、CSS 和 JS 的清晰分离,您可以将这些依赖项放置在其他文件中。如果您依赖于 lodash 或 d3 等库,您还可以链接到包含这些库的节点模块。 ZJUI-Learn 将编译页面上所有元素所需的所有依赖项的列表,删除重复的依赖项,并确保它们加载到页面上。
依赖项列在元素的 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 元素。) |
coreScripts 和 coreStyles 属性用于旧元素和问题,但已弃用,不应在新对象中使用。它列出了该元素所需的脚本和样式,分别相对于 [ZJUI-Learn directory]/public/javascripts 和 [ZJUI-Learn directory]/public/stylesheets。 [ZJUI-Learn directory]/public/javascripts中的脚本主要用于与遗留元素和问题的兼容性,而[ZJUI-Learn directory]/public/stylesheets中的样式是为特定页面而不是单个元素使用的样式保留的。
除了静态依赖关系之外,元素还可以声明动态依赖关系,对应于仅在认为必要时才加载的脚本。例如,如果一个元素可以使用 d3 库,但仅在某些情况下,它可以声明对 d3 的依赖关系:
{
"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 本身所依赖的依赖项支持使用节点模块(nodeModulesScripts 和 nodeModulesStyles)。这些依赖项可以在 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 中链接到该文件,如下所示:
{
"controller": "pl-my-element.py",
"dependencies": {
"elementScripts": ["moment.min.js"]
}
}
或者,如果您使用的模块将由多个元素使用,或者直接由问题使用,那么最好将该模块放置在 clientFilesCourse 中,而不是将其复制到每个元素目录中。在这种情况下,您可以从 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 复制到元素目录中,则可以定义对此文件的动态依赖项,如下所示:
{
"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),以便更轻松地跟踪您正在使用的版本并在必要时更新它。