Yesod - Extract session data (non-String), store it and use it - haskell

Hi there.
Here is the code I'm trying to make work :
getGameR :: Handler Html
getGameR = do
sess <- getSession
defaultLayout $ do
setTitle "Game"
$(widgetFile "hamletFile")
where
person = read $ fromJust $ Map.lookup "person" sess :: Person
data Person = Person
{
name :: Text
}
deriving (Show, Read)
The error is the following:
Handler/MyHandler.hs:87:56: Not in scope: `sess'
What I'm trying to do, is to extract data from Yesod Session (data of type Person) and store it inside 'person', to be able to use it inside the hamlet file.
Is there a way to get around that error?
If it's not possible, can you suggest another way around?
Thanks in advance.

sess is local to the do block, and thus it is not in scope in the person definition. As far as that error goes, using let inside the do block should be enough:
getGameR :: Handler Html
getGameR = do
sess <- getSession
let person = read $ fromJust $ Map.lookup "person" sess :: Person
defaultLayout $ do
setTitle "Game"
$(widgetFile "hamletFile")

If you just want to lookup single value, consider using lookupSession instead. Also, fromJust throws exception if key is not in session, you might use fromMaybe:
getGameR :: Handler Html
getGameR = do
mbPersonName <- lookupSession "person"
let defaultPerson = Person "anonymous"
let person = fromMaybe defaultPerson (readMaybe .unpack =<< mbPersonName) :: Person
defaultLayout $ do
setTitle "Game"
$(widgetFile "hamletFile")
Here is my helpers for dealing with session:
import Text.Read (readMaybe)
import Data.Maybe (fromMaybe)
readSession :: Read a => Text -> Handler (Maybe a)
readSession name = do
textValue <- lookupSession name
return (readMaybe . unpack =<< textValue)
readSessionDef :: Read a => a -> Text -> Handler a
readSessionDef def name = do
mbValue <- readSession name
return $ fromMaybe def mbValue
readSession reads anything that can be readden and returns a Maybe. readSessionDef returns default value if such key is not present in session:
getGameR :: Handler Html
getGameR = do
person <- readSessionDef (Person "anonymous") "person"
defaultLayout $ do
setTitle "Game"
$(widgetFile "hamletFile")

Related

How to parse yesod-form parameters into Haskell values

The code below is from the Home.hs file created by the yesod-simple scaffold.
I like to do simple string manipulation on text input but don't know how to parse it into a Text value.
How, for example, can I use toUpper on fileDescription?
I've tried using lookupPostParam
but I'm struggling with it's type signature:
lookupPostParam :: MonadHandler m => Text -> m (Maybe Text)
Home.hs
module Handler.Home where
import Import
import Yesod.Form.Bootstrap3 (BootstrapFormLayout (..), renderBootstrap3)
import Text.Julius (RawJS (..))
data FileForm = FileForm
{ fileInfo :: FileInfo
, fileDescription :: Text
}
getHomeR :: Handler Html
getHomeR = do
(formWidget, formEnctype) <- generateFormPost sampleForm
let submission = Nothing :: Maybe FileForm
handlerName = "getHomeR" :: Text
defaultLayout $ do
let (commentFormId, commentTextareaId, commentListId) = commentIds
aDomId <- newIdent
setTitle "Welcome To Yesod!"
$(widgetFile "homepage")
postHomeR :: Handler Html
postHomeR = do
((result, formWidget), formEnctype) <- runFormPost sampleForm
let handlerName = "postHomeR" :: Text
submission = case result of
FormSuccess res -> Just res
_ -> Nothing
defaultLayout $ do
let (commentFormId, commentTextareaId, commentListId) = commentIds
aDomId <- newIdent
setTitle "Welcome To Yesod!"
$(widgetFile "homepage")
sampleForm :: Form FileForm
sampleForm = renderBootstrap3 BootstrapBasicForm $ FileForm
<$> fileAFormReq "Choose a file"
<*> areq textField textSettings Nothing
where textSettings = FieldSettings
{ fsLabel = "What's on the file?"
, fsTooltip = Nothing
, fsId = Nothing
, fsName = Nothing
, fsAttrs =
[ ("class", "form-control")
, ("placeholder", "File description")
]
}
commentIds :: (Text, Text, Text)
commentIds = ("js-commentForm", "js-createCommentTextarea", "js-
commentList")
This is unfortunately a fault in documentation and communication.
Given
lookupPostParam :: (MonadResource m, MonadHandler m) => Text -> m (Maybe Text)
the reader is meant to infer that m is not only a MonadResouce and a MonadHandler but also Monad. This tiny little line of code packs up a lot of intent into a very small sentence; it's a wart that so much of Haskell library usage is left implicit and subtextual. For example, to call toUpper on the Text inside this type you are meant to do this:
{-# language OverloadedStrings #-}
foo :: (MonadResource m, MonadHandler m) => m (Maybe Text)
foo = do
valueMaybe <- lookupPostParam "key"
case valueMaybe of
Just value ->
pure (toUpper value)
Nothing ->
Nothing
Note that the monad stack (MonadHandler, MonadResource) has "infected" your code. This is meant to be intentional, so as to constrain you via the type checker to only run this function in the intended Yesod environment/state machine/context/whatever.
However
You are using yesod-forms and it would be nice to do the same thing within that framework. As with lookupPostParam, we can take advantage of the monad-applicative-functor typeclasses.
We can adapt this to the Form FileForm value that you have.
sampleForm :: AForm Handler FileForm
sampleForm =
FileForm <$> fileAFormReq "Choose a file"
<*> (toUpper <$> areq textField textSettings Nothing)
I think the types of yesod-forms changed between releases. I'm copying my types off the latest version as of writing, 1.4.11.
Here we take advantage of the Monad m => Functor (AForm m) instance. Knowing that we are indeed in a monad (the Handler monad) means we can use fmap and its infixed sibling <$> on the value returned by areq textField textSettings Nothing. This allows us to lift arbitrary functions acting on Text into the AForm m stack. For example, here we went from Text -> Text to AForm Handler Text -> AForm Handler Text.
Hope that helps.

Persisting an uploaded file in the database (Couldn't match type ‘ConduitM () ByteString (ResourceT IO) ()’ with ‘HandlerT App IO ByteString’)

Using yesod I am trying to persist an uploaded file in the database.
To do this, I want to extract the bytestring and persist it, but I still need the code to typecheck.
The actual problem is
Couldn't match type ‘ConduitM () ByteString (ResourceT IO) ()’
with ‘HandlerT App IO ByteString’
Expected type: HandlerT App IO ByteString
Actual type: Source (ResourceT IO) ByteString
In a stmt of a 'do' block: file <- (fileSourceRaw fileinfo)
In the expression:
do { setMessageI $ MsgUploadedImg;
uuidWrapped <- liftIO (U4.nextRandom);
let uuid = fromString $ U.toString $ uuidWrapped;
transactionId <- runDB $ insert $ Transaction userId;
.... }
The relevant part is this function (see: file <- (fileSourceRaw fileinfo)).
getImgR :: Handler Html
getImgR = do
oldImages <- runDB $ selectList [] []
mauthId <- maybeAuthId
((res, widget), enctype) <- runFormPost $ form Nothing Nothing ""
case (mauthId,res) of
(Just userId, FormSuccess (title,fileinfo)) -> do
transactionId <- runDB $ insert $ Transaction userId
file <- (fileSourceRaw fileinfo)
let newImg = Img {imgFile = Just file, imgTitle = title , imgDesc = Nothing, imgFiletype = Nothing, imgTransactionId = Just transactionId, imgUserId = Just userId}
_ <- runDB $ insert newImg
redirect $ ImgR
_ -> defaultLayout
[whamlet|
$if null oldImages
<p>There are no images
$else
<ul>
$forall Entity imgId img <- oldImages
<li>
<p>#{imgTitle img}
$if mauthId == Nothing
<form method=post action=#{ImgR} enctype=#{enctype}>
<input type=submit value=_{MsgPleaseLogin}>
$else
<form method=post action=#{ImgR} enctype=#{enctype}>
^{widget}
<input type=submit>
|]
Helper code:
type MyForm = Html -> MForm Handler (FormResult (Text, FileInfo), Widget)
form :: Maybe UserId -> Maybe TransactionId -> Text -> MyForm
form userId transactionId uuid = renderBootstrap $ (,)
<$> areq textField "Title" Nothing
<*> fileAFormReq "Image file"
Img
title Text
filetype Text Maybe
desc Textarea Maybe
file ByteString Maybe
transactionId TransactionId Maybe
userId UserId Maybe
deriving Show
Still going over the documentation, but I thought the use case was common enough to ask a question. The datatype of FileInfo is:
data FileInfo = FileInfo
{ fileName :: !Text
, fileContentType :: !Text
, fileSourceRaw :: !(Source (ResourceT IO) ByteString)
, fileMove :: !(FilePath -> IO ())
}
Thank you for your attention.
Edit: I assume the solution is contained in the documentation here
http://www.yesodweb.com/blog/2013/03/simpler-streaming-responses?
Update:
It looks like one of those links
How can I post FileInfo to a web service using Yesod and Http-Conduit?
https://www.schoolofhaskell.com/school/to-infinity-and-beyond/competition-winners/part-5
Yesod handlers, content of POSTed files
contain the solution.
Update2:
Using (Data.Conduit.Binary is DCB)
file <- runResourceT $ fileSource fileinfo $$ DCB.sinkLbs
leaves me with
Handler/Img.hs:62:42:
Couldn't match expected type ‘ByteString’
with actual type ‘Data.ByteString.Lazy.Internal.ByteString’
NB: ‘ByteString’ is defined in ‘Data.ByteString.Internal’
‘Data.ByteString.Lazy.Internal.ByteString’
is defined in ‘Data.ByteString.Lazy.Internal’
In the first argument of ‘Just’, namely ‘file’
In the ‘imgFile’ field of a record
Looks like the missing function is here:
Many types of String (ByteString)
The final code is
module Handler.Img where
import Import
import LambdaCms.Core -- for UserId
import Database.Persist.Sql (toSqlKey)
-- for uuids
import qualified Data.UUID as U
import qualified Data.UUID.V4 as U4
import Yesod.Core.Types
import qualified Data.Conduit.List as CL
import qualified Data.Conduit.Binary as DCB
import Data.ByteString as BS
import Data.ByteString.Lazy as LBS
type MyForm = Html -> MForm Handler (FormResult (Text, FileInfo), Widget)
form :: Maybe UserId -> Maybe TransactionId -> Text -> MyForm
form userId transactionId uuid = renderBootstrap $ (,)
<$> areq textField "Title" Nothing
<*> fileAFormReq "Image file"
lazyToStrictBS :: LBS.ByteString -> BS.ByteString
lazyToStrictBS x = BS.concat $ LBS.toChunks x
getImgR :: Handler Html
getImgR = do
oldImages <- runDB $ selectList [] []
mauthId <- maybeAuthId
((res, widget), enctype) <- runFormPost $ form Nothing Nothing ""
case (mauthId,res) of
(Just userId, FormSuccess (title,fileinfo)) -> do
setMessageI $ MsgUploadedImg
transactionId <- runDB $ insert $ Transaction userId
file <- runResourceT $ fileSource fileinfo $$ DCB.sinkLbs
let newImg = Img {imgFile = Just (lazyToStrictBS file), imgTitle = title , imgDesc = Nothing, imgFiletype = Nothing, imgTransactionId = Just transactionId, imgUserId = Just userId}
_ <- runDB $ insert newImg
redirect $ ImgR
_ -> defaultLayout
[whamlet|
$if Import.null oldImages
<p>There are no images
$else
<ul>
$forall Entity imgId img <- oldImages
<li>
<p>#{imgTitle img}
$if mauthId == Nothing
<form method=post action=#{ImgR} enctype=#{enctype}>
<input type=submit value=_{MsgPleaseLogin}>
$else
<form method=post action=#{ImgR} enctype=#{enctype}>
^{widget}
<input type=submit>
|]
postImgR :: Handler Html
postImgR = getImgR

Lookup query parameters in Yesod

I just initialized a Yesod project (no database) using yesod init.
My HomeR GET handler looks like this:
getHomeR :: Handler Html
getHomeR = do
(formWidget, formEnctype) <- generateFormPost sampleForm
let submission = Nothing :: Maybe (FileInfo, Text)
handlerName = "getHomeR" :: Text
defaultLayout $ do
aDomId <- newIdent
setTitle "Welcome To Yesod!"
$(widgetFile "homepage")
When using yesod devel, I can access the default homepage at http://localhost:3000/.
How can I modify the handler listed above to retrieve (and display) a HTTP GET query parameter like id=abc123 when accessing this URL:
http://localhost:3000/?id=abc123
Note: This question was answered Q&A-style and therefore intentionally doesn't show research effort!
I'll show two different methods to achieve this. For both, you'll need to add this code to your template, e.g. in homepage.hamlet:
Note that it is not guaranteed there is any id parameter present when accessing the URL, therefore the type resulting from both methods is Maybe Text. See the Shakespearean template docs for a detailed explanation of the template parameters.
Method 1: lookupGetParam
The easiest way you can do this is using lookupGetParam like this:
idValueMaybe <- lookupGetParam "id"
When using the default setting as generated by yesod init, idValueMaybe needs to be defined in both getHomeR and postHomeR if idValueMaybe is used in the template.
Your HomeR GET handler could look like this:
getHomeR :: Handler Html
getHomeR = do
idValueMaybe <- lookupGetParam "id"
(formWidget, formEnctype) <- generateFormPost sampleForm
let submission = Nothing :: Maybe (FileInfo, Text)
handlerName = "getHomeR" :: Text
defaultLayout $ do
aDomId <- newIdent
setTitle "Welcome To Yesod!"
$(widgetFile "homepage")
Method 2: reqGetParams
Instead of looking up the query parameters by name, you can also retrieve a list of query key/value pairs using reqGetParams. This can be advantageous in certain situations, e.g. if you don't know all possible keys in advance. Using the standard lookup function you can easily lookup a certain key in that list.
The relevant part of your code could look like this:
getParameters <- reqGetParams <$> getRequest
let idValueMaybe = lookup "id" getParameters :: Maybe Text
Your getHomeR could look like this:
getHomeR :: Handler Html
getHomeR = do
getParameters <- reqGetParams <$> getRequest
let idValueMaybe = lookup "id" getParameters :: Maybe Text
(formWidget, formEnctype) <- generateFormPost sampleForm
let submission = Nothing :: Maybe (FileInfo, Text)
handlerName = "getHomeR" :: Text
defaultLayout $ do
aDomId <- newIdent
setTitle "Welcome To Yesod!"
$(widgetFile "homepage")

Extracting database field values inside a Handler

I would like to extract a database field (Text) and pass it as an argument to another function from a Handler. However, I run into Type errors. Completely made up example so may feel a bit contrived but should illustrate the issue I am having.
Person
name Text
Car
personId PersonId
name Text
type Text
I would like to get a Car entity and then find the corresponding Person. Get his name and then pass it as an argument. Something like:
data MyD = MyD { input1 :: Int}
entryForm :: Text -> Form MyD -- Update: Removed the incorrect extra parameter
entryForm n1 = renderDivs $ MyD
<$> areq intField n1 Nothing
My get handler looks like:
getInputR :: CarId -> Handler Html
getInputR carId = do
car <- runDB $ get404 carId
pid <- carPersonId car
name <- getPName pid
(widget, enctype) <- generateFormPost $ entryForm name
defaultLayout $ do
$(widgetFile "my_template")
where
getPName pid = do
person <- runDB $ get404 pid
personName person
I get an error saying:
Couldn't match expected type `HandlerT App IO t0'
with actual type `KeyBackend
persistent-1.2.1:Database.Persist.Sql.Types.SqlBackend Person'
In the return type of a call of `carPersonId'
In a stmt of a 'do' block: pid <- carPersonId car
What am I doing wrong?
Thanks!
Try changing
pid <- carPersonId car
name <- getPName pid
to
name <- getPName $ carPersonId car
The value returned from your runDB call is not inside the handler monad so you don't need to use the arrow syntax to access it.
For the second error, the issue is similar: The getPName function's return type is in the Handler monad since it uses runDB, so you need to use return to put the value into the monad:
getPName pid = do
person <- runDB $ get404 pid
return $ personName person

