add tree formatter
This commit is contained in:
@@ -65,7 +65,24 @@ type Locator struct {
|
||||
}
|
||||
```
|
||||
|
||||
- `GET /api/v1/find?url=post-url` - find all comments for given post, returns flat list of `Comment`
|
||||
- `GET /api/v1/find?url=post-url&sort=fld&format=tree` - find all comments for given post
|
||||
|
||||
This is the primary call used by UI to show comments for given post. It can return two formats - `plain` and `tree`. In plain
|
||||
format result will be sorted list of `Comment`. In tree format this is going to be tree-like structure with this structure:
|
||||
|
||||
```go
|
||||
type Tree struct {
|
||||
Nodes []Node `json:"comments"`
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
Comment store.Comment `json:"comment"`
|
||||
Replies []Node `json:"replies,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
Sort can be `time` or `score`. Supported sort order with prefix -/+, i.e. `-time`. For `tree` mode sort will be applied to top-level comments only and all replies always sorted by time.
|
||||
|
||||
- `GET /api/v1/last/{max}` - get up to `{max}` last comments
|
||||
- `GET /api/v1/id/{id}` - get comment by `id`
|
||||
- `GET /api/v1/count?url=post-url` - get comment's count for `{url}`
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/umputun/remark/app/store"
|
||||
)
|
||||
|
||||
// Tree is formatter as comment tree list of comments
|
||||
type Tree struct {
|
||||
Nodes []*Node `json:"comments"`
|
||||
}
|
||||
|
||||
// Node is a comment with optional replies
|
||||
type Node struct {
|
||||
Comment store.Comment `json:"comment"`
|
||||
Replies []*Node `json:"replies,omitempty"`
|
||||
}
|
||||
|
||||
// MakeTree gets unsorted list of comments and produces Tree
|
||||
func MakeTree(comments []store.Comment, sortType string) (res Tree) {
|
||||
res = Tree{}
|
||||
|
||||
repComments := res.filter(comments, func(c store.Comment) bool { return c.ParentID == "" })
|
||||
for _, rc := range repComments {
|
||||
node := Node{Comment: rc}
|
||||
res.Nodes = append(res.Nodes, res.proc(comments, &node, rc.ID))
|
||||
}
|
||||
|
||||
// sort result according to sortType
|
||||
sort.Slice(res.Nodes, func(i, j int) bool {
|
||||
switch sortType {
|
||||
case "+time", "-time", "time":
|
||||
if strings.HasPrefix(sortType, "-") {
|
||||
return res.Nodes[i].Comment.Timestamp.After(res.Nodes[j].Comment.Timestamp)
|
||||
}
|
||||
return res.Nodes[i].Comment.Timestamp.Before(res.Nodes[j].Comment.Timestamp)
|
||||
|
||||
case "+score", "-score", "score":
|
||||
if strings.HasPrefix(sortType, "-") {
|
||||
return res.Nodes[i].Comment.Score > res.Nodes[j].Comment.Score
|
||||
}
|
||||
return res.Nodes[i].Comment.Score < res.Nodes[j].Comment.Score
|
||||
|
||||
default:
|
||||
return res.Nodes[i].Comment.Timestamp.Before(res.Nodes[j].Comment.Timestamp)
|
||||
}
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func (t *Tree) proc(comments []store.Comment, node *Node, parentID string) *Node {
|
||||
repComments := t.filter(comments, func(c store.Comment) bool { return c.ParentID == parentID })
|
||||
for _, rc := range repComments {
|
||||
rnode := &Node{Comment: rc, Replies: []*Node{}}
|
||||
node.Replies = append(node.Replies, rnode)
|
||||
|
||||
// replies always sorted by time
|
||||
sort.Slice(node.Replies, func(i, j int) bool {
|
||||
return node.Replies[i].Comment.Timestamp.Before(node.Replies[j].Comment.Timestamp)
|
||||
})
|
||||
t.proc(comments, rnode, rc.ID)
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
func (t *Tree) filter(comments []store.Comment, fn func(c store.Comment) bool) (f []store.Comment) {
|
||||
for _, c := range comments {
|
||||
if fn(c) {
|
||||
f = append(f, c)
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/umputun/remark/app/store"
|
||||
)
|
||||
|
||||
func TestStore_MakeTree(t *testing.T) {
|
||||
|
||||
// unsorted by purpose
|
||||
comments := []store.Comment{
|
||||
{ID: "14", ParentID: "1", Timestamp: time.Date(2017, 12, 25, 19, 46, 14, 0, time.UTC)},
|
||||
{ID: "1", Timestamp: time.Date(2017, 12, 25, 19, 46, 1, 0, time.UTC)},
|
||||
{ID: "2", Timestamp: time.Date(2017, 12, 25, 19, 47, 2, 0, time.UTC)},
|
||||
{ID: "11", ParentID: "1", Timestamp: time.Date(2017, 12, 25, 19, 46, 11, 0, time.UTC)},
|
||||
{ID: "13", ParentID: "1", Timestamp: time.Date(2017, 12, 25, 19, 46, 13, 0, time.UTC)},
|
||||
{ID: "12", ParentID: "1", Timestamp: time.Date(2017, 12, 25, 19, 46, 12, 0, time.UTC)},
|
||||
{ID: "131", ParentID: "13", Timestamp: time.Date(2017, 12, 25, 19, 46, 31, 0, time.UTC)},
|
||||
{ID: "132", ParentID: "13", Timestamp: time.Date(2017, 12, 25, 19, 46, 32, 0, time.UTC)},
|
||||
{ID: "21", ParentID: "2", Timestamp: time.Date(2017, 12, 25, 19, 47, 21, 0, time.UTC)},
|
||||
{ID: "22", ParentID: "2", Timestamp: time.Date(2017, 12, 25, 19, 47, 22, 0, time.UTC)},
|
||||
{ID: "4", Timestamp: time.Date(2017, 12, 25, 19, 47, 22, 0, time.UTC)},
|
||||
{ID: "3", Timestamp: time.Date(2017, 12, 25, 19, 47, 22, 0, time.UTC)},
|
||||
}
|
||||
|
||||
res := MakeTree(comments, "time")
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetIndent("", " ")
|
||||
err := enc.Encode(res)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expJSON, string(buf.Bytes()))
|
||||
t.Log(string(buf.Bytes()))
|
||||
}
|
||||
|
||||
const expJSON = `{
|
||||
"comments": [
|
||||
{
|
||||
"comment": {
|
||||
"id": "1",
|
||||
"pid": "",
|
||||
"text": "",
|
||||
"user": {
|
||||
"name": "",
|
||||
"id": "",
|
||||
"picture": "",
|
||||
"profile": "",
|
||||
"admin": false
|
||||
},
|
||||
"locator": {
|
||||
"url": ""
|
||||
},
|
||||
"score": 0,
|
||||
"votes": null,
|
||||
"time": "2017-12-25T19:46:01Z"
|
||||
},
|
||||
"replies": [
|
||||
{
|
||||
"comment": {
|
||||
"id": "11",
|
||||
"pid": "1",
|
||||
"text": "",
|
||||
"user": {
|
||||
"name": "",
|
||||
"id": "",
|
||||
"picture": "",
|
||||
"profile": "",
|
||||
"admin": false
|
||||
},
|
||||
"locator": {
|
||||
"url": ""
|
||||
},
|
||||
"score": 0,
|
||||
"votes": null,
|
||||
"time": "2017-12-25T19:46:11Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": {
|
||||
"id": "12",
|
||||
"pid": "1",
|
||||
"text": "",
|
||||
"user": {
|
||||
"name": "",
|
||||
"id": "",
|
||||
"picture": "",
|
||||
"profile": "",
|
||||
"admin": false
|
||||
},
|
||||
"locator": {
|
||||
"url": ""
|
||||
},
|
||||
"score": 0,
|
||||
"votes": null,
|
||||
"time": "2017-12-25T19:46:12Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": {
|
||||
"id": "13",
|
||||
"pid": "1",
|
||||
"text": "",
|
||||
"user": {
|
||||
"name": "",
|
||||
"id": "",
|
||||
"picture": "",
|
||||
"profile": "",
|
||||
"admin": false
|
||||
},
|
||||
"locator": {
|
||||
"url": ""
|
||||
},
|
||||
"score": 0,
|
||||
"votes": null,
|
||||
"time": "2017-12-25T19:46:13Z"
|
||||
},
|
||||
"replies": [
|
||||
{
|
||||
"comment": {
|
||||
"id": "131",
|
||||
"pid": "13",
|
||||
"text": "",
|
||||
"user": {
|
||||
"name": "",
|
||||
"id": "",
|
||||
"picture": "",
|
||||
"profile": "",
|
||||
"admin": false
|
||||
},
|
||||
"locator": {
|
||||
"url": ""
|
||||
},
|
||||
"score": 0,
|
||||
"votes": null,
|
||||
"time": "2017-12-25T19:46:31Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": {
|
||||
"id": "132",
|
||||
"pid": "13",
|
||||
"text": "",
|
||||
"user": {
|
||||
"name": "",
|
||||
"id": "",
|
||||
"picture": "",
|
||||
"profile": "",
|
||||
"admin": false
|
||||
},
|
||||
"locator": {
|
||||
"url": ""
|
||||
},
|
||||
"score": 0,
|
||||
"votes": null,
|
||||
"time": "2017-12-25T19:46:32Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": {
|
||||
"id": "14",
|
||||
"pid": "1",
|
||||
"text": "",
|
||||
"user": {
|
||||
"name": "",
|
||||
"id": "",
|
||||
"picture": "",
|
||||
"profile": "",
|
||||
"admin": false
|
||||
},
|
||||
"locator": {
|
||||
"url": ""
|
||||
},
|
||||
"score": 0,
|
||||
"votes": null,
|
||||
"time": "2017-12-25T19:46:14Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": {
|
||||
"id": "2",
|
||||
"pid": "",
|
||||
"text": "",
|
||||
"user": {
|
||||
"name": "",
|
||||
"id": "",
|
||||
"picture": "",
|
||||
"profile": "",
|
||||
"admin": false
|
||||
},
|
||||
"locator": {
|
||||
"url": ""
|
||||
},
|
||||
"score": 0,
|
||||
"votes": null,
|
||||
"time": "2017-12-25T19:47:02Z"
|
||||
},
|
||||
"replies": [
|
||||
{
|
||||
"comment": {
|
||||
"id": "21",
|
||||
"pid": "2",
|
||||
"text": "",
|
||||
"user": {
|
||||
"name": "",
|
||||
"id": "",
|
||||
"picture": "",
|
||||
"profile": "",
|
||||
"admin": false
|
||||
},
|
||||
"locator": {
|
||||
"url": ""
|
||||
},
|
||||
"score": 0,
|
||||
"votes": null,
|
||||
"time": "2017-12-25T19:47:21Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": {
|
||||
"id": "22",
|
||||
"pid": "2",
|
||||
"text": "",
|
||||
"user": {
|
||||
"name": "",
|
||||
"id": "",
|
||||
"picture": "",
|
||||
"profile": "",
|
||||
"admin": false
|
||||
},
|
||||
"locator": {
|
||||
"url": ""
|
||||
},
|
||||
"score": 0,
|
||||
"votes": null,
|
||||
"time": "2017-12-25T19:47:22Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": {
|
||||
"id": "4",
|
||||
"pid": "",
|
||||
"text": "",
|
||||
"user": {
|
||||
"name": "",
|
||||
"id": "",
|
||||
"picture": "",
|
||||
"profile": "",
|
||||
"admin": false
|
||||
},
|
||||
"locator": {
|
||||
"url": ""
|
||||
},
|
||||
"score": 0,
|
||||
"votes": null,
|
||||
"time": "2017-12-25T19:47:22Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"comment": {
|
||||
"id": "3",
|
||||
"pid": "",
|
||||
"text": "",
|
||||
"user": {
|
||||
"name": "",
|
||||
"id": "",
|
||||
"picture": "",
|
||||
"profile": "",
|
||||
"admin": false
|
||||
},
|
||||
"locator": {
|
||||
"url": ""
|
||||
},
|
||||
"score": 0,
|
||||
"votes": null,
|
||||
"time": "2017-12-25T19:47:22Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
+6
-2
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
"github.com/umputun/remark/app/migrator"
|
||||
"github.com/umputun/remark/app/rest/auth"
|
||||
"github.com/umputun/remark/app/rest/format"
|
||||
"github.com/umputun/remark/app/store"
|
||||
)
|
||||
|
||||
@@ -159,7 +160,7 @@ func (s *Server) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, JSON{"id": id, "url": url})
|
||||
}
|
||||
|
||||
// GET /find?url=post-url&sort=-time
|
||||
// GET /find?url=post-url&format=tree&sort=-time
|
||||
func (s *Server) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
url := r.URL.Query().Get("url")
|
||||
log.Printf("[DEBUG] get comments for %s", url)
|
||||
@@ -170,7 +171,10 @@ func (s *Server) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
httpError(w, r, http.StatusInternalServerError, err, "can't load comments comment")
|
||||
return
|
||||
}
|
||||
render.Status(r, http.StatusOK)
|
||||
if r.URL.Query().Get("format") == "tree" {
|
||||
renderJSONWithHTML(w, r, format.MakeTree(comments, r.URL.Query().Get("sort")))
|
||||
return
|
||||
}
|
||||
renderJSONWithHTML(w, r, comments)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user