Added the ability to edit and delete posts

This commit is contained in:
Xnoe 2022-05-21 13:42:36 +01:00
parent 89d9d7c0cd
commit 9f963bb548
Signed by: xnoe
GPG Key ID: 45AC398F44F0DAFE
3 changed files with 170 additions and 35 deletions

View File

@ -90,18 +90,19 @@ getPostsInCategory c cat page = do
getPostContent :: IConnection t => t -> Int -> IO (Maybe JSValue) getPostContent :: IConnection t => t -> Int -> IO (Maybe JSValue)
getPostContent c pid = do getPostContent c pid = do
getPost <- prepare c "SELECT title, catid, content FROM posts WHERE postid = ?;" getPost <- prepare c "SELECT title, catid, content, subtext FROM posts WHERE postid = ?;"
execute getPost [toSql (pid)] execute getPost [toSql (pid)]
result <- fetchAllRows getPost result <- fetchAllRows getPost
case result of case result of
(t:cid:con:[]):[] -> do (t:cid:con:sub:[]):[] -> do
category <- getCategory c $ fromSql cid category <- getCategory c $ fromSql cid
let title = ("title", toJSString $ fromSql t) let title = ("title", toJSString $ fromSql t)
let content = ("content", toJSString $ fromSql con) let content = ("content", toJSString $ fromSql con)
let cat = ("category", toJSString category) let cat = ("category", toJSString category)
let catid = ("catid", toJSString $ fromSql cid) let catid = ("catid", toJSString $ fromSql cid)
return $ Just $ showJSON $ toJSObject [title, content, cat, catid]; let subtext = ("subtext", toJSString $ fromSql sub)
return $ Just $ showJSON $ toJSObject [title, content, cat, catid, subtext];
_ -> fail "Post does not exist!" _ -> fail "Post does not exist!"
hexOfBS :: B.ByteString -> String hexOfBS :: B.ByteString -> String
@ -180,12 +181,11 @@ attemptCreatePost c authtoken username title subtext content category = do
case cookieValid of case cookieValid of
False -> return Nothing False -> return Nothing
True -> do True -> do
-- First we should probably validate that the category actually exists -- Create the category if it doesn't exist
result <- createCategoryIfNotExist c category result <- createCategoryIfNotExist c category
case result of case result of
Nothing -> do return Nothing Nothing -> do return Nothing
Just catid -> do Just catid -> do
-- If we're here, we know the category does actually exist
addPost <- prepare c "INSERT INTO posts(catid, title, subtext, content) VALUES(?, ?, ?, ?) RETURNING postid;" addPost <- prepare c "INSERT INTO posts(catid, title, subtext, content) VALUES(?, ?, ?, ?) RETURNING postid;"
execute addPost [toSql catid, toSql title, toSql subtext, toSql content] execute addPost [toSql catid, toSql title, toSql subtext, toSql content]
post <- fetchAllRows addPost post <- fetchAllRows addPost
@ -210,6 +210,77 @@ handleCreatePost c = do
Just pid -> ok $ toResponse $ showJSON $ toJSObject [("success", toJSString "true"), ("postid", toJSString pid)] Just pid -> ok $ toResponse $ showJSON $ toJSObject [("success", toJSString "true"), ("postid", toJSString pid)]
Nothing -> ok $ toResponse $ showJSON $ toJSObject [("success", toJSString "false"), ("postid", toJSString "")] Nothing -> ok $ toResponse $ showJSON $ toJSObject [("success", toJSString "false"), ("postid", toJSString "")]
attemptEditPost :: IConnection t => t -> String -> String -> String -> String -> String -> String -> String -> IO (Maybe String)
attemptEditPost c authtoken username title subtext content category id = do
cookieValid <- validateCookie c authtoken username
case cookieValid of
False -> return Nothing
True -> do
-- Determine if the postid we're trying to update exists
postExists <- prepare c "SELECT * FROM posts WHERE postid = ?;"
execute postExists [toSql id]
exists <- fetchAllRows postExists
case exists of
[] -> return Nothing
[_] -> do
-- Create the category if it doesn't exist
result <- createCategoryIfNotExist c category
case result of
Nothing -> do return Nothing
Just catid -> do
addPost <- prepare c "UPDATE posts SET catid = ?, title = ?, subtext = ?, content = ? WHERE postid = ?"
execute addPost [toSql catid, toSql title, toSql subtext, toSql content, toSql id]
commit c
return $ Just id
handleEditPost :: IConnection t => t -> ServerPart Response
handleEditPost c = do
methodM POST
authtoken <- lookCookieValue "token"
username <- look "username"
title <- look "title"
subtext <- look "subtext"
content <- look "content"
category <- look "category"
postid <- look "id"
success <- lift $ attemptEditPost c authtoken username title subtext content category postid
case success of
Just pid -> ok $ toResponse $ showJSON $ toJSObject [("success", toJSString "true"), ("postid", toJSString pid)]
Nothing -> ok $ toResponse $ showJSON $ toJSObject [("success", toJSString "false"), ("postid", toJSString "")]
attemptDeletePost :: IConnection t => t -> String -> String -> String -> IO (Bool)
attemptDeletePost c authtoken username id = do
cookieValid <- validateCookie c authtoken username
case cookieValid of
False -> return False
True -> do
-- Determine if the postid we're trying to update exists
postExists <- prepare c "SELECT * FROM posts WHERE postid = ?;"
execute postExists [toSql id]
exists <- fetchAllRows postExists
case exists of
[] -> return False
[_] -> do
deletePost <- prepare c "DELETE FROM posts WHERE postid = ?;"
execute deletePost [toSql id]
commit c
return True
handleDeletePost :: IConnection t => t -> ServerPart Response
handleDeletePost c = do
methodM POST
authtoken <- lookCookieValue "token"
username <- look "username"
postid <- look "id"
success <- lift $ attemptDeletePost c authtoken username postid
case success of
True -> ok $ toResponse $ showJSON $ toJSObject [("success", toJSString "true")]
False -> ok $ toResponse $ showJSON $ toJSObject [("success", toJSString "false")]
attemptWhoami :: IConnection t => t -> String -> IO (Maybe String) attemptWhoami :: IConnection t => t -> String -> IO (Maybe String)
attemptWhoami c at = do attemptWhoami c at = do
getUser <- prepare c "SELECT username FROM users WHERE authtoken = ?;" getUser <- prepare c "SELECT username FROM users WHERE authtoken = ?;"
@ -253,5 +324,7 @@ main = do
, dir "v1" $ dir "logout" $ do , dir "v1" $ dir "logout" $ do
expireCookie "token" expireCookie "token"
ok $ toResponse $ showJSON $ toJSObject [("success", toJSString "true")] ok $ toResponse $ showJSON $ toJSObject [("success", toJSString "true")]
, dir "v1" $ dir "editPost" $ handleEditPost c
, dir "v1" $ dir "deletePost" $ handleDeletePost c
, notFound $ toResponse "Endpoint does not exist" , notFound $ toResponse "Endpoint does not exist"
] ]

