Use one form field for two Model entries in Yesod - haskell

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.

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!

How do I process a parameterized Yesod form from a handler where the parameter is unavailable?

I have the following form:
userForm :: UserId -> Form UserDemographics
userForm uid = renderDivs $ UserDemographics <$>
pure uid <*>
areq yearField "Year of birth" Nothing <*>
areq textField "Gender" Nothing <*>
areq countryField "Country of residence" Nothing <*>
areq boolField "Are you a computer programmer?" Nothing
On my homepage, I use generateFormPost $ userForm (entityKey userEnt) to make a form with the UserId filled in. But I want to handle the input with AJAX, so a separate Handler gets the results of the form. The other handler doesn't have access to the UserId. How do I process the form? I tried this, which throws an error:
postDemoFormR :: Handler RepJson
postDemoFormR = do
((formData, _), _) <- runFormPost $ userForm undefined
$(logDebug) $ pack $ show formData
return $ repJson ()
I could change userForm's type to accept Maybe UserId instead of just UserId or make up a bogus UserId for the call to runFormPost but both of those are hacks. Is there an easy, clean way to do this?
You could use a hiddenField, but that's almost certainly not what you really want (any user would be able to spoof the UserId by just submitting a different value). Assuming what you're trying to do is actually say "who is the current user", you'd need some way of securely determining that in your AJAX handler (such as requireAuthId).

Error when using options fields in Yesod subsite form

I'm trying to use a selectFieldList inside a subsite form but I get the following error:
Couldn't match type 'IO' with 'HanderT master IO'
I'm running into this problem when using the following snippets, where the subsite is named TestSub (this subsite is separated from the master site):
Types:
type TestHandler a = forall master. Yesod master
=> HandlerT TestSub (HandlerT master IO) a
type Form a = forall master. Yesod master
=> Html -> MForm (HandlerT TestSub (HandlerT master IO)) (FormResult a, WidgetT TestSub IO ())
Form & handler:
testForm :: Form (Text, Int)
testForm = renderBootstrap3 BootstrapBasicForm $ (,)
<$> areq textField (bfs MsgText) Nothing
<*> areq (selectFieldList [(MsgFirst, 1), (MsgSecond, 2)]) (bfs MsgSelect) Nothing
getTestHome :: TestHandler Html
getTestHome = do
(formWidget, _) <- generateFormPost testForm
defaultLayoutSub $ do
setTitleI MsgTest
[whamlet|^{formWidget}|]
when the select field is replaced with for instance an intField the form and handler work as expected. While looking for the selectFieldList on Hoogle I found that options fields (select, radio, checkbox) have a different signature (displayed below) then the "normal" fields. I suspect this difference to be the problem but haven't found a work-around without having to implement the option fields all over.
Options field signature:
selectFieldList :: (Eq a, RenderMessage site FormMessage, RenderMessage site msg)
=> [(msg, a)]
-> Field (HandlerT site IO) a
Normal field signature:
intField :: (Monad m, Integral i, RenderMessage (HandlerSite m) FormMessage)
=> Field m i
Is there a way to get the options fields to work in a subsite context, without reimplementing them?
It's usually best to run your forms in the master site, not the subsite, by calling lift. You'll also need to modify your type synonyms a bit to match, but the basic idea is to replace:
(formWidget, _) <- generateFormPost testForm
with
(formWidget, _) <- lift $ generateFormPost testForm
EDIT
I still recommend the above approach. However, to get the alternate that you're asking for, change your type synonym to:
type Form a =
Html -> MForm (HandlerT HelloSub IO) (FormResult a, WidgetT HelloSub IO ())
and then use liftHandlerT:
liftHandlerT $ generateFormPost testForm
Keep in mind that this isn't how subsites are designed to be used, so you'll likely end up with some more friction as you keep going.
Regarding master translations: you can definitely leverage them, you just put in a constraint along the lines of RenderMessage master MessageDataType. That's what's used for FormMessage all over the place.
EDIT2
One more incantation you may find useful:
defaultLayoutSub $ liftWidgetT widget

Capturing Persistent Relations in a Form

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.

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