From 2f15523fe88c3a382abda1e64b2ef962c3ab5128 Mon Sep 17 00:00:00 2001 From: Saturnino Abril Date: Thu, 30 Mar 2017 00:06:51 +0900 Subject: APIv4 put /posts/{post_id}/patch (#5883) * APIv4 put /posts/{post_id}/patch * Add props and edit permission --- api4/post.go | 28 +++++++++++++++ api4/post_test.go | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ app/post.go | 35 ++++++++++++++++--- model/client4.go | 10 ++++++ model/post.go | 50 ++++++++++++++++++++++++++ 5 files changed, 221 insertions(+), 4 deletions(-) diff --git a/api4/post.go b/api4/post.go index 329241139..af5fc8cfa 100644 --- a/api4/post.go +++ b/api4/post.go @@ -25,6 +25,7 @@ func InitPost() { BaseRoutes.Team.Handle("/posts/search", ApiSessionRequired(searchPosts)).Methods("POST") BaseRoutes.Post.Handle("", ApiSessionRequired(updatePost)).Methods("PUT") + BaseRoutes.Post.Handle("/patch", ApiSessionRequired(patchPost)).Methods("PUT") } func createPost(c *Context, w http.ResponseWriter, r *http.Request) { @@ -245,6 +246,33 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(rpost.ToJson())) } +func patchPost(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequirePostId() + if c.Err != nil { + return + } + + post := model.PostPatchFromJson(r.Body) + + if post == nil { + c.SetInvalidParam("post") + return + } + + if !app.SessionHasPermissionToPost(c.Session, c.Params.PostId, model.PERMISSION_EDIT_OTHERS_POSTS) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHERS_POSTS) + return + } + + patchedPost, err := app.PatchPost(c.Params.PostId, post) + if err != nil { + c.Err = err + return + } + + w.Write([]byte(patchedPost.ToJson())) +} + func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) { c.RequirePostId() if c.Err != nil { diff --git a/api4/post_test.go b/api4/post_test.go index 9e0880004..8f954efaa 100644 --- a/api4/post_test.go +++ b/api4/post_test.go @@ -5,6 +5,7 @@ package api4 import ( "net/http" + "reflect" "strconv" "testing" "time" @@ -167,6 +168,107 @@ func TestUpdatePost(t *testing.T) { CheckUnauthorizedStatus(t, resp) } +func TestPatchPost(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + channel := th.BasicChannel + + isLicensed := utils.IsLicensed + license := utils.License + allowEditPost := *utils.Cfg.ServiceSettings.AllowEditPost + defer func() { + utils.IsLicensed = isLicensed + utils.License = license + *utils.Cfg.ServiceSettings.AllowEditPost = allowEditPost + utils.SetDefaultRolesBasedOnConfig() + }() + utils.IsLicensed = true + utils.License = &model.License{Features: &model.Features{}} + utils.License.Features.SetDefaults() + + *utils.Cfg.ServiceSettings.AllowEditPost = model.ALLOW_EDIT_POST_ALWAYS + utils.SetDefaultRolesBasedOnConfig() + + post := &model.Post{ + ChannelId: channel.Id, + IsPinned: true, + Message: "#hashtag a message", + Props: model.StringInterface{"channel_header": "old_header"}, + FileIds: model.StringArray{"file1", "file2"}, + HasReactions: true, + } + post, _ = Client.CreatePost(post) + + patch := &model.PostPatch{} + + patch.IsPinned = new(bool) + *patch.IsPinned = false + patch.Message = new(string) + *patch.Message = "#otherhashtag other message" + patch.Props = new(model.StringInterface) + *patch.Props = model.StringInterface{"channel_header": "new_header"} + patch.FileIds = new(model.StringArray) + *patch.FileIds = model.StringArray{"file1", "otherfile2", "otherfile3"} + patch.HasReactions = new(bool) + *patch.HasReactions = false + + rpost, resp := Client.PatchPost(post.Id, patch) + CheckNoError(t, resp) + + if rpost.IsPinned != false { + t.Fatal("IsPinned did not update properly") + } + if rpost.Message != "#otherhashtag other message" { + t.Fatal("Message did not update properly") + } + if len(rpost.Props) != 1 { + t.Fatal("Props did not update properly") + } + if !reflect.DeepEqual(rpost.Props, *patch.Props) { + t.Fatal("Props did not update properly") + } + if rpost.Hashtags != "#otherhashtag" { + t.Fatal("Message did not update properly") + } + if len(rpost.FileIds) != 3 { + t.Fatal("FileIds did not update properly") + } + if !reflect.DeepEqual(rpost.FileIds, *patch.FileIds) { + t.Fatal("FileIds did not update properly") + } + if rpost.HasReactions != false { + t.Fatal("HasReactions did not update properly") + } + + if r, err := Client.DoApiPut("/posts/"+post.Id+"/patch", "garbage"); err == nil { + t.Fatal("should have errored") + } else { + if r.StatusCode != http.StatusBadRequest { + t.Log("actual: " + strconv.Itoa(r.StatusCode)) + t.Log("expected: " + strconv.Itoa(http.StatusBadRequest)) + t.Fatal("wrong status code") + } + } + + _, resp = Client.PatchPost("junk", patch) + CheckBadRequestStatus(t, resp) + + _, resp = Client.PatchPost(GenerateTestId(), patch) + CheckForbiddenStatus(t, resp) + + Client.Logout() + _, resp = Client.PatchPost(post.Id, patch) + CheckUnauthorizedStatus(t, resp) + + th.LoginTeamAdmin() + _, resp = Client.PatchPost(post.Id, patch) + CheckNoError(t, resp) + + _, resp = th.SystemAdminClient.PatchPost(post.Id, patch) + CheckNoError(t, resp) +} + func TestGetPostsForChannel(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer TearDown() diff --git a/app/post.go b/app/post.go index bfd0fccc5..375b775e8 100644 --- a/app/post.go +++ b/app/post.go @@ -299,18 +299,19 @@ func UpdatePost(post *model.Post) (*model.Post, *model.AppError) { *newPost = *oldPost newPost.Message = post.Message + newPost.Props = post.Props newPost.EditAt = model.GetMillis() newPost.Hashtags, _ = model.ParseHashtags(post.Message) + newPost.IsPinned = post.IsPinned + newPost.HasReactions = post.HasReactions + newPost.FileIds = post.FileIds if result := <-Srv.Store.Post().Update(newPost, oldPost); result.Err != nil { return nil, result.Err } else { rpost := result.Data.(*model.Post) - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", rpost.ChannelId, "", nil) - message.Add("post", rpost.ToJson()) - - go Publish(message) + sendUpdatedPostEvent(rpost) InvalidateCacheForChannelPosts(rpost.ChannelId) @@ -318,6 +319,32 @@ func UpdatePost(post *model.Post) (*model.Post, *model.AppError) { } } +func PatchPost(postId string, patch *model.PostPatch) (*model.Post, *model.AppError) { + post, err := GetSinglePost(postId) + if err != nil { + return nil, err + } + + post.Patch(patch) + + updatedPost, err := UpdatePost(post) + if err != nil { + return nil, err + } + + sendUpdatedPostEvent(updatedPost) + InvalidateCacheForChannelPosts(updatedPost.ChannelId) + + return updatedPost, nil +} + +func sendUpdatedPostEvent(post *model.Post) { + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", post.ChannelId, "", nil) + message.Add("post", post.ToJson()) + + go Publish(message) +} + func GetPostsPage(channelId string, page int, perPage int) (*model.PostList, *model.AppError) { if result := <-Srv.Store.Post().GetPosts(channelId, page*perPage, perPage, true); result.Err != nil { return nil, result.Err diff --git a/model/client4.go b/model/client4.go index 72d8951b9..5259cb915 100644 --- a/model/client4.go +++ b/model/client4.go @@ -1193,6 +1193,16 @@ func (c *Client4) UpdatePost(postId string, post *Post) (*Post, *Response) { } } +// PatchPost partially updates a post. Any missing fields are not updated. +func (c *Client4) PatchPost(postId string, patch *PostPatch) (*Post, *Response) { + if r, err := c.DoApiPut(c.GetPostRoute(postId)+"/patch", patch.ToJson()); err != nil { + return nil, &Response{StatusCode: r.StatusCode, Error: err} + } else { + defer closeBody(r) + return PostFromJson(r.Body), BuildResponse(r) + } +} + // GetPost gets a single post. func (c *Client4) GetPost(postId string, etag string) (*Post, *Response) { if r, err := c.DoApiGet(c.GetPostRoute(postId), etag); err != nil { diff --git a/model/post.go b/model/post.go index c419deb56..0d9651924 100644 --- a/model/post.go +++ b/model/post.go @@ -54,6 +54,14 @@ type Post struct { HasReactions bool `json:"has_reactions,omitempty"` } +type PostPatch struct { + IsPinned *bool `json:"is_pinned"` + Message *string `json:"message"` + Props *StringInterface `json:"props"` + FileIds *StringArray `json:"file_ids"` + HasReactions *bool `json:"has_reactions"` +} + func (o *Post) ToJson() string { b, err := json.Marshal(o) if err != nil { @@ -190,3 +198,45 @@ func (o *Post) AddProp(key string, value interface{}) { func (o *Post) IsSystemMessage() bool { return len(o.Type) >= len(POST_SYSTEM_MESSAGE_PREFIX) && o.Type[:len(POST_SYSTEM_MESSAGE_PREFIX)] == POST_SYSTEM_MESSAGE_PREFIX } + +func (p *Post) Patch(patch *PostPatch) { + if patch.IsPinned != nil { + p.IsPinned = *patch.IsPinned + } + + if patch.Message != nil { + p.Message = *patch.Message + } + + if patch.Props != nil { + p.Props = *patch.Props + } + + if patch.FileIds != nil { + p.FileIds = *patch.FileIds + } + + if patch.HasReactions != nil { + p.HasReactions = *patch.HasReactions + } +} + +func (o *PostPatch) ToJson() string { + b, err := json.Marshal(o) + if err != nil { + return "" + } + + return string(b) +} + +func PostPatchFromJson(data io.Reader) *PostPatch { + decoder := json.NewDecoder(data) + var post PostPatch + err := decoder.Decode(&post) + if err != nil { + return nil + } + + return &post +} -- cgit v1.2.3-1-g7c22