From 188182a3a4499965dbbde254aed0ae5e60e71dc5 Mon Sep 17 00:00:00 2001 From: Christopher Speller Date: Tue, 3 Nov 2015 11:34:52 -0500 Subject: Adding ability to fetch posts around a specifc post --- api/post.go | 66 +++++++++++++++++++++++++++ api/post_test.go | 77 +++++++++++++++++++++++++++++++ model/client.go | 18 ++++++++ store/sql_post_store.go | 98 ++++++++++++++++++++++++++++++++++++++++ store/sql_post_store_test.go | 105 +++++++++++++++++++++++++++++++++++++++++++ store/store.go | 2 + 6 files changed, 366 insertions(+) diff --git a/api/post.go b/api/post.go index c98bf2d71..31a7ab3b5 100644 --- a/api/post.go +++ b/api/post.go @@ -31,6 +31,8 @@ func InitPost(r *mux.Router) { sr.Handle("/posts/{time:[0-9]+}", ApiUserRequiredActivity(getPostsSince, false)).Methods("GET") sr.Handle("/post/{post_id:[A-Za-z0-9]+}", ApiUserRequired(getPost)).Methods("GET") sr.Handle("/post/{post_id:[A-Za-z0-9]+}/delete", ApiUserRequired(deletePost)).Methods("POST") + sr.Handle("/post/{post_id:[A-Za-z0-9]+}/before/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsBefore)).Methods("GET") + sr.Handle("/post/{post_id:[A-Za-z0-9]+}/after/{offset:[0-9]+}/{num_posts:[0-9]+}", ApiUserRequired(getPostsAfter)).Methods("GET") } func createPost(c *Context, w http.ResponseWriter, r *http.Request) { @@ -812,6 +814,70 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) { } } +func getPostsBefore(c *Context, w http.ResponseWriter, r *http.Request) { + getPostsBeforeOrAfter(c, w, r, true) +} +func getPostsAfter(c *Context, w http.ResponseWriter, r *http.Request) { + getPostsBeforeOrAfter(c, w, r, false) +} +func getPostsBeforeOrAfter(c *Context, w http.ResponseWriter, r *http.Request, before bool) { + params := mux.Vars(r) + + id := params["id"] + if len(id) != 26 { + c.SetInvalidParam("getPostsBeforeOrAfter", "channelId") + return + } + + postId := params["post_id"] + if len(postId) != 26 { + c.SetInvalidParam("getPostsBeforeOrAfter", "postId") + return + } + + numPosts, err := strconv.Atoi(params["num_posts"]) + if err != nil || numPosts <= 0 { + c.SetInvalidParam("getPostsBeforeOrAfter", "numPosts") + return + } + + offset, err := strconv.Atoi(params["offset"]) + if err != nil || offset < 0 { + c.SetInvalidParam("getPostsBeforeOrAfter", "offset") + return + } + + cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, id, c.Session.UserId) + // We can do better than this etag in this situation + etagChan := Srv.Store.Post().GetEtag(id) + + if !c.HasPermissionsToChannel(cchan, "getPostsBeforeOrAfter") { + return + } + + etag := (<-etagChan).Data.(string) + if HandleEtag(etag, w, r) { + return + } + + var pchan store.StoreChannel + if before { + pchan = Srv.Store.Post().GetPostsBefore(id, postId, numPosts, offset) + } else { + pchan = Srv.Store.Post().GetPostsAfter(id, postId, numPosts, offset) + } + + if result := <-pchan; result.Err != nil { + c.Err = result.Err + return + } else { + list := result.Data.(*model.PostList) + + w.Header().Set(model.HEADER_ETAG_SERVER, etag) + w.Write([]byte(list.ToJson())) + } +} + func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) { terms := r.FormValue("terms") diff --git a/api/post_test.go b/api/post_test.go index e54e9ef0c..3452c9788 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -329,6 +329,83 @@ func TestGetPostsSince(t *testing.T) { } } +func TestGetPostsBeforeAfter(t *testing.T) { + Setup() + + team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user1 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} + user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User) + store.Must(Srv.Store.User().VerifyEmail(user1.Id)) + + Client.LoginByEmail(team.Name, user1.Email, "pwd") + + channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} + channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel) + + time.Sleep(10 * time.Millisecond) + post0 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post0 = Client.Must(Client.CreatePost(post0)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post1a1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: post1.Id} + post1a1 = Client.Must(Client.CreatePost(post1a1)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post2 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post3 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"} + post3 = Client.Must(Client.CreatePost(post3)).Data.(*model.Post) + + time.Sleep(10 * time.Millisecond) + post3a1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: post3.Id} + post3a1 = Client.Must(Client.CreatePost(post3a1)).Data.(*model.Post) + + r1 := Client.Must(Client.GetPostsBefore(channel1.Id, post1a1.Id, 0, 10, "")).Data.(*model.PostList) + + if r1.Order[0] != post1.Id { + t.Fatal("wrong order") + } + + if r1.Order[1] != post0.Id { + t.Fatal("wrong order") + } + + if len(r1.Posts) != 2 { + t.Fatal("wrong size") + } + + r2 := Client.Must(Client.GetPostsAfter(channel1.Id, post3a1.Id, 0, 3, "")).Data.(*model.PostList) + + if len(r2.Posts) != 0 { + t.Fatal("should have been empty") + } + + post2.Message = "new message" + Client.Must(Client.UpdatePost(post2)) + + r3 := Client.Must(Client.GetPostsAfter(channel1.Id, post1a1.Id, 0, 2, "")).Data.(*model.PostList) + + if r3.Order[0] != post3.Id { + t.Fatal("wrong order") + } + + if r3.Order[1] != post2.Id { + t.Fatal("wrong order") + } + + if len(r3.Order) != 2 { + t.Fatal("missing post update") + } +} + func TestSearchPosts(t *testing.T) { Setup() diff --git a/model/client.go b/model/client.go index 19183098e..a15cb5eaf 100644 --- a/model/client.go +++ b/model/client.go @@ -618,6 +618,24 @@ func (c *Client) GetPostsSince(channelId string, time int64) (*Result, *AppError } } +func (c *Client) GetPostsBefore(channelId string, postid string, offset int, limit int, etag string) (*Result, *AppError) { + if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/post/%v/before/%v/%v", channelId, postid, offset, limit), "", etag); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil + } +} + +func (c *Client) GetPostsAfter(channelId string, postid string, offset int, limit int, etag string) (*Result, *AppError) { + if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/post/%v/after/%v/%v", channelId, postid, offset, limit), "", etag); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), PostListFromJson(r.Body)}, nil + } +} + func (c *Client) GetPost(channelId string, postId string, etag string) (*Result, *AppError) { if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/post/%v", channelId, postId), "", etag); err != nil { return nil, err diff --git a/store/sql_post_store.go b/store/sql_post_store.go index f514d23ed..8b1cae3dd 100644 --- a/store/sql_post_store.go +++ b/store/sql_post_store.go @@ -332,6 +332,104 @@ func (s SqlPostStore) GetPostsSince(channelId string, time int64) StoreChannel { return storeChannel } +func (s SqlPostStore) GetPostsBefore(channelId string, postId string, numPosts int, offset int) StoreChannel { + return s.getPostsAround(channelId, postId, numPosts, offset, true) +} + +func (s SqlPostStore) GetPostsAfter(channelId string, postId string, numPosts int, offset int) StoreChannel { + return s.getPostsAround(channelId, postId, numPosts, offset, false) +} + +func (s SqlPostStore) getPostsAround(channelId string, postId string, numPosts int, offset int, before bool) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var direction string + var sort string + if before { + direction = "<" + sort = "DESC" + } else { + direction = ">" + sort = "ASC" + } + + var posts []*model.Post + var parents []*model.Post + _, err1 := s.GetReplica().Select(&posts, + `(SELECT + * + FROM + Posts + WHERE + (CreateAt `+direction+` (SELECT CreateAt FROM Posts WHERE Id = :PostId) + AND ChannelId = :ChannelId + AND DeleteAt = 0) + ORDER BY CreateAt `+sort+` + LIMIT :NumPosts + OFFSET :Offset)`, + map[string]interface{}{"ChannelId": channelId, "PostId": postId, "NumPosts": numPosts, "Offset": offset}) + _, err2 := s.GetReplica().Select(&parents, + `(SELECT + * + FROM + Posts + WHERE + Id + IN + (SELECT * FROM (SELECT + RootId + FROM + Posts + WHERE + (CreateAt `+direction+` (SELECT CreateAt FROM Posts WHERE Id = :PostId) + AND ChannelId = :ChannelId + AND DeleteAt = 0) + ORDER BY CreateAt `+sort+` + LIMIT :NumPosts + OFFSET :Offset) + temp_tab)) + ORDER BY CreateAt DESC`, + map[string]interface{}{"ChannelId": channelId, "PostId": postId, "NumPosts": numPosts, "Offset": offset}) + + if err1 != nil { + result.Err = model.NewAppError("SqlPostStore.GetPostContext", "We couldn't get the posts for the channel", "channelId="+channelId+err1.Error()) + } else if err2 != nil { + result.Err = model.NewAppError("SqlPostStore.GetPostContext", "We couldn't get the parent posts for the channel", "channelId="+channelId+err2.Error()) + } else { + + list := &model.PostList{Order: make([]string, 0, len(posts))} + + // We need to flip the order if we selected backwards + if before { + for _, p := range posts { + list.AddPost(p) + list.AddOrder(p.Id) + } + } else { + l := len(posts) + for i := range posts { + list.AddPost(posts[l-i-1]) + list.AddOrder(posts[l-i-1].Id) + } + } + + for _, p := range parents { + list.AddPost(p) + } + + result.Data = list + } + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (s SqlPostStore) getRootPosts(channelId string, offset int, limit int) StoreChannel { storeChannel := make(StoreChannel) diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go index 872423c5a..c2348b04c 100644 --- a/store/sql_post_store_test.go +++ b/store/sql_post_store_test.go @@ -383,6 +383,111 @@ func TestPostStoreGetPostsWtihDetails(t *testing.T) { } } +func TestPostStoreGetPostsBeforeAfter(t *testing.T) { + Setup() + o0 := &model.Post{} + o0.ChannelId = model.NewId() + o0.UserId = model.NewId() + o0.Message = "a" + model.NewId() + "b" + o0 = (<-store.Post().Save(o0)).Data.(*model.Post) + time.Sleep(2 * time.Millisecond) + + o1 := &model.Post{} + o1.ChannelId = model.NewId() + o1.UserId = model.NewId() + o1.Message = "a" + model.NewId() + "b" + o1 = (<-store.Post().Save(o1)).Data.(*model.Post) + time.Sleep(2 * time.Millisecond) + + o2 := &model.Post{} + o2.ChannelId = o1.ChannelId + o2.UserId = model.NewId() + o2.Message = "a" + model.NewId() + "b" + o2.ParentId = o1.Id + o2.RootId = o1.Id + o2 = (<-store.Post().Save(o2)).Data.(*model.Post) + time.Sleep(2 * time.Millisecond) + + o2a := &model.Post{} + o2a.ChannelId = o1.ChannelId + o2a.UserId = model.NewId() + o2a.Message = "a" + model.NewId() + "b" + o2a.ParentId = o1.Id + o2a.RootId = o1.Id + o2a = (<-store.Post().Save(o2a)).Data.(*model.Post) + time.Sleep(2 * time.Millisecond) + + o3 := &model.Post{} + o3.ChannelId = o1.ChannelId + o3.UserId = model.NewId() + o3.Message = "a" + model.NewId() + "b" + o3.ParentId = o1.Id + o3.RootId = o1.Id + o3 = (<-store.Post().Save(o3)).Data.(*model.Post) + time.Sleep(2 * time.Millisecond) + + o4 := &model.Post{} + o4.ChannelId = o1.ChannelId + o4.UserId = model.NewId() + o4.Message = "a" + model.NewId() + "b" + o4 = (<-store.Post().Save(o4)).Data.(*model.Post) + time.Sleep(2 * time.Millisecond) + + o5 := &model.Post{} + o5.ChannelId = o1.ChannelId + o5.UserId = model.NewId() + o5.Message = "a" + model.NewId() + "b" + o5.ParentId = o4.Id + o5.RootId = o4.Id + o5 = (<-store.Post().Save(o5)).Data.(*model.Post) + + r1 := (<-store.Post().GetPostsBefore(o1.ChannelId, o1.Id, 4, 0)).Data.(*model.PostList) + + if len(r1.Posts) != 0 { + t.Fatal("Wrong size") + } + + r2 := (<-store.Post().GetPostsAfter(o1.ChannelId, o1.Id, 4, 0)).Data.(*model.PostList) + + if r2.Order[0] != o4.Id { + t.Fatal("invalid order") + } + + if r2.Order[1] != o3.Id { + t.Fatal("invalid order") + } + + if r2.Order[2] != o2a.Id { + t.Fatal("invalid order") + } + + if r2.Order[3] != o2.Id { + t.Fatal("invalid order") + } + + if len(r2.Posts) != 5 { + t.Fatal("wrong size") + } + + r3 := (<-store.Post().GetPostsBefore(o3.ChannelId, o3.Id, 2, 0)).Data.(*model.PostList) + + if r3.Order[0] != o2a.Id { + t.Fatal("invalid order") + } + + if r3.Order[1] != o2.Id { + t.Fatal("invalid order") + } + + if len(r3.Posts) != 3 { + t.Fatal("wrong size") + } + + if r3.Posts[o1.Id].Message != o1.Message { + t.Fatal("Missing parent") + } +} + func TestPostStoreGetPostsSince(t *testing.T) { Setup() o0 := &model.Post{} diff --git a/store/store.go b/store/store.go index 53a6e053b..ce4d90883 100644 --- a/store/store.go +++ b/store/store.go @@ -86,6 +86,8 @@ type PostStore interface { Get(id string) StoreChannel Delete(postId string, time int64) StoreChannel GetPosts(channelId string, offset int, limit int) StoreChannel + GetPostsBefore(channelId string, postId string, numPosts int, offset int) StoreChannel + GetPostsAfter(channelId string, postId string, numPosts int, offset int) StoreChannel GetPostsSince(channelId string, time int64) StoreChannel GetEtag(channelId string) StoreChannel Search(teamId string, userId string, params *model.SearchParams) StoreChannel -- cgit v1.2.3-1-g7c22