add tree formatter

This commit is contained in:
Umputun
2017-12-25 20:27:27 -06:00
parent e5c2af6d72
commit 880192012b
4 changed files with 392 additions and 3 deletions
+18 -1
View File
@@ -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}`
+76
View File
@@ -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
}
+292
View File
@@ -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
View File
@@ -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)
}