创建多项选择测验系统,第 3 部分

作者:Reuven M. Lerner

上个月,我们继续研究改进 CGI 测验引擎用户界面的方法。这个引擎基于 QuizQuestions.pm Perl 模块,允许我们创建许多不同的多项选择测验。每个测验都存储在服务器文件系统上的单独 ASCII 文本文件中,并且由于 QuizQuestions 对象提供的抽象层,我们能够忽略信息的存储方式,而专注于测验本身。

或者我们可以吗?正如我们上个月所看到的,使用 HTML 表单和 CGI 程序相对容易创建测验。实际上,我们毫不费力地创建了这样一个程序;然而,正如我在上个月专栏的结尾所指出的那样,我们的工作只完成了一半。使用 HTML 表单和 CGI 程序简化了创建测验文件的过程,并减少了出错的可能性。但是,如果有人想修改他已经创建的测验,他仍然必须理解我们的文件格式,并确保不破坏它。如果有一个单独的程序,允许用户创建和修改他们的测验,从而避免直接处理测验文件,那就太好了。

这样一个程序对于其无错误、用户友好的测验创建来说是可取的。我们使用文本文件来存储信息,并使用制表符或其他特殊字符分隔给定记录中的字段。如果我们能提供一个图形界面,限制用户可以输入的数据类型,那么问题就会减少。此外,测验编辑器将授权我们网站的设计者和制作人——在越来越多的网站中,他们与负责创建 CGI 程序和保持网络运行的技术人员是分开的。虽然你可能是一位经验丰富的系统管理员,知道制表符和空格的区别,并且在编辑 /etc/fstab 或 Makefile 之前不会退缩,但网站上大多数面向内容的人员都不是有经验的用户。如果我们能提供一个工具,在不需要学习 Emacs 的情况下创建和修改测验,不需要向我们提出问题,也不需要沮丧地抓头发,那么为什么不呢?

上个月,我们看了一个简单的程序 create-quizfile.pl,它从 HTML 表单中获取信息,并正确地将其格式化为测验文件。本月,我们将采用该测验创建器背后的基本思想,并研究如何编写测验编辑器。也就是说,我们的新程序将允许我们创建测验,就像之前的程序一样,但也将允许我们通过修改问题和答案的文本或从测验文件中删除现有问题来编辑测验。

从上面的描述来看,我们的基本操作单元似乎是测验文件中的一行文本,其中将包含一个问题。处理这些数据的最简单方法是将它们保存在字符串数组中,我们称之为 @lines。这意味着 $lines[0] 是测验文件中的第一行非注释、非空白行——换句话说,是第一组问题和答案。例如,要查找第五个问题的第三个答案,我们调用 $lines[4],使用 Perl 的“split”运算符跨制表符拆分它,并读取该数组的第四个元素。测验文件的每一行的格式为 问题 答案1 答案2 答案3 答案4 正确答案,每个字段与其相邻字段之间用制表符分隔。测验文件的每一行都可以用包含制表符的字符串或通过跨制表符拆分字符串创建的列表来表示。

幸运的是,由于我们迄今为止使用的 QuizQuestions 对象模块,我们不必过多考虑测验文件的格式。当我们创建 QuizQuestions 的新实例时,我们实际上创建了一个新的空白测验。要向该测验添加问题,我们使用 addQuestion 方法,该方法期望接收六个参数——毫不奇怪,这些参数与测验文件每一行上出现的参数相同。

为了创建一个全新的测验,我们的程序(名为 edit-quiz.pl)必须创建一个 HTML 表单,其中包含用户可以输入一个或多个问题和答案的元素。为了编辑旧的测验文件,edit-quizfile.pl 必须创建与磁盘上已存在的测验文件相对应的 QuizFile 实例。然后,它必须从该测验文件中读取问题,将每个问题转换为一组 HTML 表单元素。这让用户可以编辑问题和答案,删除现有问题并添加新问题。

但是等一下——如果 edit-quizfile.pl 要创建用户将用于编辑测验的 HTML 表单,那么哪个程序将获取此表单的内容并实际对其进行处理呢?毕竟,CGI 程序是单次操作:它们接收输入,执行一些处理并生成输出。似乎如果我们的程序生成一个 HTML 表单作为输出,它也不能接受来自该表单的输入并将其保存到磁盘。这里的秘密是 edit-quizfile.pl 可以通过期望被调用两次来接受自己的输入。在第一次调用时,它创建用于编辑测验的表单,在第二次调用时,它以测验的形式保存提交的数据。因此,我们的程序在两个不同的场合执行两个不同的操作。

为什么不简单地编写两个单独的程序,一个显示表单,第二个处理表单呢?我们当然可以这样处理,但这会将我们限制为单次迭代编辑。通过将所有测验编辑功能都保留在一个程序中,我们可以创建一个编辑循环,允许我们进行多次操作,而不仅仅是一次性处理。我们可以重用用于显示测验文件当前状态的代码,因为当我们的程序被调用时,我们总是显示测验文件的状态。通过将所有代码都放在一个程序中,并将显示代码放在这个程序的底部,一切都变得更容易理解和维护。

