Capturing Persistent Relations in a Form - haskell

I have defined a one-to-many relationship in Persistent but could not figure out how to create a form that can take one of the foreign keys as input. Simplifying my use case to something like this:
Person
name String
Car
personId PersonId
name Text
type Text
Now when I try to generate a Form for Car, what should be the field type for personId? I tried something like this but get an error:
entryForm :: Maybe Car -> Form Car
entryForm car = renderDivs $ Car
<$> areq personIdField "Person" Nothing
<*> areq textField "Car Name" ( carName <$> car)
<*> areq textField "Type" ( carType <$> car)
When I run the above I get the error: Not in scope: `personIdField'.
I tried intField and it says:
Couldn't match expected type `KeyBackend
persistent-1.2.1:Database.Persist.Sql.Types.SqlBackend Person'
with actual type `Text'
Expected type: Field
m0
(KeyBackend
persistent-1.2.1:Database.Persist.Sql.Types.SqlBackend Person)
Actual type: Field m0 Text
In the first argument of `areq', namely `intField'
In the second argument of `(<$>)', namely
`areq intField "Person" Nothing
Ideally I would like to populate a drop down of Person Names (if there are not too many) or have a free form text field (e.g., with autocomplete) when there are too many. Any suggestions on how I can get foreign key as an input from the user?
Update:
I tried using selectField as follows but not sure if I am doing this correctly. I still get an error. First I created a where statement to get personId:
where
personId = do
person <- runDB $ selectFirst [] [Asc PersonName]
case person of
Just (Entity pId p) -> return pId
-- Nothing -> ???
and then I changed my first areq to
<$> areq (selectField [("First", personId)]) "Person Name" Nothing
Thanks!

I was able to figure out how to use selectField properly. This is what I ended up doing:
where
people = do
entities <- runDB $ selectList [] [Asc PersonName]
optionsPairs $ map (\p -> (personName $ entityVal p, entityKey p)) entities
The form field became:
<$> areq (selectField people) "Person Name" Nothing
I still have not figured out free form entry just yet but this is a good start.

Related

How to create a entity key form field

I want to create a new form field to enter an entity key, in this case an ArticleId instead of an Integer field, can I do that?
There is my Article model
Article
title Text
content Text
userId Int
score Int Maybe
deriving Show
deriving Eq
And there is my AForm Article handler
<$> areq intField (bfs ("Article Id" :: Text)) Nothing
<*> areq intField (bfs ("Tag Id" :: Text)) Nothing
I want to do this to avoid use toSqlKey in my selectList
Thanks!

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

Use one form field for two Model entries in Yesod

I am making a link aggregator where people can submit articles.
My data model contains
Article
title Text
url Text
domain Text
I would like the user to input a url into the form and then I run a function on the URL to extract the domain and then save both in the database.
I would like the user to only have to enter the URL once, as in:
entryForm = renderDivs $ Article¬
<$> areq textField "Url" Nothing¬
<*> areq textField "Title" Nothing¬
But I get this error
Couldn't match type ‘Text -> Article’ with ‘Article’
Expected type: Form Article
Actual type: blaze-markup-0.7.0.0:Text.Blaze.Internal.Markup
-> MForm
(HandlerT App IO)
(FormResult (Text -> Article),
WidgetT (HandlerSite (HandlerT App IO)) IO ())
In the expression:
renderDivs
$ Article <$> areq textField "Url" Nothing
<*> areq textField "Title" Nothing
In an equation for ‘entryForm’:
entryForm
= renderDivs
$ Article <$> areq textField "Url" Nothing
<*> areq textField "Title" Nothing
because clearly the form doesn't match the type Article.
I am not sure how to proceed. I've been told I can either a)write an alternative Article' datatype and convert between the two, or b) create my own custom field, though both of those seem difficult to me as a newbie.
I'd recommend a helper function like:
makeArticle :: Text -> Text -> Article
Which takes the title and URL, extracts the domain name from the URL, and constructs an Article value. Then you can use that in place of calling the Article data constructor directly.

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 define a field on an applicative form for a foreign key in Yesod?

If we have defined 2 simple objects in our models file, for example :-
Person
name Text
Age Int
Book
title Text
author Text
We can define an applicative form for Book as :-
addBookForm = renderDivs $ Book
<$> areq textField "title" Nothing
<*> areq textField "author" Nothing
However, if we want to change the author from just a text field, to the id of a person, as :-
Book
title Text
author PersonId
Then the above form won't compile, with this error :-
Couldn't match expected type `KeyBackend Database.Persist.GenericSql.Raw.SqlBackend Person' with actual type `Text'
Expected type: Field
sub0
master0
(KeyBackend Database.Persist.GenericSql.Raw.SqlBackend Person)
Actual type: Field sub0 master0 Text
In the first argument of `areq', namely `textField'
In the second argument of `(<*>)', namely
`areq textField "author" Nothing'
How do we now define the author field ?
Do we need to use a monadic form ?
Thanks !
The error message means you are trying to use Text (from the field result) as a Key.
You can use checkMMap to wrap the textField and modify the result:
addBookForm = renderDivs $ Book
<$> areq textField "title" Nothing
<*> (entityKey <$> areq authorField "author" Nothing)
where
authorField = checkMMap findAuthor (personName . entityVal) textField
findAuthor name = do
mperson <- runDB $ selectFirst [PersonName ==. name] []
case mperson of
Just person -> return $ Right person
Nothing -> return $ Left ("Person not found." :: Text)
The findAuthor function gets simpler if you add a unique constructor to the Person field:
Person
name Text
...
UniquePerson name
Then instead of selectFirst ... you can do
mperson <- runDB $ getBy $ UniquePerson name

Resources