View File

@ -18,7 +18,6 @@ services:
database: database:
image: postgres image: postgres
restart: unless-stopped
environment: environment:
- POSTGRES_PASSWORD=root - POSTGRES_PASSWORD=root
- POSTGRES_USER=blog - POSTGRES_USER=blog

View File

@ -34,7 +34,8 @@ type alias Post = {
id: String, id: String,
category: Category, category: Category,
title: String, title: String,
subtext: String subtext: String,
content: String
} }
type Route type Route
@ -45,6 +46,7 @@ type Route
| LoginView | LoginView
| LogoutView | LogoutView
| CreatePostView | CreatePostView
| EditPostView
routeParser : P.Parser (Route -> a) a routeParser : P.Parser (Route -> a) a
routeParser = routeParser =
@ -61,16 +63,16 @@ routeParser =
processPostListing : Decoder (List Post) processPostListing : Decoder (List Post)
processPostListing = processPostListing =
Json.Decode.list ( Json.Decode.list (
map5(\a->\b->\c->\d->\e-> Post a (Category b c) d e) (field "id" string) (field "catid" string) (field "category" string) (field "title" string) (field "subtext" string) map5(\a->\b->\c->\d->\e-> Post a (Category b c) d e "") (field "id" string) (field "catid" string) (field "category" string) (field "title" string) (field "subtext" string)
) )
processCategoryListing : Decoder (List Category) processCategoryListing : Decoder (List Category)
processCategoryListing = processCategoryListing =
Json.Decode.list (map2 Category (field "id" string) (field "name" string)) Json.Decode.list (map2 Category (field "id" string) (field "name" string))
processPost : Decoder (String, String, String) processPost : Decoder {title: String, content: String, subtext: String, category: Category}
processPost = processPost =
map3 (\a -> \b -> \c -> (a,b,c)) (field "title" string) (field "content" string) (field "category" string) map5 (\a -> \b -> \c -> \d -> \e -> {title=a,content=b,subtext=e,category=(Category c d)}) (field "title" string) (field "content" string) (field "catid" string) (field "category" string) (field "subtext" string)
processCategory : Decoder (String, List Post) processCategory : Decoder (String, List Post)
processCategory = processCategory =
@ -82,6 +84,12 @@ processLogout =
"true" -> True "true" -> True
_ -> False) (field "success" string) _ -> False) (field "success" string)
processDeletePost : Decoder (Bool)
processDeletePost =
Json.Decode.map (\a -> case a of
"true" -> True
_ -> False) (field "success" string)
processLogin : Decoder (Maybe User) processLogin : Decoder (Maybe User)
processLogin = processLogin =
map2 (\a->\b->case a of map2 (\a->\b->case a of
@ -94,6 +102,12 @@ processCreatePost =
"true" -> Just b "true" -> Just b
_ -> Nothing) (field "success" string) (field "postid" string) _ -> Nothing) (field "success" string) (field "postid" string)
processEditPost : Decoder (Maybe String)
processEditPost =
map2 (\a->\b->case a of
"true" -> Just b
_ -> Nothing) (field "success" string) (field "postid" string)
processWhoami : Decoder (Maybe String) processWhoami : Decoder (Maybe String)
processWhoami = processWhoami =
map2 (\a->\b->case a of map2 (\a->\b->case a of
@ -127,10 +141,7 @@ type alias Model = {
username: String, username: String,
password: String, password: String,
user: Maybe User, user: Maybe User,
title: String, post: Post
subtext: String,
content: String,
category: String
} }
type Msg type Msg
@ -138,7 +149,7 @@ type Msg
| UrlChanged Url.Url | UrlChanged Url.Url
| GotPosts (Result Http.Error (List Post)) | GotPosts (Result Http.Error (List Post))
| GotCategories (Result Http.Error (List Category)) | GotCategories (Result Http.Error (List Category))
| GotPost (Result Http.Error (String, String, String)) | GotPost (Result Http.Error {title: String, content: String, subtext: String, category: Category})
| GotCategory (Result Http.Error (String, List Post)) | GotCategory (Result Http.Error (String, List Post))
| LoginResult (Result Http.Error (Maybe User)) | LoginResult (Result Http.Error (Maybe User))
| UsernameUpdate String | UsernameUpdate String
@ -152,7 +163,13 @@ type Msg
| CategoryUpdate String | CategoryUpdate String
| CreatePost | CreatePost
| CreatePostResult (Result Http.Error (Maybe String)) | CreatePostResult (Result Http.Error (Maybe String))
| DeletePostResult (Result Http.Error Bool)
| EditPostResult (Result Http.Error (Maybe String))
| GotWhoami (Result Http.Error (Maybe String)) | GotWhoami (Result Http.Error (Maybe String))
| GotoEditPost
| GotoDeletePost
| EditPost
| DeletePost
init : () -> Url.Url -> Browser.Navigation.Key -> (Model, Cmd Msg) init : () -> Url.Url -> Browser.Navigation.Key -> (Model, Cmd Msg)
init _ url key = init _ url key =
@ -161,7 +178,7 @@ init _ url key =
header = "", body = "", footer = "", pinnedPosts = [], header = "", body = "", footer = "", pinnedPosts = [],
posts = [], route = r, key = key, errMessage = Nothing, posts = [], route = r, key = key, errMessage = Nothing,
username = "", password = "", user = Nothing, username = "", password = "", user = Nothing,
title = "", subtext = "", content = "", category = "Blog Post" post = Post "" (Category "" "") "" "" ""
}, },
Http.get {url = "/v1/whoami", expect = Http.expectJson GotWhoami processWhoami} Http.get {url = "/v1/whoami", expect = Http.expectJson GotWhoami processWhoami}
) )
@ -176,11 +193,16 @@ update msg model =
UrlChanged req -> UrlChanged req ->
( (
let r = P.parse routeParser req in let r = P.parse routeParser req in
({model | route = r}, messageOfRoute r) let imodel = case r of
Just CreatePostView -> {model | post = Post "" (Category "" "") "" "" ""}
Just (PostView p) -> let post = model.post in {model | post = {post | id = p}}
_ -> model
in
({imodel | route = r}, messageOfRoute r)
) )
GotPosts (Ok l) -> ({model | errMessage = Nothing, posts = l}, Cmd.none) GotPosts (Ok l) -> ({model | errMessage = Nothing, posts = l}, Cmd.none)
GotPosts (Err _) -> ({model | errMessage = Just "Failed to load posts"}, Cmd.none) GotPosts (Err _) -> ({model | errMessage = Just "Failed to load posts"}, Cmd.none)
GotPost (Ok (title, content, category)) -> ({model | errMessage = Nothing, header = title, body = content, footer = category}, Cmd.none) GotPost (Ok {title, content, subtext, category}) -> let post = model.post in ({model | post = {post | title = title, content = content, category = category, subtext = subtext}}, Cmd.none)
GotCategory (Ok (title, posts)) -> ({model | errMessage = Nothing, header = title, posts = posts}, Cmd.none) GotCategory (Ok (title, posts)) -> ({model | errMessage = Nothing, header = title, posts = posts}, Cmd.none)
Login -> (model, handleLogin model) Login -> (model, handleLogin model)
LoginResult (Ok (Just _ as u)) -> ({model | user = u}, load "/") LoginResult (Ok (Just _ as u)) -> ({model | user = u}, load "/")
@ -189,15 +211,21 @@ update msg model =
LogoutResult _ -> (model, load "/") LogoutResult _ -> (model, load "/")
UsernameUpdate u -> ({model | username = u}, Cmd.none) UsernameUpdate u -> ({model | username = u}, Cmd.none)
PasswordUpdate p -> ({model | password = p}, Cmd.none) PasswordUpdate p -> ({model | password = p}, Cmd.none)
TitleUpdate t -> ({model | title = t}, Cmd.none) TitleUpdate t -> let post = model.post in ({model | post = {post | title = t}}, Cmd.none)
SubtextUpdate s -> ({model | subtext = s}, Cmd.none) SubtextUpdate s -> let post = model.post in ({model | post = {post | subtext = s}}, Cmd.none)
ContentUpdate c -> ({model | content = c}, Cmd.none) ContentUpdate c -> let post = model.post in ({model | post = {post | content = c}}, Cmd.none)
CategoryUpdate c -> ({model | category = c}, Cmd.none) CategoryUpdate c -> let post = model.post in let category = model.post.category in ({model | post = {post | category = {category | name = c}}}, Cmd.none)
CreatePost -> (model, handleCreatePost model) CreatePost -> (model, handleCreatePost model)
CreatePostResult (Ok (Just postid)) -> (model, load ("/post/" ++ postid)) CreatePostResult (Ok (Just postid)) -> (model, load ("/post/" ++ postid))
CreatePostResult (Ok Nothing) -> ({model | errMessage = Just "Failed to create post"}, Cmd.none) CreatePostResult (Ok Nothing) -> ({model | errMessage = Just "Failed to create post"}, Cmd.none)
DeletePostResult (Ok True) -> (model, load "/")
DeletePostResult (Ok False) -> ({model | errMessage = Just "Failed to delete post"}, Cmd.none)
GotWhoami (Ok (Just u)) -> ({model | user = Just (User u)}, messageOfRoute model.route) GotWhoami (Ok (Just u)) -> ({model | user = Just (User u)}, messageOfRoute model.route)
GotWhoami _ -> (model, messageOfRoute model.route) GotWhoami _ -> (model, messageOfRoute model.route)
GotoEditPost -> ({model | route = Just EditPostView}, Cmd.none)
EditPost -> (model, handleEditPost model)
EditPostResult (Ok (Just postid)) -> (model, load ("/post/" ++ postid))
DeletePost -> (model, handleDeletePost model)
_ -> ({model | errMessage = Just "Something has gone horribly wrong."}, Cmd.none) _ -> ({model | errMessage = Just "Something has gone horribly wrong."}, Cmd.none)
handleLogin : Model -> Cmd Msg handleLogin : Model -> Cmd Msg
@ -217,14 +245,46 @@ handleCreatePost model =
url = "/v1/createPost", url = "/v1/createPost",
body = multipartBody [ body = multipartBody [
stringPart "username" user.username, stringPart "username" user.username,
stringPart "title" model.title, stringPart "title" model.post.title,
stringPart "subtext" model.subtext, stringPart "subtext" model.post.subtext,
stringPart "content" model.content, stringPart "content" model.post.content,
stringPart "category" model.category stringPart "category" model.post.category.name
], ],
expect = Http.expectJson CreatePostResult processCreatePost expect = Http.expectJson CreatePostResult processCreatePost
} }
handleEditPost : Model -> Cmd Msg
handleEditPost model =
case model.user of
Nothing -> Cmd.none
Just user ->
Http.post {
url = "/v1/editPost",
body = multipartBody [
stringPart "username" user.username,
stringPart "id" model.post.id,
stringPart "title" model.post.title,
stringPart "subtext" model.post.subtext,
stringPart "content" model.post.content,
stringPart "category" model.post.category.name
],
expect = Http.expectJson EditPostResult processEditPost
}
handleDeletePost : Model -> Cmd Msg
handleDeletePost model =
case model.user of
Nothing -> Cmd.none
Just user ->
Http.post {
url = "/v1/deletePost",
body = multipartBody [
stringPart "username" user.username,
stringPart "id" model.post.id
],
expect = Http.expectJson DeletePostResult processDeletePost
}
type alias Document msg = { type alias Document msg = {
title: String, title: String,
body: List (Html msg) body: List (Html msg)
@ -263,10 +323,13 @@ renderModel model =
case model.route of case model.route of
Just route -> case route of Just route -> case route of
Home -> div [] [h1 [] [text "Welcome to my blog."],cardListing model] Home -> div [] [h1 [] [text "Welcome to my blog."],cardListing model]
PostView _ -> div [] [h1 [] [text (model.header)], p [] [text(model.body)], h3 [] [text(model.footer ++ " ")]] PostView _ -> div [] ([h1 [] [text (model.post.title)], p [] [text(model.post.content)], h3 [] [text(model.post.category.name ++ " ")]] ++ case model.user of
Nothing -> []
Just u -> [a [href "", onClick DeletePost] [text "Delete Post"], text " - ", a [href "", onClick GotoEditPost] [text "Edit Post"]])
CategoryView _ -> div [] [h1 [] [text (model.header)], cardListing model] CategoryView _ -> div [] [h1 [] [text (model.header)], cardListing model]
LoginView -> loginForm model LoginView -> loginForm model
CreatePostView -> createPostForm model CreatePostView -> createPostForm model CreatePost "Create Post"
EditPostView -> createPostForm model EditPost "Edit Post"
_ -> h1 [] [ text ("Not found."), a [ href "/"] [text("Return home")] ] _ -> h1 [] [ text ("Not found."), a [ href "/"] [text("Return home")] ]
_ -> h1 [] [ text ("Not found."), a [ href "/"] [text("Return home")] ] _ -> h1 [] [ text ("Not found."), a [ href "/"] [text("Return home")] ]
cardListing : Model -> Html Msg cardListing : Model -> Html Msg
@ -327,17 +390,17 @@ renderPost post =
] ]
] ]
createPostForm : Model -> Html Msg createPostForm : Model -> Msg -> String -> Html Msg
createPostForm model = createPostForm model msg buttonText =
div [ div [
style "display" "flex", style "display" "flex",
style "flex-direction" "column" style "flex-direction" "column"
] [ ] [
viewInput "text" "title" "Title" model.title TitleUpdate, viewInput "text" "title" "Title" model.post.title TitleUpdate,
viewInput "text" "subtext" "Subtext" model.subtext SubtextUpdate, viewInput "text" "subtext" "Subtext" model.post.subtext SubtextUpdate,
viewTextarea "content" "Content" model.content ContentUpdate, viewTextarea "content" "Content" model.post.content ContentUpdate,
viewInput "text" "category" "Subtext" model.category CategoryUpdate, viewInput "text" "category" "Blog Post" model.post.category.name CategoryUpdate,
button [onClick CreatePost] [text "Create Post"] button [onClick msg] [text buttonText]
] ]
loginForm : Model -> Html Msg loginForm : Model -> Html Msg