显示测验

在我们开始编写代码之前,让我们弄清楚这一切是如何工作的。我们以多种不同的方式调用 edit-quiz.pl,同时使用 GET(即,在浏览器窗口中输入其 URL 或单击 HTML 页面中的超链接)和 POST(即,当我们单击 HTML 表单底部的 submit 按钮时)。

如果使用不带任何参数的 GET 调用 edit-quiz.pl,我们会询问我们希望创建或编辑哪个测验。输入测验文件的名称并按回车键将其提交给程序,然后程序会收到此参数——再次使用 GET——在查询字符串中。

对于那些对 CGI 编程比较陌生的人来说,查询字符串是指 URL 中问号后面的任何内容。它允许将简单参数传递给 CGI 程序,而无需使用 POST,这是一种更复杂和精密的传递信息协议。因此,如果有人调用程序

http://www.fictional.edu/cgi-bin/program.pl

查询字符串为空,因为没有参数。但是如果有人调用 CGI 程序

http://www.fictional.edu/cgi-bin/program.pl?foobar
参数是 foobar。如果我们使用 CGI.pm,一个用于编写 CGI 程序的 Perl 模块(可从 CPAN 获取,网址为 https://perldotcom.perl5.cn/CPAN),我们可以理论上使用 query_string 方法检索查询字符串的内容,如下所示
my $query = new CGI;
my $query_string = $query->query_string;
出于我不太理解的原因,query_string 方法返回的查询字符串带有预先添加的 keywords=,就好像查询字符串已在名为 keywords 的 HTML 表单元素中提交给我们的 CGI 程序一样。虽然这有时会派上用场,但在大多数情况下,我发现它是这个原本优秀的软件包中令人惊讶的怪癖。

如果 CGI.pm 要将查询字符串视为名为 keywords 的参数,我们必须以与处理其他参数相同的方式检索其值,即

my $query = new CGI;
my $query_string = $query->param("keywords");

乍一看可能有点奇怪,但你会习惯的。

我们使用 CGI.pm 中的方法 request_method 确定我们的程序是通过 GET 还是 POST 调用的。换句话说,我们可以这样做

my $query = new CGI;
my $request_method = $query-7gt;request_method;

此时,变量 $request_method 包含字符串 GETPOST,具体取决于程序的调用方式。这两种调用方法之间的主要区别在于参数传递给程序的方式:GET 将所有变量值都放在查询字符串中发送,而 POST 则通过 stdin(与标准输入关联的文件句柄)发送它们。幸运的是,CGI.pm 使我们无需处理这些方法,并且无论它们的来源如何,都会无形地将参数交给我们。

在任何情况下,如果我们的程序在查询字符串中被调用了任何值,我们都会打印出一个 HTML 表单,其中包含该名称的测验文件的内容,或者一个空白 HTML 表单,允许用户创建该名称的测验。您可以在 清单 1 中看到该程序。

这个程序中有几点需要注意。首先,我们需要告诉程序每个测验可以包含的最大问题数。我们通过在程序顶部设置全局变量 $MAX_QUESTIONS 来做到这一点。通过更改此变量,可以允许非常短或非常长的测验。

此外,请注意我们如何设法创建一个 HTML 页面,该页面使用查询字符串中的参数调用我们的程序。我们使用了 <ISINDEX> 标签,该标签几乎已被 Web 遗忘,主要是因为它创建了一个丑陋的文本框,其说明很难或不可能更改,并且很少与手头的主题相关。尽管如此,如果您想为程序提供一种机制,将用户定义的参数馈送到自身,<ISINDEX> 还是很有用的。

此外,我们基于从查询字符串中的用户收到的测验名称创建 QuizQuestions 的新实例。创建 QuizQuestions 的实例后,我们指示它从磁盘加载其内容。当然,如果这是一个新测验,那么就没有什么可加载的,这在 loadFile 方法返回的错误消息中已注明。我们不关心打开文件时是否出现错误——如果测验文件存在,则内容会显示在 HTML 表单中,但如果它不存在,则将其视为新测验。

当然,完全忽略错误消息不是一个好主意。但是 loadFile 方法返回的错误消息相当原始,指示文件是否已成功加载。更好的错误消息可能会区分无法找到所讨论的文件、存在但无法读取的测验文件以及包含错误的测验文件。但就目前而言,这就是我们所拥有的全部,所以我们不得不忍受它。

我们通过将值放在变量中来插入每个 HTML 表单元素的当前值。Perl 的优点之一是未初始化的变量默认为空字符串 ("")。这意味着如果我们没有设置特定的问题或答案,事情就不会崩溃。相反,由于我们从变量中获得了空字符串,我们可以将变量粘贴到表单元素的 value 属性中,从而重置表单元素的值。

我们使用了一些技巧来指示选择列表(用于指示正确答案)的哪个元素应默认选中。这是代码

my $letter = "";
foreach $letter
("a","b","c","d")
{
   print "<option ";
   print "selected " if ($letter eq $correct);
   print "$letter>$letter\n";
}
print "</select>\n";

在此代码中,我们只是遍历所有四个可能的正确答案,在适当的 <option> 标签内插入单词 selected

如您所见,创建一个显示测验文件内容的程序并不困难。如果我们想创建一个编辑器,我们需要编写程序的后半部分,即获取提交的表单内容并将其保存到磁盘的部分。幸运的是,我们组织 HTML 表单的方式使这相当轻松。

编辑测验

当用户单击页面底部的 submit 按钮时,通过 POST 调用 edit-quiz.pl 中处理保存信息的部分。此时,HTML 表单元素被发送到 edit-quiz.pl,并使用 CGI.pm 中的 param 方法使其可用。

因此,当使用 POST 调用 edit-quiz.pl 时,我们只需要执行以下操作

  1. 创建 QuizQuestions 的新实例

  2. 遍历所有提交的 HTML 表单元素

  3. 将这些表单元素转换为新问题

  4. 保存 QuizQuestions

最简单的方法是循环遍历所有可能的元素。由于全局变量 $MAX_QUESTIONS,我们知道可能存在多少元素。因此,我们可以执行类似的操作

my $counter = 0;
foreach $counter (0 .. $MAX_QUESTIONS)
{
   # Add question number $counter
}

现在,通过使用 QuizQuestions 中的 addQuestion 方法来完成添加新问题。创建 QuizQuestions 的实例后,我们可以通过调用该方法来添加新问题,并将问题文本、可能的答案和正确答案作为参数传递给它。鉴于我们的 HTML 表单元素的名称是规则的,我们可以扩展上面的循环,如下所示

# Create an instance of QuizQuestions
my $questions = new QuizQuestions($quizname);
# Add questions to $questions
my $counter = 0;
foreach $counter (1 .. $MAX_QUESTIONS)
{
   # Only handle as many questions as were filled
   # in, by
   # checking to see if the question was entered
   last unless ($query->param("question-$counter")
         ne "$counter");
   # Set the question
   my @question =
                ($query->param("question-$counter"),
                $query->param("answer-a-$counter"),
                $query->param("answer-b-$counter"),
                $query->param("answer-c-$counter"),
                $query->param("answer-d-$counter"),
                $query->param("correct-$counter"));
   # Add the question to the quiz
   $questions->addQuestion(@question);
}
如果您看过上个月的专栏,上面的循环应该看起来很熟悉。这是因为该循环是从 create-quiz.pl 中提取的,我更改了变量名并修改了“last”语句的条件。这确保了如果测验问题的文本与测验编号相同,循环将退出,因为在我们的编辑器中,每个测验问题都设置为其编号。

现在您已经了解了 edit-quiz.pl 的核心工作原理,请查看 清单 2 中的整个程序。如您所见,进行此程序工作不需要太多修改。这是因为大部分工作都由已经创建的对象完成。CGI.pm 负责读取 HTML 表单元素并以良好的打包格式将其交给我们,而 QuizQuestions.pm 负责从测验文件中加载和保存问题。现在我们不必弄脏我们的手了。(我们的用户也不必,他们现在可以创建测验,而无需担心格式问题。)

我还对 edit-quiz.pl 的原始片段进行了一些重新调整,这样我们总是能看到我们编辑工作的成果,并且可以对测验进行其他更改。这比标准的 Web 界面要好一些,在标准的 Web 界面中,保存也意味着退出。在这里,没有任何真正的 exit 按钮,因为保存会将您带回到您之前的位置。

当前版本的这些程序存在一些小问题。特别是,用户在其中一个文本字段中输入一对引号 ("") 可能会很危险。浏览器可能难以确定哪些引号属于字符串,哪些引号设置字符串的边界。(但是,令我惊讶的是,我 Linux 机器上的 Netscape 3.0 似乎可以很好地处理这个问题。)

虽然我们大部分时间都忘记了它,但我们仍然被我们的老朋友制表符所困扰,它仍然分隔着我们测验文件中的字段。如果用户在我们的测验编辑器中的一个文本字段中输入制表符,则测验文件的格式将被损坏并会导致问题。解决这个问题的方法是遍历 CGI.pm 交给我们的每个 HTML 表单元素,并删除任何制表符。更好的是,我们可以用空格替换它们。

注释

下个月

下个月,我们将借助 Text::Template 模块的魔力,研究您可以使用 HTML 和 Perl 创建的混合模板。通过将程序嵌入到 HTML 文件中,我们可以同时包含静态 HTML 和编程元素。当您想使用程序生成输出,但又不想将 HTML 样式锁定在程序内部时,这可能是一个很大的优势,因为您网站的编辑和设计人员最终会想要更改程序输出的内容和样式。

Reuven M. Lerner 是一位居住在以色列海法的互联网和 Web 顾问,自 1993 年初以来一直使用 Web。在他的业余时间,他做饭、阅读并为他社区的教育项目做志愿者。您可以通过 reuven@netvision.net.il 与他联系。

加载 Disqus 评论