Wednesday, February 7, 2018

Saving a POSTed form (6)

In the last post, I got as far as an HTML form you can edit in a browser and submit to a servant web server which promptly fails.

In this post, I'm going to implement the POST side of this, which needs to handle saving the result back to the database, if the input is valid, and go back to the user if not.

t

POSTs are handled by this rather poor implementation at the moment:

handleRegistrationPost = error "We'll implement this later"

First, give it a type signature. The API definition for this end point is:

 "registration" :> S.Capture "id" Integer :> S.ReqBody '[S.FormUrlEncoded] [(String,String)] :> S.Post '[HTML] B.Html

... so handleRegistrationPost will take two parameters: an Integer from the Capture (as with handleRegistration, and the form body as [(String,String)] key-value pairs.

So add on this type signature to the existing implementation (and check it still typechecks, if you want):

handleRegistrationPost :: Integer -> [(String, String)] -> S.Handler B.Html

First, load the identified registration from the database, as before: (we need this for constructing the DF.Form which always seems a bit awkward to me)

handleRegistrationPost identifier reqBody = do
  [registration] <- liftIO $ bracket
    (PG.connectPostgreSQL "user='postgres'")
    PG.close
    $ \conn -> do
      PG.query
        conn
        "SELECT firstname, lastname, dob FROM registration WHERE id = ?"
        [identifier] :: IO [Registration]

(If you're feeling fancy, factor this out with handleRegistration at this point)

We've been sent some stuff from the user's browser, containing new values for form fields. If we're lucky, those will all be valid and we can apply them to the DF.Form to get a Registration value. If we're not so lucky, the values won't be valid. However, we can still apply them to the form and represent them in a DF.View (like in handleRegistration).

  viewValue <- DF.postForm "Registration" (registrationDigestiveForm registration) (servantPathEnv reqBody)

That servantPathEnv is a helper to munge the form parameters into something digestive-functors is happy with. The code is later on in the post.

This viewValue has two cases: the form is invalid, or the form is valid; I'll deal with the invalid case first, because it is most like handleRegistration. All I've done here is change the h1 heading text. Other than that, we generate an HTML version of the view and send it back to the user.

  case viewValue of
    (view, Nothing) ->
      return $ B.docTypeHtml $ do
        B.body $ do
          B.h1 $ do "Registration (there were errors): "
                    B.toHtml (show identifier)
          htmlForRegistration view

The other case occurs when the form *does* contain enough to create a valid Registration. In this case, it should be written out to the database over the top of the existing registration:

    (_, Just newRegistration) -> do
      liftIO $ bracket
        (PG.connectPostgreSQL "user='postgres'")
        PG.close
        $ \conn -> PG.execute conn
                   "UPDATE registration SET firstname = ?, lastname = ?, dob = ? WHERE id = ?"
                   (firstname newRegistration,
                    lastname newRegistration,
                    dob newRegistration,
                    identifier
                   )
      return "Record updated."

We also need that helper servantPathEnv that I'll just paste in here without much explanation. It munges the form parameters from [(String, String)] into a form that digestive-functors likes.

Add text as a dependency in package.yaml, and then:

import qualified Data.Maybe as M
import qualified Data.Text as T
...
servantPathEnv :: Monad m => [(String, String)] -> DF.FormEncType -> m (DF.Env m)
servantPathEnv reqBody _ = return env
  where
      pathAsString = T.unpack . DF.fromPath
      packAsInput = DF.TextInput . T.pack
      lookupParam p = lookup (pathAsString p) reqBody
      env path = return (packAsInput <$> (M.maybeToList (lookupParam path)))

So now you should have a form you can update. Edit the names. Save. Go to another computer, and observe that the form has your changes.

You might wonder, though, how we ever hit that code path that comes back to you when your input is invalid. That's not really going to happen the way things are set up now, because this form will accept pretty much anything. Next time, I'll implement some validation rules to give some restrictions on what you can put into a form.

Commit dbe32fea contains the changes for this post.

No comments:

Post a Comment