How to render Html algebraic data type in Yesod

I'm trying to use CKEditor in my Yesod application. Data from CKEditor is returned to the server via Textarea, I then store it as Html in database. My problem is I do know know how to display the Html algebraic data type once I retrieve it from database in the handler. I've been reading this tutorial, but it will only display the Html as a big long string, not as markup.
Note: titleA and contextA are the variable that I want to display in article-local-display.
contextA is the Html algebraic data type
PS: Do I need to transform Html to hamlet in order to render?
module Handler.Article where
import Import
import Data.Text (unpack)
import Data.Time (getCurrentTime)
import Data.String (fromString)
getArticleR :: Handler RepHtml
getArticleR = do
defaultLayout $ do
setTitle "Search For Article"
$(widgetFile "header")
$(widgetFile "article")
postArticleR :: Handler RepHtml
postArticleR = do
redirect ArticleR
getArticleLocalR :: Handler RepHtml
getArticleLocalR = do
articles <- runDB $ selectList ([] :: [Filter Article]) [Desc ArticleTime]
defaultLayout $ do
setTitle "Local Article"
$(widgetFile "header")
$(widgetFile "article-local")
getArticleLocalDisplayR :: ArticleId -> Handler RepHtml
getArticleLocalDisplayR articleId = do
article <- runDB $ get404 articleId
let titleA = articleTitle article
contextA = articleContext article
defaultLayout $ do
setTitle "Article"
$(widgetFile "header")
$(widgetFile "article-local-display")
getArticleLocalCreateR :: Handler RepHtml
getArticleLocalCreateR = do
defaultLayout $ do
setTitle "Create article"
addScript $ StaticR ckeditor_ckeditor_js
$(widgetFile "header")
$(widgetFile "article-local-create")
postArticleLocalCreateR :: Handler RepHtml
postArticleLocalCreateR = do
articleForm <- runInputPost $ ArticleForm <$> ireq textField "title" <*> ireq textareaField "editor1"
now <- liftIO getCurrentTime
let titleA = title articleForm
html = toHtml $ unTextarea $ context articleForm
_ <- runDB $ insert $ Article titleA html now
redirect ArticleLocalR
data ArticleForm = ArticleForm {
title :: Text,
context :: Textarea
}
deriving Show
models file:
Article
title Text
context Html
time UTCTime
deriving
article-local-display.hamlet
<h1>#{titleA}
<article>#{contexA}
So I changed the context from Html to Text.
Article
title Text
context Text
time UTCTime
deriving
Then added preEscapedText when using the value.
let contextA = preEscapedText $ articleContext article
Now it's displaying properly.

Resources