How is it used translated messages inside a Haskell file? - haskell

I have searched in internet, in the Yesod Web ebook and other tutorials (Yesod Tutorial) but I have not been able to clarify this problem. I am using the scaffolded site.
I have a handler, inside it returns a value, the email if the user is authenticated or a string if he is not. What I want is to return the localized message instead the string "(Unknown User ID)". My problem is to use a value from the message file (ex. MsgHello), if I do this, it returns errors like:
Couldn't match expected type AppMessage' with actual typeText'
I have tried using (show MsgHello) or (pack MsgHello), even calling msg <- getMessageRender but I have not been able to do what I expect. If you have any suggestions, they are welcome.
Thanks!!
PD: This is part of the code that I am working on, line :
getUserProfileR :: Handler RepHtml
getUserProfileR = do
maid <- maybeAuth
let user = case maid of
Nothing -> "(Unknown User ID)"
Just (Entity _ u) -> userEmail u
defaultLayout $ do
setTitleI MsgUserProfile
$(widgetFile "nhUserProfile")

Thanks to Tickhon Jelvis for pointing out those web pages, also I found this one: Poly Hamlet i18n where I was able to get the solution to the problem.
So, if I would like to use a localized message, I would do:
getUserProfileR :: Handler RepHtml
getUserProfileR = do
maid <- maybeAuth
msg <- getRenderMessage
let user = case maid of
Nothing -> msg MsgNoUser --"(Unknown User ID)"
Just (Entity _ u) -> userEmail u
defaultLayout $ do
setTitleI MsgUserProfile
$(widgetFile "nhUserProfile")
Also remember that there is a helper function "setTitleI" which takes directly a Msg value and avoids the use of "msg MsgThisPageTitle"

My understanding of the I18N module is that you want to take your AppMessage value and use renderMessage on it.
You need to pass in a type specifying your translation type and a list of languages as well as your message. The translation type is created using the mkMessage function and the list of languages looks something like ["en-US", "en-GB", "fr"].

Related

Yesod: Passing the current user to a form

