server.py¶
每个问题的 server.py 文件通过生成随机参数和相应的正确答案来创建随机问题变体。
以下部分构建了一个简单的问题示例,要求学生将数字加倍。问题将如下所示:
<pl-question-panel>
If $x = {{params.x}}$, what is $y$ if $y$ is double $x$?
</pl-question-panel>
<pl-integer-input answers-name="y" label="$y =$"></pl-integer-input>
<pl-submission-panel>
{{feedback.y}}
</pl-submission-panel>
Tip
有关 {{params.x}} 和 {{feedback.y}} Mustache 语法的更多详细信息可以在 问题模板文档 中找到。
步骤一:generate¶
首先,调用 generate 函数来生成问题变体。它应该使用问题的任何必要参数更新 data["params"],并使用正确答案更新 data["correct_answers"]。
import random
def generate(data):
# Generate random parameters for the question and store them in the data["params"] dict:
data["params"]["x"] = random.randint(5, 10)
# Also compute the correct answer (if there is one) and store in the data["correct_answers"] dict:
data["correct_answers"]["y"] = 2 * data["params"]["x"]
这些值可以在带有 Mustache 语法的 question.html 文件中使用。例如,您可以将 data["params"]["x"] 与 {{params.x}} 一起使用。
下面的代码片段使用 <pl-question-panel> 元素,因此它仅在 问题面板 的上下文中显示。
<pl-question-panel>
If $x = {{params.x}}$, what is $y$ if $y$ is double $x$?
</pl-question-panel>
随机化¶
问题变体根据变体种子 (data["variant_seed"]) 进行随机化,并且所有随机生成器(random、np.random 等)均以此值作为种子。
???提示“生成虚假数据”
If you need to generate fake data, you can use the [`faker`](https://faker.readthedocs.io/en/master/) library. This is useful for generating random names, addresses, and other data that looks real but is not.
```python title="server.py"
from faker import Faker
fake = Faker()
fake.name()
# 'Lucy Cechtelar'
```
参数生成¶
对于生成的浮点答案,在向学生显示数字时以及计算正确答案时使用一致的舍入非常重要。向学生显示四舍五入的数字,但使用未四舍五入的版本进行评分可能会导致意外的结果。
===“好”
```python title="server.py"
def generate(data):
# Rounds numbers at the beginning
a = np.round(33.33337, 2)
b = np.round(33.33333, 2)
data["params"]["a_for_student"] = f'{a:.2f}'
data["params"]["b_for_student"] = f'{b:.2f}'
data["correct_answers"]["c"] = a - b
```
===“不好”
```python title="server.py"
def generate(data):
a = 33.33337
b = 33.33333
data["params"]["a_for_student"] = f'{a:.2f}'
data["params"]["b_for_student"] = f'{a:.2f}'
# Correct answer is computed with full precision,
# but the parameters displayed to students are rounded.
data["correct_answers"]["c"] = a - b
```
步骤2:prepare¶
接下来,在所有元素(例如 <pl-integer-input>)运行 prepare() 后调用 prepare 函数。可以这样做来进行任何类型的最终后处理,但并不常用。
步骤3:render¶
接下来,调用 render(data, html) 函数来呈现问题。您可以使用此函数来覆盖问题的呈现方式。渲染函数需要两个参数:data 和 html,并且应返回 HTML 字符串。小胡子模板渲染后的 HTML 可通过 html 参数获得。除了非常高级的问题之外,很少使用它,并且在本示例中我们不需要它。
步骤4:parse¶
当学生提交答案时,在各个元素解析提交的答案后,将调用 parse 函数来解析提交的答案。此函数可用于显示比输入元素更具体的格式错误或以不同的方式解析输入。当我们的解析函数运行时,<pl-integer-input> 已经将提交的值解析为整数,如果该值无效,则会向学生显示错误。在此示例中,我们将允许学生仅提交正整数,因此我们将检查这一点,如果是负数,则使用 data["format_errors"] 设置格式错误。
parse 函数通常读取或修改的值是:
data["raw_submitted_answers"][NAME]- 学生提交的确切原始答案。data["submitted_answers"][NAME]- 按元素解析的答案(例如字符串转换为数字)。data["format_errors"][NAME]- 命名答案的格式错误消息。
如果存在格式错误,则提交内容为“无效”且不会评分。要提供反馈但保持提交“有效”,可以改用 data["feedback"][NAME]。
def parse(data):
# Reject negative numbers for "y" if we don't already have a format error
if "y" not in data["format_errors"] and int(data["submitted_answers"]["y"]) < 0:
data["format_errors"]["y"] = "Negative numbers are not allowed"
如果我们需要进一步处理输入,我们也可以直接修改提交的答案。例如,如果我们想接受否定答案作为其绝对值,我们可以这样做:
def parse(data):
# Ensure that "y" is positive
data["submitted_answers"]["y"] = abs(int(data["submitted_answers"]["y"]))
Info
一般来说,问题生成过程的每个函数都在问题中的所有元素“之后”运行。例如,server.py 中的 parse() 函数在所有元素运行完其 parse() 函数后运行。使用 data 字典时记住这一点很重要,因为它将包含元素完成处理*之后的结果。
尽管自定义评分的问题可能不依赖于各个元素的完整评分功能,但强烈建议使用内置元素进行学生输入,因为这些元素默认包含有用的解析和反馈。解析后的学生答案存在于 data["submitted_answers"] 词典中。
Note
数据字典中 "submitted_answers" 键下存储的数据可能具有不同的类型。具体来说,pl-integer-input
element 有时将非常大的整数存储为字符串,而不是大多数情况下使用的 Python int 类型。定制分级机的最佳实践
这种情况下的代码始终将数据转换为所需的类型,例如 int(data["submitted_answers"][name])。请参阅
ZJUI-Learn 元素文档 有关特定元素的更详细讨论。
parse() 函数还可用于创建要发送到外部分级机的自定义文件。这可以通过 pl.add_submitted_file() 函数来完成,如下例所示:
import prairielearn as pl
def parse(data):
code = f"x = {data["submitted_answers"]["expression"]}"
pl.add_submitted_file(data, "user_code.py", raw_contents=code)
步骤5:grade¶
最后调用grade(data)函数对问题进行评分。年级职能负责:
- 设置
data["partial_scores"]中每个命名答案的分数和反馈。 - 将问题的总分设置为
data["score"]。 - 在
data["feedback"]中设置问题的总体反馈。
该函数仅在 parse() 未产生格式错误时运行,因此我们可以假设所有数据都是有效的。在此之前,所有元素都已经对他们的答案(如果有)进行了评分。
grade 函数通常读取或修改的值是:
data["partial_scores"][NAME]["score"]- 指定答案的单独分数(0 到 1)。data["partial_scores"][NAME]["feedback"]- 来自评分元素的每个命名答案的反馈。data["score"]- 问题的总分(0 到 1)。data["feedback"][NAME]- 每个命名答案的总体问题反馈。
建议您在学生在解决方案方面取得进展时向他们提供额外的反馈,并奖励部分学分。
如果未定义该函数,则将根据 data["correct_answers"] 中设置的正确答案自动对问题进行评分。学生提供的每个答案也将得到评分要素的反馈。如果定义了 grade 函数,则您收到的数据已按元素分级。您应该确保仅在答案不正确时授予部分学分,否则您可能会为正确答案授予部分学分。在下面的代码片段中,我们使用 set_weighted_score_data 实用程序更新 data["score"]。
您可以设置 data["format_errors"][NAME] 将提交标记为无效。这将导致该问题不会耗尽学生的评估尝试之一。但是,我们鼓励您在 parse 而不是 grade 中尽可能多地检查无效数据;当学生在手动或外部评分的问题以及没有实时评分的评估中点击“仅保存”时,将调用 parse 函数。
import math
import prairielearn as pl
def grade(data):
# Give half points for incorrect answers larger than "x", only if not already correct.
# Use math.isclose to avoid possible floating point errors.
y_is_correct = math.isclose(data["partial_scores"]["y"]["score"], 1.0)
if not y_is_correct and int(data["submitted_answers"]["y"]) > data["params"]["x"]:
data["partial_scores"]["y"]["score"] = 0.5
pl.set_weighted_score_data(data)
data["feedback"]["y"] = "Your value for $y$ is larger than $x$, but incorrect."
没有固定正确答案的评分¶
自定义 grade 函数不仅限于将提交与 data["correct_answers"] 进行比较:
- 对于具有多个正确答案 (e.g., "give an example of a matrix with some property") 的问题,评分功能可以检查提交的答案是否满足所需的属性。在这种情况下,
data["correct_answers"]可以保存一个有效答案的示例来向学生展示。有关示例,请参阅此演示问题。 - 对于学生收集自己的数据(例如,实验室实验的测量结果)的问题,评分函数可以根据学生自己提交的值计算预期答案,以便接受与他们的数据一致的任何答案。有关示例,请参阅此演示问题。
提供反馈¶
要设置自定义反馈,评分功能应在 data["feedback"] 字典中设置相应的条目。这些反馈条目在渲染 question.html 时传入,可以使用小胡子前缀 {{feedback.}} 来访问。有关示例,请参阅上面的示例 或此演示问题。
某些元素通过 data["partial_scores"][NAME]["feedback"] 提供反馈,您可以在评分功能中覆盖该反馈。该字段通常是字符串,但某些元素(例如 <pl-drawing> 和 <pl-checkbox>)使用不同的类型。此处提供的反馈将附加到学生对该要素的回答中,这可以使学生更轻松地解释他们在较长问题中收到的反馈。由于并非所有元素都以这种方式提供反馈,并且有些元素使用不同的数据结构来表示它们提供的反馈,因此我们建议使用 data["feedback"][NAME] 来提供特定于问题的反馈。
!!! tip “针对特定答案的反馈”
如果您想对不使用/支持 `data["partial_scores"][NAME]["feedback"]` 的特定答案提供反馈,
您可以直接在 `question.html` 中的元素之后呈现反馈:
<!-- prettier-ignore -->
```html title="question.html"
<pl-string-input ...></pl-string-input>
{{#feedback.value1}}<div>Feedback: {{.}}</div>{{/feedback.value1}}
```
data["feedback"][NAME] 中的总体问题反馈需要使用 {{feedback.NAME}} 在 question.html 模板中显式呈现。如果元素支持,data["partial_scores"][NAME]["feedback"] 中给出的反馈将由元素自动呈现(假设该元素可见)。
对浮点答案进行评分¶
对于涉及浮点数的评分函数,避免与 == 进行精确比较。与 == 的浮点比较可能会意外失败,因为 Python 中的计算可能会引入浮点错误。相反,请使用 math.isclose 函数,因为它在给定容差内执行比较。 prairielearn Python 库还提供了多个函数来执行更专业的比较:
is_correct_scalar_ra使用相对和绝对容差比较浮点数。is_correct_scalar_sf将浮点数与指定数量的有效数字进行比较。is_correct_scalar_dd将浮点数与指定位数进行比较。
重新计算分数¶
整个问题的任何自定义评分函数都应将 data["score"] 设置为 0.0 到 1.0 之间的值,这将是给定问题的最终分数。如果自定义评分函数仅对问题的特定部分进行评分,则评分函数应在 data["partial_scores"] 中设置相应的字典条目,然后重新计算整个问题的最终 data["score"] 值。 prairielearn Python 库提供以下分数重新计算函数:
set_weighted_score_data将data["score"]设置为data["partial_scores"]中条目的加权平均值。- 如果
data["partial_scores"]中的所有条目均为 1.0,set_all_or_nothing_score_data将data["score"]设置为 1.0,否则设置为 0.0。
可以像这样使用:
import prairielearn as pl
def grade(data):
# update partial_scores as necessary
data["partial_scores"]["y"]["score"] = 0.5
# compute total question score
pl.set_weighted_score_data(data)
更详细的信息可以在分级实用程序文档 中找到。
如果您不想显示各个部分的分数徽章,则可以在计算 data["score"] 后取消设置 data["partial_scores"] 中的字典条目。
???示例“代码片段”
```python title="server.py"
import prairielearn as pl
def grade(data):
# update partial_scores as necessary
data["partial_scores"]["y"]["score"] = 0.5
# compute total question score
pl.set_weighted_score_data(data)
# unset partial scores
data["partial_scores"]["y"]["score"] = None
```
问题生命周期¶
下图显示了问题的生命周期,包括调用的服务器函数、呈现的不同面板以及与学生的交互点。
完整示例¶
完成的完整 question.html 和 server.py 示例如下所示:
<pl-question-panel>
If $x = {{params.x}}$, what is $y$ if $y$ is double $x$?
</pl-question-panel>
<pl-integer-input answers-name="y" label="$y =$"></pl-integer-input>
<pl-submission-panel>
{{feedback.y}}
</pl-submission-panel>
import random
import math
import prairielearn as pl
def generate(data):
# Generate random parameters for the question and store them in the data["params"] dict:
data["params"]["x"] = random.randint(5, 10)
# Also compute the correct answer (if there is one) and store in the data["correct_answers"] dict:
data["correct_answers"]["y"] = 2 * data["params"]["x"]
def parse(data):
# Reject negative numbers for "y" if we don't already have a format error
if "y" not in data["format_errors"] and int(data["submitted_answers"]["y"]) < 0:
data["format_errors"]["y"] = "Negative numbers are not allowed"
def grade(data):
# Give half points for incorrect answers larger than "x", only if not already correct.
# Use math.isclose to avoid possible floating point errors.
y_is_correct = math.isclose(data["partial_scores"]["y"]["score"], 1.0)
if not y_is_correct and int(data["submitted_answers"]["y"]) > data["params"]["x"]:
data["partial_scores"]["y"]["score"] = 0.5
pl.set_weighted_score_data(data)
data["feedback"]["y"] = "Your value for $y$ is larger than $x$, but incorrect."
server.py功能¶
该表总结了 server.py 中可以定义的函数。
| 功能 | 更新data? |
可修改的 data 按键 |
描述 |
|---|---|---|---|
generate() |
correct_answers、params |
为新的随机问题变体生成参数和真实答案。根据需要为任何变量设置 data["params"][name] 和 data["correct_answers"][name]。就地修改 data 字典。 |
|
prepare() |
answers_names、correct_answers、params |
元素代码运行后的最终问题准备。可以根据需要修改数据。就地修改 data 字典。 |
|
render() |
不适用。以字符串形式返回 html |
渲染一个面板的 HTML 并将其作为字符串返回。 | |
parse() |
correct_answers、feedback、format_errors、params、submitted_answers |
解析学生输入的data["submitted_answers"][var]数据,修改该变量。就地修改 data 字典。 |
|
grade() |
correct_answers、feedback、format_errors、params、partial_scores、score、submitted_answers |
用 data["submitted_answers"][var] 来确定分数。将分数和任何反馈存储在 data["partial_scores"][var]["score"] 和 data["partial_scores"][var]["feedback"] 中。就地修改 data 字典。 |
|
file() |
不适用。返回 object(字符串、字节类、文件类) |
动态生成文件对象来代替物理文件。通过问题元素中的 type="dynamic" 触发(例如,pl-figure、pl-file-download)。通过 data['filename'] 访问请求的文件名。如果 file() 不返回任何内容,则将使用空字符串。 |
|
test() |
feedback、format_errors、gradable、partial_scores、raw_submitted_answers、score |
生成测试用例以确保问题可以对各种学生输入进行评分。 |
如表中所示,所有函数(render 除外)都接受单个参数 data(字典),并就地修改它。 render 函数接受两个参数:data 字典和根据模板和元素计算的 html 内容。
data 字典¶
| 属性 | 类型 | 描述 |
|---|---|---|
params |
dict |
问题变体的参数。这些在 generate() 函数中设置,并且可以在 question.html 文件中使用。 |
correct_answers |
dict |
问题变体的正确答案。每个项目都从一个命名答案映射到一个值。 |
submitted_answers |
dict |
学生对问题进行解析后提交的答案。 |
raw_submitted_answers |
dict |
针对该问题提交的原始学生答案。 |
format_errors |
dict |
每个答案的格式错误字典。每个项目都从命名答案映射到错误消息。 |
partial_scores |
dict |
每个答案的部分分数字典。每个条目都是一个字典,其键为 score、(float) 和 weight(int,可选)。 |
score |
float |
问题变体的总分。 |
feedback |
dict |
每个答案的反馈 词典。每个项目都从命名答案映射到反馈消息。 |
variant_seed |
int |
此问题变体的随机种子。 |
preferences |
dict |
当前评估上下文的只读问题首选项。值来自问题的默认值与任何评估覆盖的合并。 |
options |
dict |
系统提供的选项,例如文件路径和 URL(请参阅访问文件)。 |
filename |
str |
file() 函数中的请求的动态文件 的名称。 |
test_type |
str |
test() 函数 中运行的测试类型。 |
answers_names |
dict |
一本字典,其键列出了问题中答案的名称。 |
panel |
str |
正在渲染哪个面板(question、submission 或 answer)。 |
correct_answer_shown |
bool |
是否正在渲染答案面板(用于渲染其他面板时使用)。 |
editable |
bool |
问题当前是否处于可编辑状态。 |
num_valid_submissions |
int |
学生针对当前变体提交的有效(不包含格式错误)的数量。 |
manual_grading |
bool |
是否应显示手动评分内容。这是手动评分视图中的 true,也适用于 AI 评分渲染时的问答面板。 |
ai_grading |
bool |
问题是否正在渲染以进行 AI 评分。 |
gradable |
bool |
提交的内容是否可以评分。如果存在格式错误,则自动设置为 false。 |
并非所有字段在每个功能中都可用 - 有些字段仅存在于特定阶段。详情请参见下表数据字段范围。您还可以在QuestionData参考中查看所有字段的完整列表。
数据字段范围¶
data 字典中的每个字段要么存储每个变体(在所有提交中共享),要么存储每个提交(对于每个学生提交都是唯一的)。某些字段仅在特定功能中可用。
| 领域 | 范围 | 笔记 |
|---|---|---|
params |
两者 | 存储在变体和每次提交中。 |
correct_answers |
两者 | 存储在变体和每次提交中。 |
submitted_answers |
提交 | — |
raw_submitted_answers |
提交 | — |
format_errors |
提交 | — |
partial_scores |
提交 | — |
score |
提交 | — |
feedback |
提交 | — |
preferences |
变体 | |
variant_seed |
变体 | — |
options |
变体 | — |
filename |
无(未保存) | 仅在 file() 中。 |
test_type |
无(未保存) | 仅在 test() 中。 |
answers_names |
无(未保存) | 仅在 prepare() 中。 |
panel |
无(未保存) | 仅在 render() 中。 |
correct_answer_shown |
无(未保存) | 仅在 render() 中。 |
editable |
无(未保存) | 仅在 render() 中。 |
num_valid_submissions |
无(未保存) | 仅在 render() 中。 |
manual_grading |
无(未保存) | 仅在 render() 中。 |
ai_grading |
无(未保存) | 仅在 render() 中。 |
gradable |
提交 | 仅适用于 parse()、grade() 和 test()。 |
Note
params 和 correct_answers 在 generate() 中按变体设置,并存储在变体和每次提交中。提交副本是时间点快照。如果 parse() 或 grade() 修改这些值,则更新的值将保存到变体和提交中。
问题数据存储¶
与问题变体相关的所有持久数据都存储在 data 字典中的不同条目下。该字典由 ZJUI-Learn 以 JSON 格式存储,因此,data 中的所有内容都必须是 JSON 可序列化的。 Python 中的某些类型本身是 JSON 可序列化的,例如字符串、列表和字典,而其他类型则不是,例如复数、numpy ndarray 和 pandas DataFrame。
prairielearn Python库提供实用函数to_json和from_json(conversion_utils.py的一部分),可以序列化和反序列化作为问题数据的一部分存储的各种对象。请参阅这些函数的文档以获取更多信息。以下是如何使用它们来存储和检索 numpy 数组的简单示例:
import numpy as np
import prairielearn as pl
def generate(data):
data["params"]["numpy_array"] = pl.to_json(np.array([1.2, 3.5, 5.1]))
def grade(data):
pl.from_json(data["params"]["numpy_array"])
pl.to_json 函数支持不同类型编码的仅关键字选项(例如 pl.to_json(var, df_encoding_version=2))。添加这些选项是为了允许新的编码行为,同时仍然保留与现有用法的向后兼容性。
df_encoding_version控制 Pandas DataFrame 的编码。通过设置pl.to_json(df, df_encoding_version=2)对 DataFramedf进行编码允许缺失值和日期时间值,而pl.to_json(df, df_encoding_version=1)(default) 则不允许。但是,df_encoding_version=1支持复数,而df_encoding_version=2不支持。
np_encoding_version控制 Numpy 值的编码。当使用np_encoding_version=1时,那么pl.to_json只能序列化np.float64和np.complex128,并且反序列化后它们的类型将被擦除(将分别变成原生Pythonfloat和complex)。建议设置np_encoding_version=2,它支持所有 numpy 标量的序列化,并且不会导致反序列化时类型擦除。
访问磁盘上的文件¶
server.py中的函数还可以通过data["options"]字典从与问题相关的各个目录中检索内容,例如serverFilesCourse/和clientFilesQuestion/。更多详细信息,请参见客户端和服务器文件的文档。
访问用户和组身份¶
课程可以选择加入,以便 server.py 接收用户和组身份。课程所有者可以在课程设置页面上启用此功能。启用后,data["options"] 包含两个额外密钥:data["options"]["user"] 和 data["options"]["group"]。
def generate(data):
user = data["options"]["user"] # Variant owner; None on group assessments
# { "uid": "student@example.com", "uin": "123456", "name": "John Doe" }
group = data["options"]["group"] # None on individual assessments
# { "name": "Group 1", "members": [ { "uid": "student@example.com", "uin": "123456", "name": "John Doe" } ] }
if user is not None:
data["params"]["greeting"] = f"Hello, {user['name']}!"
if group is not None:
# group["members"] entries have the same shape as `user`.
data["params"]["group_member_uids"] = [m["uid"] for m in group["members"]]
user 字典具有键 uid(始终存在)、uin 和 name(后两个可能是 None)。团体评估为None。
group 字典有 name 和 members(与 user 形状相同的列表)。如果评估是个人作品,则为None。
???信息“提供了谁的身份”
When a staff member opens a student variant (e.g., in manual grading or opening student view), the `user` corresponds to the student that owns the variant, not the staff member or current viewer. Group assessments receive `None` because the shared variant has no single owner.
A group's members can change over time: the members when a question was generated may be different than when a question is graded. Similarly, a user's name, UID, and UIN may also change over time. The value of `options["user"]` and `options["group"]` will always reflect the latest information.
仅当以下所有为真时,才会向 server.py 提供用户和组数据:
- 课程已选择让
server.py接收用户数据。在生产中,课程所有者在课程设置页面上启用此功能;对于本地开发,可以在infoCourse.json中的"options"下设置"questionsReceiveUserData": true,这仅在开发模式下有效。 - 该问题不共享。一旦问题被公开共享或公开共享其来源,
server.py就永远不会收到用户数据,包括在问题本身的课程和公共预览中。 - 该问题按其原始过程呈现。对于通过共享集从其他课程导入的问题,
server.py永远不会接收用户数据,无论任一课程的设置如何。
当用户数据没有提供给server.py时,data["options"]["user"]和data["options"]["group"]都是None。钥匙始终存在。
使用 file() 生成动态文件¶
您可以在server.py中动态生成文件对象。这些文件永远不会实际出现在磁盘上。它们在 file() 中生成,并以字符串、类字节对象或类文件对象的形式返回。 file() 可以访问与 generate() 函数创建的对象相同的 data 对象,包括 data["params"] 和 data["correct_answers"]。使用动态生成的 fig.png 的完整 question.html 和 server.py 示例如下所示:
<p>Here is a dynamically-rendered figure showing a line of slope $a = {{params.a}}$:</p>
<pl-figure file-name="fig.png" type="dynamic"></pl-figure>
import random
import io
import matplotlib.pyplot as plt
def generate(data):
data["params"]["a"] = random.choice([0.25, 0.5, 1, 2, 4])
def file(data):
# check for the appropriate filename
if data["filename"] == "fig.png":
# plot a line with slope "a"
plt.plot([0, data["params"]["a"]], [0, 1])
# make a bytes object (a buffer)
buf = io.BytesIO()
# save the figure data into the buffer
plt.savefig(buf, format="png")
return buf
由于 pl-figure 的 type 设置为 "dynamic",因此对该 URL 的文件请求将被路由到 server.py 中的 file() 函数。请求的文件名存储在 data["filename"] 中,文件内容应从 file() 函数返回。
我们建议使用 pl-figure 和 pl-file-download 元素来显示或下载文件。具体来说,您应该对应下载或在单独选项卡中显示的文件(例如 PDF、源代码等)使用 pl-file-download,对应显示的图像使用 pl-figure。
??? info“高级动态文件使用”
The URL for the dynamically generated files is set to `{{options.client_files_question_dynamic_url}}` in the HTML, which is a special URL that ZJUI-Learn uses to route requests to the `file()` function. You could instead write `question.html` like so:
```html title="question.html"
<p>Here is a dynamically-rendered figure showing a line of slope $a = {{params.a}}$:</p>
<img src="{{options.client_files_question_dynamic_url}}/fig.png" />
```
依赖提交数据的动态文件¶
当动态文件包含在提交面板中时,它还将访问提交信息,包括 data["submitted_answers"]、data["score"]、data["partial_scores"] 和上面列出的所有其他与提交相关的数据。
如果您的动态文件依赖于提交数据(例如,data["submitted_answers"]),则应确保 pl-figure 或 pl-file-download 元素仅在提交面板中可见。这可以通过将元素包装在 <pl-submission-panel> 标记中来完成,如下所示。这可确保仅在学生提交答案后生成文件。
<pl-submission-panel>
<p>Here is a dynamically-rendered figure showing your submitted answer:</p>
<pl-figure file-name="submitted.png" type="dynamic"></pl-figure>
</pl-submission-panel>
请注意,提交面板将包含任何类型提交的文件,包括:
- 有效(正确或不正确)的提交。
- 无效提交(例如,提交的格式错误)。
- “仅保存”提交(例如,当学生单击“仅保存”或在没有实时评分的评估中)。
您应该确保您的文件生成代码可以处理所有这些情况。例如,如果文件依赖于有效提交的答案,则应在使用之前检查 data["submitted_answers"] 中的答案是否存在且有效。如果答案不存在,您可以生成默认文件或空白图像。
def file(data):
if data["filename"] == "submitted.png":
if data["submitted_answers"].get("y") is None:
# generate a default image
else:
# generate the file using data["submitted_answers"]["y"]
或者,您可以仅在答案存在且有效时将图像包含在 HTML 中,使用以 data["format_errors"] 为条件的小胡子:
<pl-submission-panel>
{{^format_errors.y}}
<p>Here is a dynamically-rendered figure showing your submitted answer:</p>
<pl-figure file-name="submitted.png" type="dynamic"></pl-figure>
{{/format_errors.y}}
</pl-submission-panel>
特别请注意,对于“仅保存”提交,不会填充分数。如果您的动态文件使用分数信息,则在使用它们之前应检查 data["score"] 或 data["partial_scores"] 中的值是否不是 None。
使用 test() 测试问题¶
问题测试功能位于问题的“设置”选项卡下。

调用test()函数来测试问题。此功能可用于确保您的问题正常运行。 test()函数生成的raw_submitted_answers将用来代替学生提交的内容,其余字段将与解析和评分的结果进行比较。如果您的代码崩溃,或者任何字段不同,测试将报告问题。
!!! warning “重要的”
大多数题不需要实现`server.py`中的`test()`函数。您只需在以下情况下实施它:
1. 您没有在问题的 `generate` 或 `prepare` 阶段设置 `correct_answers` 。在这种情况下,您使用的元素不知道如何生成正确的输入集,并且责任转移到 `server.py` 中的 `test()` 函数。
2. 您使用的元素尚未实现 `test()` 功能。所有第一方元素都实现了此功能,因此只有在您使用自定义课程元素时才需要考虑这一点。
3. 您正在使用自定义 `grade` 函数来修改问题的评分行为。在这种情况下,您需要确保 `test()` 函数生成适当的输入来测试您的自定义评分逻辑。
test() 函数接收 prepare() 的输出以及 test_type 参数。 test_type 是 correct、incorrect 或 invalid。您的函数应根据输入(例如 data["correct_answers"])和 test_type 生成 raw_submitted_answers。它还应该更新 score 和 feedback。