A Stateless Quiz System
The Program

by Arne Sommer

A Stateless Quiz System with Raku - Part 3: The Program

[250.3] Published 18. August 2023

[ Index | Introduction | Security | The Program | Notes | RakuConf ]

The application (or program, if you are old school), is surprisingly short.

The first part takes care of the CSV file.

File: app (part 1)
#! /usr/bin/env raku

use Cro::HTTP::Router;                                   # [1]
use Cro::HTTP::Server;                                   # [2]

my %q;                                                   # [3]
my $number = 0;                                          # [4]
my $prev_id;                                             # [5]

for "sample-quiz.csv".IO.lines -> $row                   # [6]
{
  next unless $row;                                      # [7]

  $number++;                                             # [8]

  my ($id, $pre, $question, $answer) = $row.split(";");  # [9]

  %q{$prev_id}<next> = $id if $prev_id;                  # [10]

  %q{$id} =                                              # [11]
  {
    id       => $id,
    number   => $number,
    pre      => $pre,
    question => $question,
    answer   => $answer,
  };
      
  $prev_id = $id;                                         # [12]
}

[1] This module is used to set up routes; see part 2.

[2] This module is the application server itself; see part 3.

[3] The questions will end up in this two-dimentional hash. Supply a legal question ID, and you will get a hash as the one in [11] (with the addition of «next»).

[4] The current question number (running from 1 and upwards).

[5] The previous question ID, so that we can set up the pointer to the next question (or rather; from the previous one, to the current one) in [10].

[6] Iterate over the rows in the CSV file.

[7] Skip empty rows.

[8] Increase the question number counter.

[9] Get the entries from the CSV row.

[10] Set up the reference to the next question, if given, for the previous one.

[11] Set up the hash with the values for the current question. Note that the «id» field is not really useful, as we already have it - and have to use it to get to this hash in the first place.

[12] See [5] and [10].

The second part takes care of the routes, i.e. web pages. They ate also called request handlers.

See cro.services/docs/reference/cro-http-router for more information about «Cro:HTTP::Router».

File: app (part 2)
my $application = route
{
  get -> 'q', $id                                      # [13]
  {
    content 'text/html', q-page($id, "");
  }
  get -> 'q1', $id                                     # [14]
  {
    content 'text/html', q-page($id, "ok");
  }
  get -> 'q0', $id                                     # [15]
  {
    content 'text/html', q-page($id, "err");
  }
  post -> 'a'                                          # [16]
  {
    request-body -> (:$id, :$answer, *%rest)           # [17]
    {
      if $answer.lc eq %q{$id}<answer>.lc              # [18]
      {
        redirect :see-other, "/q1/{ %q{$id}<next> }";  # [18a]
      }
      else
      {
        redirect :see-other, "/q0/{ $id }";            # [19]
      }
    }
  }
}

[13] The handler for the initial (first) question. We delegate the content creation to the «q-page» procedure, which will be presented in part 4.

[14] The same as the «q» handler, except that it additionally will show an «ok» message (the green box). This is used to show that the user got the answer to the previous question right

[15] Also the same as the «q» handler, except that it additionally will show an «error» message (the red box). This is used to explain to the user why they are presented with the same question as last time.

[16] The «a» handler takes care of processing the answer. This is a post handler, as the answer is posted (i.e. not available in the URL).

[17] Get hold of the request body; i.e. the Question ID and the answer given by the user.

[18] Is the answer correct? If so, redirect to the next question, with the «ok» message [18a]. Note that we coerce both the user supplied answer and the correct answer to lowercase, before the comparison. Thus «Raku» will be accepted, even if we specified «raku» in the CSV file.

[19] Wrong answer; redirect to the current question, with the «error» message.

The third part sets up the application server (or service, as it is called in Cro lingo).

See cro.services/docs/reference/cro-http-service for more information about «Cro:HTTP::Service».

File: app (part 3)
my Cro::Service $hello = Cro::HTTP::Server.new:
  :host<localhost>,
  :port<8080>,
  :$application;

$hello.start;

react whenever signal(SIGINT) { $hello.stop; exit; }
File: app (part 4)
sub q-page ($id, $status)             # [20]
{
  return "Error 418" unless %q{$id};  # [21]

  return qq:to/END/;                  # [22]
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Sample Quiz</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
  </head>
  <body>
    <div class="container">
    { if $status eq "ok"  { "<div class='card'><div class='card-body bg-success text-white'>Correct answerr.</div></div>" } }
    { if $status eq "err" { "<div class='card'><div class='card-body bg-danger text-white'>Wrong answer. Please try again.</div></div>" } }
    { %q{$id}<pre> || ""}
    { %q{$id}<number> ?? "<h2>Question " ~ %q{$id}<number> ~ "</h2>" ~ %q{$id}<question> ~ "<br/><br/>" !! "" }
    { %q{$id}<next> ??
      "<form action='/a' method='post'>
        Answer:
        <input name='answer' type='text'>
        <input name='id' value='" ~ %q{$id}<id> ~ "' type='hidden'>
        <input type='submit' value='Send in'>
      </form>" !! ""
      }
    </div>
  </body>
</html>
END
}

[20] The procedure producing the html.

[21] If the user doctors the URL so that we get an unknown question ID, say so. Note that I have have chosen not to return an HTTP error code, as that would make life easier for a user looking for legal IDs.

[22] We got a legal question ID. Note the use of externally hosted Bootstrap for a very simplistic graphical design. Also note how we handle the $status variable, resulting in either nothing, a green or a red box - by the use of standard Raku embedding «code in string» with curly brackets ({ and }).

Note that we could have used a templating system, but that would have added complexity. Raku strings are flexible enough to make this hard coded approach work.

[ Index | Introduction | Security | The Program | Notes | RakuConf ]