7 Web Programming

7.7 Example: A Web Questionnaire

In many web applications, clients want to store or update information on the server, e.g., by putting orders for books, flight tickets, etc. In this section we show an example where a client can change persistent data stored on the web server.

A web questionnaire
Figure 7.2: A web questionnaire

Consider the implementation of a web-based questionnaire which allows the clients to vote on a particular topic. Figure 7.2 shows an example of such a questionnaire. The votes are stored on the web server. The current votings are shown after a client submits a vote (see Figure 7.3).

Answer to the web questionnaire
Figure 7.3: Answer to the web questionnaire

In order to provide an implementation that is easy to maintain, we define the main question and the choices for the answers as constants in our program so that they can be easily adapted to other questionnaires:

question :: String
question = "Who is your favorite actress?"
choices :: [String]
choices = ["Doris Day", "Jodie Foster", "Marilyn Monroe",
           "Julia Roberts", "Sharon Stone", "Meryl Streep"]

The current votes are stored in a file on the web server. We define the name of this file as a constant in our program:

voteFile :: String
voteFile = "votes.data"

For the sake of simplicity, this file is a simple text file. If there are n choices for voting, the file has n lines where each line contains the textual representation of the number of votes for the corresponding choice. Thus, the following operation reads the vote file and returns the list of numbers in this file or, if the file does not exist, initializes the vote file and returns zeros (the prelude function lines breaks a string into a list of lines, where lines are separated by newline characters, and the opposite function unlines concatenates a list of strings into lines).

readVoteFile :: IO [Int]
readVoteFile = do
  existnumfile <- doesFileExist voteFile
  if existnumfile
    then do vfcont <- readFile voteFile
            return (map read (lines vfcont))
    else do let nums = take (length choices) (repeat 0)
            writeFile voteFile (unlines (map show nums))
            return nums

To update the vote file, we define overwriteVoteFile that writes a list of numbers into the vote file. The numbers are written into a new file that is moved to the vote file in order to avoid an overlapping between reading and writing the same file. doesFileExist, removeFile, and renameFile are I/O operations defined in the library System.Directory (from package directory) to check the existence of a file, delete a file, and rename a file, respectively.

overwriteVoteFile :: [Int] -> IO ()
overwriteVoteFile nums = do
  writeFile (voteFile ++ ".new") (unlines (map show nums))
  removeFile voteFile
  renameFile (voteFile ++ ".new") voteFile

When a client submits a vote, we have to increment the corresponding number in the vote file. This can be easily done by reading the current votes and writing the votes that are incremented by the auxiliary function incNth:

incNumberInFile :: Int -> IO ()
incNumberInFile voteindex = do
  nums <- readVoteFile
  overwriteVoteFile (incNth nums voteindex)
 where
  incNth []     _             = []
  incNth (x:xs) n | n==0      = (x+1) : xs
                  | otherwise = x : incNth xs (n-1)

Now we have all auxiliary definitions that are necessary to define the web script. First, we show the definition of the HTML page “evalPage” that shows the current votes (which produces the result shown in Figure 7.3). The prelude function “zip” joins two lists into one list of pairs of corresponding elements.

evalPage :: IO HtmlPage
evalPage = do
  votes <- exclusiveIO (voteFile ++ ".lock") readVoteFile
  return $ form "Evaluation"
   [h1 [htxt "Current votes:"],
    table (map (\(s,v) -> [[htxt s], [htxt $ show v]])
               (zip choices votes))]

Note that we ensure the exclusive access to the vote file by the use of the operation exclusiveIO.55 5 The operation exclusiveIO is defined in the library System.IOExts contained in the package io-extra. This is necessary since there is not much control on the access to web pages by clients. In particular, the same CGI program might be executed in parallel if two clients accessing them simultaneously. This can cause problems if both read and update the same information. Thus, it is mandatory to ensure exlusive data access in web applications which might change data. If the data is stored in files, it must be manually done, as above. If the data is stored in databases, the database system ensures the exclusive access but one has to take care on the definition of transations.

Now we can define our form that allows the user to submit a vote (see Figure 7.2). It uses radio buttons as input elements. Radio buttons are lists of buttons where exactly one button can be turned on. Thus, all buttons have the same HTML reference but different values. When a form is submitted, the HTML environment maps the HTML reference to the value of the selected radio button. A complete radio button suite consists always of a main button (radioMain) which is initially on and some further buttons with the same HTML reference as the main button (radioOthers) that are initially off. In our example, we associate to each button the index of the corresponding choice as a value. The event handler questHandler increments he appropriate vote number and returns the current votes by the use of evalPage [Browse Program][Download Program]:

questForm :: HtmlFormDef ()
questForm = simpleFormDef $
  [h1 [htxt question],
   radioMain vref "0", htxt (head choices), breakline] ++
   concatMap (\(i,s) -> [radioOther vref (show i), htxt s, breakline])
             (zip [1..] (tail choices)) ++
   [hrule, button "submit" questHandler]
 where
   vref free
   questHandler env = do
     exclusiveIO (voteFile ++ ".lock")
                 (incNumberInFile (read (env vref)))
     evalPage
main :: IO HtmlPage
main = return $ page "Vote Form" [formElem questForm]