diff --git a/app/rest/api/rest.go b/app/rest/api/rest.go index 1866db56..821ff33c 100644 --- a/app/rest/api/rest.go +++ b/app/rest/api/rest.go @@ -444,11 +444,16 @@ func (s *Rest) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) { log.Printf("[DEBUG] get comments for userID %s, %s", userID, siteID) data, err := s.Cache.Get(cache.Key(cache.URLKey(r), userID, siteID), func() ([]byte, error) { - comments, count, e := s.DataService.User(siteID, userID, limit, 0) + comments, e := s.DataService.User(siteID, userID, limit, 0) if e != nil { return nil, e } comments = s.adminService.alterComments(comments, r) + + count, e := s.DataService.UserCount(siteID, userID) + if e != nil { + return nil, e + } resp.Comments, resp.Count = comments, count return encodeJSONWithHTML(resp) }) diff --git a/app/store/engine/bolt_accessor.go b/app/store/engine/bolt_accessor.go index e7673ade..62a24fee 100644 --- a/app/store/engine/bolt_accessor.go +++ b/app/store/engine/bolt_accessor.go @@ -294,14 +294,14 @@ func (b *BoltDB) Info(locator store.Locator, readOnlyAge int) (store.PostInfo, e // User extracts all comments for given site and given userID // "users" bucket has sub-bucket for each userID, and keeps it as ts:ref -func (b *BoltDB) User(siteID, userID string, limit, skip int) (comments []store.Comment, totalComments int, err error) { +func (b *BoltDB) User(siteID, userID string, limit, skip int) (comments []store.Comment, err error) { comments = []store.Comment{} commentRefs := []string{} bdb, err := b.db(siteID) if err != nil { - return nil, 0, err + return nil, err } if limit == 0 || limit > userLimit { @@ -317,37 +317,56 @@ func (b *BoltDB) User(siteID, userID string, limit, skip int) (comments []store. } c := userIDBkt.Cursor() - totalComments = 0 skipComments := 0 for k, v := c.Last(); k != nil; k, v = c.Prev() { - totalComments++ + if len(commentRefs) >= limit { + break + } if skip > 0 && skipComments < skip { skipComments++ continue } - if len(commentRefs) < limit { - commentRefs = append(commentRefs, string(v)) - } + commentRefs = append(commentRefs, string(v)) } return nil }) if err != nil { - return comments, totalComments, err + return comments, err } // retrieve comments for refs for _, v := range commentRefs { url, commentID, e := b.parseRef([]byte(v)) if e != nil { - return comments, totalComments, errors.Wrapf(e, "can't parse reference %s", v) + return comments, errors.Wrapf(e, "can't parse reference %s", v) } if c, e := b.Get(store.Locator{SiteID: siteID, URL: url}, commentID); e == nil { comments = append(comments, c) } } - return comments, totalComments, err + return comments, err +} + +// UserCount returns number of comments for user TODO: this can be slow, but userIDBkt just refs +func (b *BoltDB) UserCount(siteID, userID string) (int, error) { + bdb, err := b.db(siteID) + if err != nil { + return 0, err + } + count := 0 + err = bdb.View(func(tx *bolt.Tx) error { + usersBkt := tx.Bucket([]byte(userBucketName)) + userIDBkt := usersBkt.Bucket([]byte(userID)) + if userIDBkt == nil { + return errors.Errorf("no comments for user %s in store", userID) + } + stats := userIDBkt.Stats() + count = stats.KeyN + return nil + }) + return count, err } // Get returns comment for locator.URL and commentID string diff --git a/app/store/engine/bolt_accessor_test.go b/app/store/engine/bolt_accessor_test.go index 6cfea02f..c23081f9 100644 --- a/app/store/engine/bolt_accessor_test.go +++ b/app/store/engine/bolt_accessor_test.go @@ -225,28 +225,25 @@ func TestBoltDB_GetForUser(t *testing.T) { defer os.Remove(testDb) b := prep(t) - res, count, err := b.User("radio-t", "user1", 5, 0) + res, err := b.User("radio-t", "user1", 5, 0) assert.Nil(t, err) assert.Equal(t, 2, len(res)) - assert.Equal(t, 2, count) assert.Equal(t, "some text2", res[0].Text, "sorted by -time") - res, count, err = b.User("radio-t", "user1", 1, 0) + res, err = b.User("radio-t", "user1", 1, 0) assert.Nil(t, err) assert.Equal(t, 1, len(res), "allow 1 comment") - assert.Equal(t, 2, count) assert.Equal(t, "some text2", res[0].Text, "sorted by -time") - res, count, err = b.User("radio-t", "user1", 1, 1) + res, err = b.User("radio-t", "user1", 1, 1) assert.Nil(t, err) assert.Equal(t, 1, len(res), "allow 1 comment") - assert.Equal(t, 2, count) assert.Equal(t, `some text, link`, res[0].Text, "second comment") - _, _, err = b.User("bad", "user1", 1, 0) + _, err = b.User("bad", "user1", 1, 0) assert.EqualError(t, err, `site "bad" not found`) - _, _, err = b.User("radio-t", "userZ", 1, 0) + _, err = b.User("radio-t", "userZ", 1, 0) assert.EqualError(t, err, `no comments for user userZ in store`) } @@ -271,36 +268,46 @@ func TestBoltDB_GetForUserPagination(t *testing.T) { } // seek 0, 5 comments - res, count, err := b.User("radio-t", "user1", 5, 0) + res, err := b.User("radio-t", "user1", 5, 0) assert.Nil(t, err) - assert.Equal(t, 50, count) assert.Equal(t, 5, len(res)) assert.Equal(t, "id-49", res[0].ID) assert.Equal(t, "id-45", res[4].ID) // seek 10, 3 comments - res, count, err = b.User("radio-t", "user1", 3, 10) + res, err = b.User("radio-t", "user1", 3, 10) assert.Nil(t, err) - assert.Equal(t, 50, count) assert.Equal(t, 3, len(res)) assert.Equal(t, "id-39", res[0].ID) assert.Equal(t, "id-37", res[2].ID) // seek 45, ask 10 comments - res, count, err = b.User("radio-t", "user1", 10, 45) + res, err = b.User("radio-t", "user1", 10, 45) assert.Nil(t, err) - assert.Equal(t, 50, count) assert.Equal(t, 5, len(res)) assert.Equal(t, "id-4", res[0].ID) assert.Equal(t, "id-0", res[4].ID) // seek 55, ask 10 comments - res, count, err = b.User("radio-t", "user1", 10, 55) + res, err = b.User("radio-t", "user1", 10, 55) assert.Nil(t, err) - assert.Equal(t, 50, count) assert.Equal(t, 0, len(res)) } +func TestBoltDB_GetForUserCounter(t *testing.T) { + defer os.Remove(testDb) + b := prep(t) + count, err := b.UserCount("radio-t", "user1") + assert.Nil(t, err) + assert.Equal(t, 2, count) + + _, err = b.UserCount("bad", "user1") + assert.EqualError(t, err, `site "bad" not found`) + + _, err = b.UserCount("radio-t", "userZ") + assert.EqualError(t, err, `no comments for user userZ in store`) +} + func TestBoltDB_Ref(t *testing.T) { b := BoltDB{} comment := store.Comment{ diff --git a/app/store/engine/bolt_admin.go b/app/store/engine/bolt_admin.go index e8a25a17..703a2b05 100644 --- a/app/store/engine/bolt_admin.go +++ b/app/store/engine/bolt_admin.go @@ -206,7 +206,7 @@ func (b *BoltDB) Blocked(siteID string) (users []store.BlockedUser, err error) { // get user name from comment user section userName := "" - userComments, _, e := b.User(siteID, string(k), 1, 0) + userComments, e := b.User(siteID, string(k), 1, 0) if e == nil && len(userComments) > 0 { userName = userComments[0].User.Name } diff --git a/app/store/engine/bolt_admin_test.go b/app/store/engine/bolt_admin_test.go index 1fe9a98e..e9f710cf 100644 --- a/app/store/engine/bolt_admin_test.go +++ b/app/store/engine/bolt_admin_test.go @@ -109,7 +109,7 @@ func TestBoltAdmin_DeleteUser(t *testing.T) { assert.Nil(t, err) assert.Equal(t, 0, c, "0 count") - _, _, err = b.User("radio-t", "user1", 5, 0) + _, err = b.User("radio-t", "user1", 5, 0) assert.EqualError(t, err, "no comments for user user1 in store") comments, err := b.Last("radio-t", 10) diff --git a/app/store/engine/engine.go b/app/store/engine/engine.go index 05e4ebb6..2c191433 100644 --- a/app/store/engine/engine.go +++ b/app/store/engine/engine.go @@ -27,15 +27,16 @@ type UserRequest struct { // Accessor defines all usual access ops avail for regular user type Accessor interface { - Create(comment store.Comment) (commentID string, err error) // create new comment, avoid dups by id - Get(locator store.Locator, commentID string) (store.Comment, error) // get comment by id - Put(locator store.Locator, comment store.Comment) error // update comment, mutable parts only - Find(locator store.Locator, sort string) ([]store.Comment, error) // find comments for locator - Last(siteID string, limit int) ([]store.Comment, error) // last comments for given site, sorted by time - User(siteID, userID string, limit, skip int) ([]store.Comment, int, error) // comments by user, sorted by time - Count(locator store.Locator) (int, error) // number of comments for the post - List(siteID string, limit int, skip int) ([]store.PostInfo, error) // list of commented posts - Info(locator store.Locator, readonlyAge int) (store.PostInfo, error) // get post info + Create(comment store.Comment) (commentID string, err error) // create new comment, avoid dups by id + Get(locator store.Locator, commentID string) (store.Comment, error) // get comment by id + Put(locator store.Locator, comment store.Comment) error // update comment, mutable parts only + Find(locator store.Locator, sort string) ([]store.Comment, error) // find comments for locator + Last(siteID string, limit int) ([]store.Comment, error) // last comments for given site, sorted by time + User(siteID, userID string, limit, skip int) ([]store.Comment, error) // comments by user, sorted by time + UserCount(siteID, userID string) (int, error) // comments count by user + Count(locator store.Locator) (int, error) // number of comments for the post + List(siteID string, limit int, skip int) ([]store.PostInfo, error) // list of commented posts + Info(locator store.Locator, readonlyAge int) (store.PostInfo, error) // get post info } // Admin defines all store ops avail for admin only