Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add delete post functionality #15

Merged
merged 22 commits into from
Jan 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
64cbc58
Added header to group the home link and nav. Added aria label to link…
Thomasorus Jan 11, 2022
f3bc80c
Titles need to start with h1 and flow logically to h2, h3, etc...
Thomasorus Jan 11, 2022
6f27809
Inputs need labels to be voiced correctly, placeholders are not enoug…
Thomasorus Jan 11, 2022
bc948a6
Changed some styles for the header and navigations. Same design, just…
Thomasorus Jan 11, 2022
407c93c
Grouped threads in main to allow screen readers users to jump directl…
Thomasorus Jan 11, 2022
b04f6e8
Added main for semantics, added labels for inputs, h2 to h1.
Thomasorus Jan 11, 2022
ad2d52b
Inlined the logo with the correct size instead of img tag. Removed ma…
Thomasorus Jan 11, 2022
a3656dd
Added visually hidden class
Thomasorus Jan 11, 2022
f3f27e7
Added some semantic text and hid it to sighted readers with the visua…
Thomasorus Jan 11, 2022
ec55a0d
Added main, label for inputs, made the form vertical for readability.…
Thomasorus Jan 11, 2022
475cffa
Moved instructions before form. If you use a screen reader, you start…
Thomasorus Jan 11, 2022
aa7553e
Added password instructions
Thomasorus Jan 11, 2022
ae5fc1f
Added main and fixed title h2 to h1
Thomasorus Jan 11, 2022
06410d6
Error messages should not be in footer. Informations should be before…
Thomasorus Jan 11, 2022
3c6c1c0
Changed div>p to article>p to align with the current design but keep …
Thomasorus Jan 11, 2022
36c058c
Added main, h2 to h1.
Thomasorus Jan 11, 2022
9cf06f7
Forgot to put everything into main, and article for this page
Thomasorus Jan 11, 2022
f0d767e
fix conflicts
cblgh Jan 12, 2022
fdcfd52
add delete post functionality
cblgh Jan 12, 2022
0b701b7
add confirmation dialog on delete (and prevent unregrettable mistakes!)
cblgh Jan 13, 2022
8ff8507
improve error messages when deleting posts
cblgh Jan 13, 2022
2ea0003
resolve conflict
cblgh Jan 13, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,11 @@ func (d DB) CreateThread(title, content string, authorid, topicid int) (int, err
// https://medium.com/aubergine-solutions/how-i-handled-null-possible-values-from-database-rows-in-golang-521fb0ee267
// type NullTime sql.NullTime
type Post struct {
ID int
ThreadTitle string
Content template.HTML
Author string
AuthorID int
Publish time.Time
LastEdit sql.NullTime // TODO: handle json marshalling with custom type
}
Expand All @@ -182,7 +184,7 @@ func (d DB) GetThread(threadid int) []Post {
// users table to get user name
// threads table to get thread title
query := `
SELECT t.title, content, u.name, p.publishtime, p.lastedit
SELECT p.id, t.title, content, u.name, p.authorid, p.publishtime, p.lastedit
FROM posts p
INNER JOIN users u ON u.id = p.authorid
INNER JOIN threads t ON t.id = p.threadid
Expand All @@ -200,14 +202,28 @@ func (d DB) GetThread(threadid int) []Post {
var data Post
var posts []Post
for rows.Next() {
if err := rows.Scan(&data.ThreadTitle, &data.Content, &data.Author, &data.Publish, &data.LastEdit); err != nil {
if err := rows.Scan(&data.ID, &data.ThreadTitle, &data.Content, &data.Author, &data.AuthorID, &data.Publish, &data.LastEdit); err != nil {
log.Fatalln(util.Eout(err, "get data for thread %d", threadid))
}
posts = append(posts, data)
}
return posts
}

func (d DB) GetPost(postid int) (Post, error) {
stmt := `
SELECT p.id, t.title, content, u.name, p.authorid, p.publishtime, p.lastedit
FROM posts p
INNER JOIN users u ON u.id = p.authorid
INNER JOIN threads t ON t.id = p.threadid
WHERE p.id = ?
`
var data Post
err := d.db.QueryRow(stmt, postid).Scan(&data.ID, &data.ThreadTitle, &data.Content, &data.Author, &data.AuthorID, &data.Publish, &data.LastEdit)
err = util.Eout(err, "get data for thread %d", postid)
return data, err
}

type Thread struct {
Title string
Author string
Expand Down Expand Up @@ -257,10 +273,10 @@ func (d DB) EditPost(content string, postid int) {
util.Check(err, "edit post %d", postid)
}

func (d DB) DeletePost(postid int) {
func (d DB) DeletePost(postid int) error {
stmt := `DELETE FROM posts WHERE id = ?`
_, err := d.Exec(stmt, postid)
util.Check(err, "deleting post %d", postid)
return util.Eout(err, "deleting post %d", postid)
}

func (d DB) CreateTopic(title, description string) {
Expand Down
20 changes: 19 additions & 1 deletion html/thread.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
{{ template "head" . }}
<main>
<h1>{{ .Data.Title }}</h1>
{{ $userID := .LoggedInID }}
{{ $threadURL := .Data.ThreadURL }}
{{ range $index, $post := .Data.Posts }}
<article>
<div>
<p><span><b>{{ $post.Author }}</b><span class="visually-hidden"> responded:</span></span><span style="float: right;"><time datetime="{{ $post.Publish | formatDate }}">{{ $post.Publish | formatDateRelative }}</time></span></p>
{{ if eq $post.AuthorID $userID }}
<span style="float: right;" aria-label="Delete this post">
<form style="display: inline-block;" method="POST" action="/post/delete/{{ $post.ID }}"
onsubmit="return confirm('Delete post for all posterity?');"
>
<button style="background-color: transparent; border: 0; padding: 0;" type="submit">delete</button>
<input type="hidden" name="thread" value="{{ $threadURL }}">
</form>
</span>
{{ end }}
<p>
<span><b>{{ $post.Author }}</b>
<span class="visually-hidden"> responded:</span></span>
<span style="margin-left: 0.5rem; font-style: italic;">
<time datetime="{{ $post.Publish | formatDate }}">{{ $post.Publish | formatDateRelative }}</time>
</span>
</p>
{{ $post.Content }}
</article>
{{ end }}
Expand Down
94 changes: 71 additions & 23 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"html/template"
"net/http"
"net/url"
"strconv"
"strings"
"time"

Expand All @@ -23,9 +22,10 @@ import (
/* TODO (2022-01-03): include csrf token via gorilla, or w/e, when rendering */

type TemplateData struct {
Data interface{}
LoggedIn bool // TODO (2022-01-09): put this in a middleware || template function or sth?
Title string
Data interface{}
LoggedIn bool // TODO (2022-01-09): put this in a middleware || template function or sth?
LoggedInID int
Title string
}

type IndexData struct {
Expand Down Expand Up @@ -54,8 +54,9 @@ type LoginData struct {
}

type ThreadData struct {
Title string
Posts []database.Post
Title string
Posts []database.Post
ThreadURL string
}

type RequestHandler struct {
Expand Down Expand Up @@ -159,21 +160,14 @@ func (h RequestHandler) renderView(res http.ResponseWriter, viewName string, dat
}

func (h RequestHandler) ThreadRoute(res http.ResponseWriter, req *http.Request) {
parts := strings.Split(strings.TrimSpace(req.URL.Path), "/")
// invalid route, redirect to index
if len(parts) < 2 || parts[2] == "" {
IndexRedirect(res, req)
return
}
threadid, ok := util.GetURLPortion(req, 2)
loggedIn, userid := h.IsLoggedIn(req)

threadid, err := strconv.Atoi(parts[2])
if err != nil {
dump(err)
title := "Page not found"
if !ok {
title := "Thread not found"
data := GenericMessageData{
Title: title,
Message: "The visited page does not exist (anymore?)",
Message: "The thread does not exist (anymore?)",
}
h.renderView(res, "generic-message", TemplateData{Data: data, LoggedIn: loggedIn})
return
Expand All @@ -197,7 +191,7 @@ func (h RequestHandler) ThreadRoute(res http.ResponseWriter, req *http.Request)
thread[i].Content = util.Markup(post.Content)
}
title := thread[0].ThreadTitle
view := TemplateData{ThreadData{title, thread}, loggedIn, title}
view := TemplateData{Data: ThreadData{title, thread, req.URL.Path}, LoggedIn: loggedIn, LoggedInID: userid, Title: title}
h.renderView(res, "thread", view)
}

Expand All @@ -219,7 +213,7 @@ func (h RequestHandler) IndexRoute(res http.ResponseWriter, req *http.Request) {
loggedIn, _ := h.IsLoggedIn(req)
// show index listing
threads := h.db.ListThreads()
view := TemplateData{IndexData{threads}, loggedIn, "threads"}
view := TemplateData{Data: IndexData{threads}, LoggedIn: loggedIn, Title: "threads"}
h.renderView(res, "index", view)
}

Expand All @@ -240,7 +234,7 @@ func (h RequestHandler) LoginRoute(res http.ResponseWriter, req *http.Request) {
loggedIn, _ := h.IsLoggedIn(req)
switch req.Method {
case "GET":
h.renderView(res, "login", TemplateData{LoginData{}, loggedIn, ""})
h.renderView(res, "login", TemplateData{Data: LoginData{}, LoggedIn: loggedIn, Title: ""})
cblgh marked this conversation as resolved.
Show resolved Hide resolved
case "POST":
username := req.PostFormValue("username")
password := req.PostFormValue("password")
Expand All @@ -252,7 +246,7 @@ func (h RequestHandler) LoginRoute(res http.ResponseWriter, req *http.Request) {
}
if err != nil {
fmt.Println(err)
h.renderView(res, "login", TemplateData{LoginData{FailedAttempt: true}, loggedIn, ""})
h.renderView(res, "login", TemplateData{Data: LoginData{FailedAttempt: true}, LoggedIn: loggedIn, Title: ""})
return
}
// save user id in cookie
Expand Down Expand Up @@ -291,7 +285,7 @@ func (h RequestHandler) RegisterRoute(res http.ResponseWriter, req *http.Request
LinkMessage: "Visit the",
LinkText: "index",
}
h.renderView(res, "generic-message", TemplateData{data, loggedIn, "register"})
h.renderView(res, "generic-message", TemplateData{Data: data, LoggedIn: loggedIn, Title: "register"})
return
}

Expand Down Expand Up @@ -383,7 +377,7 @@ func (h RequestHandler) RegisterRoute(res http.ResponseWriter, req *http.Request
ed.Check(err, "generate keypair")
kpJson, err := keypair.Marshal()
ed.Check(err, "marshal keypair")
h.renderView(res, "register-success", TemplateData{RegisterSuccessData{string(kpJson)}, loggedIn, "registered successfully"})
h.renderView(res, "register-success", TemplateData{Data: RegisterSuccessData{string(kpJson)}, LoggedIn: loggedIn, Title: "registered successfully"})
default:
fmt.Println("non get/post method, redirecting to index")
IndexRedirect(res, req)
Expand Down Expand Up @@ -452,6 +446,59 @@ func (h RequestHandler) NewThreadRoute(res http.ResponseWriter, req *http.Reques
}
}

func (h RequestHandler) DeletePostRoute(res http.ResponseWriter, req *http.Request) {
if req.Method == "GET" {
IndexRedirect(res, req)
return
}
threadURL := req.PostFormValue("thread")
postid, ok := util.GetURLPortion(req, 3)
loggedIn, userid := h.IsLoggedIn(req)

// generic error message base, with specifics being swapped out depending on the error
genericErr := GenericMessageData{
Title: "Unaccepted request",
LinkMessage: "Go back to",
Link: threadURL,
LinkText: "the thread",
}

renderErr := func(msg string) {
fmt.Println(msg)
genericErr.Message = msg
h.renderView(res, "generic-message", TemplateData{Data: genericErr, LoggedIn: loggedIn})
}

if !loggedIn || !ok {
renderErr("Invalid post id, or you were not allowed to delete it")
return
}

post, err := h.db.GetPost(postid)
if err != nil {
dump(err)
renderErr("The post you tried to delete was not found")
return
}

authorized := post.AuthorID == userid
switch req.Method {
case "POST":
if authorized {
err = h.db.DeletePost(postid)
if err != nil {
dump(err)
renderErr("Error happened while deleting the post")
return
}
} else {
renderErr("That's not your post to delete? Sorry buddy!")
return
}
}
http.Redirect(res, req, threadURL, http.StatusSeeOther)
}

func Serve(allowlist []string, sessionKey string, isdev bool) {
port := ":8272"
dbpath := "./data/forum.db"
Expand All @@ -469,6 +516,7 @@ func Serve(allowlist []string, sessionKey string, isdev bool) {
http.HandleFunc("/logout", handler.LogoutRoute)
http.HandleFunc("/login", handler.LoginRoute)
http.HandleFunc("/register", handler.RegisterRoute)
http.HandleFunc("/post/delete/", handler.DeletePostRoute)
cblgh marked this conversation as resolved.
Show resolved Hide resolved
http.HandleFunc("/thread/new/", handler.NewThreadRoute)
http.HandleFunc("/thread/", handler.ThreadRoute)
http.HandleFunc("/robots.txt", handler.RobotsRoute)
Expand Down
17 changes: 17 additions & 0 deletions util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"fmt"
"html/template"
"log"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/gomarkdown/markdown"
Expand Down Expand Up @@ -98,3 +100,18 @@ func SanitizeURL(input string) string {
// TODO(2022-01-08): evaluate use of strict content guardian?
return strings.ToLower(input)
}

// returns an id from a url path, and a boolean. the boolean is true if we're returning what we expect; false if the
// operation failed
func GetURLPortion(req *http.Request, index int) (int, bool) {
var desiredID int
parts := strings.Split(strings.TrimSpace(req.URL.Path), "/")
if len(parts) < index || parts[index] == "" {
return -1, false
}
desiredID, err := strconv.Atoi(parts[index])
if err != nil {
return -1, false
}
return desiredID, true
}