I've looked for this, but the answer found here ends up on a list containing the value. I'm wondering if there isn't another, more straightforward way to do what I need.
I have a form:
formReview :: UserId -> Form Review
formReview uid = renderDivs $ Review <$>
areq textField "Text" Nothing <*>
areq intField "Rating" Nothing <*>
areq (selectField films) "Film" Nothing <*>
pure uid
as you can see I'm trying to pass an user ID to the form, because these are the fields for Review:
Review
text Text
rating Int
film FilmId
author UserId
it requires the ID of the author.
The way I'm trying to do this is by doing the following on postReviewsR:
postReviewsR :: Handler Html
postReviewsR = do
uid <- lookupSession "_ID"
case uid of
Nothing -> do
defaultLayout [whamlet| <h1> User isn't logged in.|]
Just uid ->
((result, _), _) <- runFormPost $ formReview uid
case result of
FormSuccess review -> do
runDB $ insert review
defaultLayout [whamlet|
<h1> Review posted.
|]
_ -> redirect ReviewsR
it has to be a Maybe because in theory you could try to post something without being logged in, so uid could be empty. If I try to go straight to ((result, _), _) <- runFormPost $ formReview uid it says there's a Maybe Text.
Now my problem is similar the other post, it's this error:
• Couldn't match type ‘Text’ with ‘Key User’
Expected type: UserId
Actual type: Text
• In the first argument of ‘formReview’, namely ‘uid’
In the second argument of ‘($)’, namely ‘formReview uid’
In a stmt of a 'do' block:
((result, _), _) <- runFormPost $ formReview uid
and the suggestion in the linked post is to use keyToValues :: Key record -> [PersistValue] to turn the Key, which apparently is just Text, into what I need, the UserId.
It seems too clunky to me that I would need to do all this, then use head on that list to get the value within, all to get the ID into the form. There has to be another, more correct way, right? What am I missing here? How do I get the ID of the user who's making the Review in there?
EDIT: I should point out that I'm not using Yesod.Auth so I can't use its functions.
Assuming you're using a SQL backend, check out the persistent SQL documentation. You'll find the toSqlKey :: ToBackendKey SqlBackend record => Int64 -> Key record function. Basically, you'll need to parse your Text to Int64 and get a key using toSqlKey. You should probably also check if the key you're getting is actually valid.
(You've apparently misread the error, your uid is just a Text value, but you need a UserID which is a Key User).

CSRF: Yesod runFormPost always FormFailure

Apologies in advance for the code dump. I'm pretty new to both Yesod and Haskell and I'm struggling with CSRF issues. The problem as I understand it is that the form's _token isn't matching the environment token (running runFormPostNoToken works just fine). I have a pair of routes:
/ HomeR GET
/upload UploadR POST
The Handler for HomeR is defined as such:
getHomeR :: Handler Html
getHomeR = do
((res, uploadWidget), enctype) <- runFormPost imgForm
setTitle "Title"
$(widgetFile "homepage")
And the form itself and the upload Handler are:
imgForm :: Html -> MForm (HandlerT App IO) (FormResult Img, Widget)
imgForm hiddenInput = do
(titleRes, titleView) <- mreq textField uploadFormTitleSettings Nothing
(descRes, descView) <- mopt textareaField uploadFormDescriptionSettings Nothing
(fileRes, fileView) <- mreq fileField uploadFormAttachmentSettings Nothing
let imgRes = Img
<$> titleRes
<*> descRes
<*> fileRes
<*> pure (Likes 0)
<*> pure (Dislikes 0)
<*> pure (UserID 1)
<*> pure (Community 1)
let imgUploadWidget = do
toWidget
[whamlet|
^{ fvInput titleView }
^{ fvInput descView }
^{ fvInput fileView }
#{ hiddenInput }
<button type="submit">Submit
|]
return (imgRes, imgUploadWidget)
postImgUploadR :: HandlerT App IO Html
postImgUploadR = do
((imgRes, imgUploadWidget), enctype) <- runFormPost imgForm
let submission :: HandlerT App IO Html
submission = case imgRes of
FormSuccess upload -> defaultLayout [whamlet|The form was uploaded|]
FormMissing -> defaultLayout [whamlet|The form is missing|]
FormFailure upload -> defaultLayout [whamlet|The form failed.|]
submission
Unfortunately I'm not even sure what question to be asking here -- hopefully there's something obviously wrong with my code and someone can point me in the right direction. I spent some time reading through the source code of the functions and I think I understand it, but I'm not sure where the second, erroneous CSRF token is coming from (I assumed it would be set in a session variable and therefore wouldn't change). It's been quite a few hours and all my attempts to figure this out have failed.
Well, this turns out to be one of the most time-consuming bugs with one of the most mundane answers I've ever dealt with.
Some time ago I added in sslOnlySessions to makeSessionBackend and forgot about it. After trying to wrap my brain around every possible way I could have done something wrong, I took a step back, tried to think of a different angle to approach the problem from, and it hit me like a ton of bricks.
I'm posting this answer on the off chance someone 10 years from now will make the same dumb mistake I did, and after scouring their code for mistakes, finally fire up Google and find an easy answer.
Godspeed, future Haskeller.

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")

How can I show a <select> for a has many relationship in Yesod?

I have a simple one-to-many relationship, say something like this
Process
name Text
Report
time Text
process ProcessId
I'd like to create a form for creating posts where I'll be able to select from a list of existing users. Something like this.
processOptions :: Handler (OptionList (KeyBackend SqlBackend Process))
processOptions = optionsPersist [] [Desc ProcessName] id
postForm = renderDivs $ Report
<$> areq textField "Time" Nothing
<*> areq (selectField processOptions) "Process" Nothing
The prolem is, that I can't just figure out how to properly use selectField together with optionsPersist. I've looked at the source code for clues, but I just can't figure out what to do with this.
The expected type of the optionsPersist seems to be Handler (OptionList (KeyBackend SqlBackend Process)), though what it actually returns is Handler (OptionList (Entity Process)). I'm not sure if I'm missing something, or if there's some unwrapping that needs to be done.
Here's the actual error message
Couldn't match type ‘Entity (ProcessGeneric SqlBackend)’
with ‘KeyBackend SqlBackend Process’
Expected type: Handler (OptionList (KeyBackend SqlBackend Process))
Actual type: HandlerT
App IO (OptionList (Entity (ProcessGeneric SqlBackend)))
While there is a question for a similar topic, I don't think this is a duplicate, as this question is about using optionsPersist, while the other question simply generates the options manually.
I think you're almost there. The problem doesn't seem to be in how you're calling selectField or processOptions. The issue is that the result of that call is going to be an Entity Process, whereas the second field in Report is an ProcessId. So you just need to use fmap (aka <$>) to convert. I believe the following will do it:
entityKey <$> (areq (selectField processOptions) "Process" Nothing)

Shared data in Yesod

I want to share some data across requests in Yesod. In my case that data is a MVar (Data.Map Text ReadWriteLock), but I don't think the format of the data being shared matters too much here.
In Foundation.hs, there is a comment that says I can add fields to App, and every handler will have access to the data there. This seems like an approach I could use to share data between different handlers. I have been looking through the Yesod book, but I could not find any examples of getting data from App.
How would I access the newly created field from within a handler?
I think this might be a good use case for STM. I could share a TVar (Data.Map Text ReadWriteLock). But creating a TVar wraps the TVar in the STM monad. I might be mistaken, but to me that seems like the entire Yesod "main loop" would need to be run in the STM monad.
Is using STM a viable option here? Could anyone elaborate on how this might be achieved?
This tutorial for building a file server with Yesod shows quite nicely how you can use STM to access shared data. The relevant part starts from part 2.
To elaborate on pxqr's comment, you want to do something like this.
In your Foundation.hs file (assuming you started your project with yesod init).
data App = App
{ ... other fields
, shared :: TVar Int -- New shared TVar field
}
Then in your Application.hs file where you create the App instance.
makeFoundation conf = do
.... snip .....
tv <- newTVarIO 0 -- Initialize your TVar
let logger = Yesod.Core.Types.Logger loggerSet' getter
foundation = App conf s manager logger tv -- Add TVar here
return foundation
Then in your Handler use the TVar
getHomeR :: Handler Html
getHomeR = do
app <- getYesod -- Get the instance of App
x <- liftIO $ atomically $ do -- Read and update the TVar value.
val <- readTVar (shared app)
writeTVar (shared app) (val + 1)
return val
(formWidget, formEnctype) <- generateFormPost sampleForm
let submission = Nothing :: Maybe (FileInfo, Text)
handlerName = "getHomeR" :: Text
defaultLayout $ do
aDomId <- newIdent
-- Use the TVar value (increments on each page refresh).
setTitle $ fromString (show x)
$(widgetFile "homepage")

Resources