MM-47750: Adds PostAcknowledgements table and apis (#21689)

* MM-46410: adds urgency on mention counts

We have introduced priority for posts in
https://github.com/mattermost/mattermost-webapp/pull/10951.
We do need to color the mention badges in the webapp with a prominent
color when a mention is posted in an urgent message.
A thread has urgent mentions if the root post is marked as urgent, and
the replies contain mentions to the user viewing the thread.

This PR adds two columns, urgentmentioncount, and isurgent, in
channelmembers, and threads tables respectively.
Furthermore when asking for team/thread mention counts, we also return
urgent mention counts for the user.

* Fixes method in tests

* empty commit

* Fixes method call

* Fixes single thread response is_urgent

* Fixes errors

* Fixes mysql migration and adds graphql schema

* Fixes tests

* Refactors IsUrgent and Adds PostsPriority table

Changes:
- removes is_urgent from the threads table
- adds a new table to hold posts priorities
- refactors priority out of the props and into the new table

* Fixes

* Adds translation strings

* Fixes migrations and tests

* Fixes tests

* empty

* Adds Priority to Copy

* empty

* Fixes priority not saved when boards is enabled

We are nilifying Metadata when post.ForPlugin(), which didn't save Priority
for a post when Boards was enabled.
This commit copies metadata again to the post, so metadata are
reinstated.

* Fixes tests

* Adding store tests and fixes syntax error

* Uses threads.ThreadTeamId

* Fixes error

* Adds UrgentMentionCount in graphql api test

* Fetches post priority in batches

* Addresses review comments

* Restore only priority on create post

* Fixes tests

* Nits

* Some refactoring

* Fixes get thread options when post priority enabled

* Adds missing translation

* Use the constant instead of "urgent" string

* Renames urgent constant

* MM-47750: Adds PostAcknowledgements table and apis

- Adds post acknowledgement api/app/store methods to be able to save and
delete post acknowledgements by users.
- Adds wesbsocket events for acknowledgement created/deleted
- Returns post acknowledgements in the post's metadata

* Empty

* Fixes incorrect urgent count when marking a post as unread

* Adds license

* Fixes ACK api, and adds tests

* Fixes vet

* Fixes tests

* Addresses review comments

* Remove unnecessary lines

* Adds config option and changes return of delete ack

* Empty

* Empty

* Enable config by default

* Fixes intl

* Fixes test after setting config default true

* Changes endpoints to PostForUser

* Avoids replica lag

* Fixes error in merge

* Fixes RetryLayer tests due to merge

* Empty

* Empty

* Empty

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
Kyriakos Z
2022-11-24 02:41:23 +02:00
коммит произвёл GitHub
родитель c44d37629a
Коммит 27db854089
32 изменённых файлов: 1483 добавлений и 19 удалений

Просмотреть файл

@@ -38,6 +38,9 @@ func (api *API) InitPost() {
api.BaseRoutes.Post.Handle("/pin", api.APISessionRequired(pinPost)).Methods("POST")
api.BaseRoutes.Post.Handle("/unpin", api.APISessionRequired(unpinPost)).Methods("POST")
api.BaseRoutes.PostForUser.Handle("/ack", api.APISessionRequired(acknowledgePost)).Methods("POST")
api.BaseRoutes.PostForUser.Handle("/ack", api.APISessionRequired(unacknowledgePost)).Methods("DELETE")
}
func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -941,6 +944,80 @@ func unpinPost(c *Context, w http.ResponseWriter, _ *http.Request) {
saveIsPinnedPost(c, w, false)
}
func acknowledgePost(c *Context, w http.ResponseWriter, r *http.Request) {
// license check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequirePostId().RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
acknowledgement, appErr := c.App.SaveAcknowledgementForPost(c.AppContext, c.Params.PostId, c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
js, err := json.Marshal(acknowledgement)
if err != nil {
c.Err = model.NewAppError("acknowledgePost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(js)
}
func unacknowledgePost(c *Context, w http.ResponseWriter, r *http.Request) {
// license check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequirePostId().RequireUserId()
if c.Err != nil {
return
}
if !c.App.SessionHasPermissionToUser(*c.AppContext.Session(), c.Params.UserId) {
c.SetPermissionError(model.PermissionEditOtherUsers)
return
}
if !c.App.SessionHasPermissionToChannelByPost(*c.AppContext.Session(), c.Params.PostId, model.PermissionReadChannel) {
c.SetPermissionError(model.PermissionReadChannel)
return
}
_, err := c.App.GetSinglePost(c.Params.PostId, false)
if err != nil {
c.Err = err
return
}
appErr := c.App.DeleteAcknowledgementForPost(c.AppContext, c.Params.PostId, c.Params.UserId)
if appErr != nil {
c.Err = appErr
return
}
ReturnStatusOK(w)
}
func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequirePostId()
if c.Err != nil {

Просмотреть файл

@@ -3342,3 +3342,87 @@ func TestPostReminder(t *testing.T) {
require.Truef(t, caught, "User should have received %s event", model.WebsocketEventEphemeralMessage)
}
func TestAcknowledgePost(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
client := th.Client
post := th.BasicPost
ack, _, err := client.AcknowledgePost(post.Id, th.BasicUser.Id)
require.NoError(t, err)
acks, appErr := th.App.GetAcknowledgementsForPost(post.Id)
require.Nil(t, appErr)
require.Len(t, acks, 1)
require.Equal(t, acks[0], ack)
_, resp, err := client.AcknowledgePost("junk", th.BasicUser.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.AcknowledgePost(GenerateTestId(), th.BasicUser.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, resp, err = client.AcknowledgePost(post.Id, "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
_, resp, err = client.AcknowledgePost(post.Id, th.BasicUser2.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
client.Logout()
_, resp, err = client.AcknowledgePost(post.Id, th.BasicUser.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
_, _, err = th.SystemAdminClient.AcknowledgePost(post.Id, th.SystemAdminUser.Id)
require.NoError(t, err)
}
func TestUnacknowledgePost(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
client := th.Client
post := th.BasicPost
ack, _, err := client.AcknowledgePost(post.Id, th.BasicUser.Id)
require.NoError(t, err)
acks, appErr := th.App.GetAcknowledgementsForPost(post.Id)
require.Nil(t, appErr)
require.Len(t, acks, 1)
require.Equal(t, acks[0], ack)
resp, err := client.UnacknowledgePost("junk", th.BasicUser.Id)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = client.UnacknowledgePost(GenerateTestId(), th.BasicUser.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
resp, err = client.UnacknowledgePost(post.Id, "junk")
require.Error(t, err)
CheckBadRequestStatus(t, resp)
resp, err = client.UnacknowledgePost(post.Id, th.BasicUser2.Id)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
_, err = client.UnacknowledgePost(post.Id, th.BasicUser.Id)
require.NoError(t, err)
acks, appErr = th.App.GetAcknowledgementsForPost(post.Id)
require.Nil(t, appErr)
require.Len(t, acks, 0)
client.Logout()
resp, err = client.UnacknowledgePost(post.Id, th.BasicUser.Id)
require.Error(t, err)
CheckUnauthorizedStatus(t, resp)
}

Просмотреть файл

@@ -6569,6 +6569,7 @@ func TestSingleThreadGet(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.ThreadAutoFollow = true
*cfg.ServiceSettings.PostPriority = false
*cfg.ServiceSettings.CollapsedThreads = model.CollapsedThreadsDefaultOn
*cfg.ServiceSettings.PostPriority = true
cfg.FeatureFlags.PostPriority = true
@@ -6615,6 +6616,7 @@ func TestSingleThreadGet(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.PostPriority = true
cfg.FeatureFlags.PostPriority = true
})
tr, _, err = th.Client.GetUserThread(th.BasicUser.Id, th.BasicTeam.Id, threads.Threads[0].PostId, true)

Просмотреть файл

@@ -509,6 +509,7 @@ type AppIface interface {
DeactivateGuests(c *request.Context) *model.AppError
DeactivateMfa(userID string) *model.AppError
DeauthorizeOAuthAppForUser(userID, appID string) *model.AppError
DeleteAcknowledgementForPost(c *request.Context, postID, userID string) *model.AppError
DeleteAllExpiredPluginKeys() *model.AppError
DeleteAllKeysForPlugin(pluginID string) *model.AppError
DeleteBrandImage() *model.AppError
@@ -567,6 +568,8 @@ type AppIface interface {
GenerateMfaSecret(userID string) (*model.MfaSecret, *model.AppError)
GeneratePublicLink(siteURL string, info *model.FileInfo) string
GenerateSupportPacket() []model.FileData
GetAcknowledgementsForPost(postID string) ([]*model.PostAcknowledgement, *model.AppError)
GetAcknowledgementsForPostList(postList *model.PostList) (map[string][]*model.PostAcknowledgement, *model.AppError)
GetActivePluginManifests() ([]*model.Manifest, *model.AppError)
GetAllChannels(c request.CTX, page, perPage int, opts model.ChannelSearchOpts) (model.ChannelListWithTeamData, *model.AppError)
GetAllChannelsCount(c request.CTX, opts model.ChannelSearchOpts) (int64, *model.AppError)
@@ -984,6 +987,7 @@ type AppIface interface {
SanitizeProfile(user *model.User, asAdmin bool)
SanitizeTeam(session model.Session, team *model.Team) *model.Team
SanitizeTeams(session model.Session, teams []*model.Team) []*model.Team
SaveAcknowledgementForPost(c *request.Context, postID, userID string) (*model.PostAcknowledgement, *model.AppError)
SaveAdminNotification(userId string, notifyData *model.NotifyAdminToUpgradeRequest) *model.AppError
SaveAdminNotifyData(data *model.NotifyAdminData) (*model.NotifyAdminData, *model.AppError)
SaveBrandImage(imageData *multipart.FileHeader) *model.AppError

Просмотреть файл

@@ -2838,6 +2838,28 @@ func (a *OpenTracingAppLayer) DefaultChannelNames(c request.CTX) []string {
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteAcknowledgementForPost(c *request.Context, postID string, userID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteAcknowledgementForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeleteAcknowledgementForPost(c, postID, userID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeleteAllExpiredPluginKeys() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeleteAllExpiredPluginKeys")
@@ -4411,6 +4433,50 @@ func (a *OpenTracingAppLayer) GenerateSupportPacket() []model.FileData {
return resultVar0
}
func (a *OpenTracingAppLayer) GetAcknowledgementsForPost(postID string) ([]*model.PostAcknowledgement, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAcknowledgementsForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAcknowledgementsForPost(postID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetAcknowledgementsForPostList(postList *model.PostList) (map[string][]*model.PostAcknowledgement, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetAcknowledgementsForPostList")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetAcknowledgementsForPostList(postList)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetActivePluginManifests() ([]*model.Manifest, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetActivePluginManifests")
@@ -14312,6 +14378,28 @@ func (a *OpenTracingAppLayer) SanitizeTeams(session model.Session, teams []*mode
return resultVar0
}
func (a *OpenTracingAppLayer) SaveAcknowledgementForPost(c *request.Context, postID string, userID string) (*model.PostAcknowledgement, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveAcknowledgementForPost")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.SaveAcknowledgementForPost(c, postID, userID)
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) SaveAdminNotification(userId string, notifyData *model.NotifyAdminToUpgradeRequest) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SaveAdminNotification")

130
app/post_acknowledgements.go Обычный файл
Просмотреть файл

@@ -0,0 +1,130 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"encoding/json"
"errors"
"net/http"
"github.com/mattermost/mattermost-server/v6/app/request"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
"github.com/mattermost/mattermost-server/v6/store"
)
func (a *App) SaveAcknowledgementForPost(c *request.Context, postID, userID string) (*model.PostAcknowledgement, *model.AppError) {
post, err := a.GetSinglePost(postID, false)
if err != nil {
return nil, err
}
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return nil, err
}
if channel.DeleteAt > 0 {
return nil, model.NewAppError("SaveAcknowledgementForPost", "api.acknowledgement.save.archived_channel.app_error", nil, "", http.StatusForbidden)
}
acknowledgedAt := model.GetMillis()
acknowledgement, nErr := a.Srv().Store().PostAcknowledgement().Save(postID, userID, acknowledgedAt)
if nErr != nil {
var appErr *model.AppError
switch {
case errors.As(nErr, &appErr):
return nil, appErr
default:
return nil, model.NewAppError("SaveAcknowledgementForPost", "app.acknowledgement.save.save.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
a.Srv().Go(func() {
a.sendAcknowledgementEvent(model.WebsocketEventAcknowledgementAdded, acknowledgement, post)
})
return acknowledgement, nil
}
func (a *App) DeleteAcknowledgementForPost(c *request.Context, postID, userID string) *model.AppError {
post, err := a.GetSinglePost(postID, false)
if err != nil {
return err
}
channel, err := a.GetChannel(c, post.ChannelId)
if err != nil {
return err
}
if channel.DeleteAt > 0 {
return model.NewAppError("DeleteAcknowledgementForPost", "api.acknowledgement.delete.archived_channel.app_error", nil, "", http.StatusForbidden)
}
oldAck, nErr := a.Srv().Store().PostAcknowledgement().Get(postID, userID)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return model.NewAppError("GetPostAcknowledgement", "app.acknowledgement.get.app_error", nil, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("GetPostAcknowledgement", "app.acknowledgement.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if model.GetMillis()-oldAck.AcknowledgedAt > 5*60*1000 {
return model.NewAppError("DeleteAcknowledgementForPost", "api.acknowledgement.delete.deadline.app_error", nil, "", http.StatusForbidden)
}
nErr = a.Srv().Store().PostAcknowledgement().Delete(oldAck)
if nErr != nil {
return model.NewAppError("DeleteAcknowledgementForPost", "app.acknowledgement.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
a.Srv().Go(func() {
a.sendAcknowledgementEvent(model.WebsocketEventAcknowledgementRemoved, oldAck, post)
})
return nil
}
func (a *App) GetAcknowledgementsForPost(postID string) ([]*model.PostAcknowledgement, *model.AppError) {
acknowledgements, nErr := a.Srv().Store().PostAcknowledgement().GetForPost(postID)
if nErr != nil {
return nil, model.NewAppError("GetAcknowledgementsForPost", "app.acknowledgement.getforpost.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
return acknowledgements, nil
}
func (a *App) GetAcknowledgementsForPostList(postList *model.PostList) (map[string][]*model.PostAcknowledgement, *model.AppError) {
acknowledgements, err := a.Srv().Store().PostAcknowledgement().GetForPosts(postList.Order)
if err != nil {
return nil, model.NewAppError("GetPostAcknowledgementsForPostList", "app.acknowledgement.get.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
acknowledgementsMap := make(map[string][]*model.PostAcknowledgement)
for _, ack := range acknowledgements {
acknowledgementsMap[ack.PostId] = append(acknowledgementsMap[ack.PostId], ack)
}
return acknowledgementsMap, nil
}
func (a *App) sendAcknowledgementEvent(event string, acknowledgement *model.PostAcknowledgement, post *model.Post) {
// send out that a acknowledgement has been added/removed
message := model.NewWebSocketEvent(event, "", post.ChannelId, "", nil, "")
acknowledgementJSON, err := json.Marshal(acknowledgement)
if err != nil {
a.Log().Warn("Failed to encode acknowledgement to JSON", mlog.Err(err))
}
message.Add("acknowledgement", string(acknowledgementJSON))
a.Publish(message)
}

149
app/post_acknowledgements_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,149 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"testing"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/stretchr/testify/require"
)
func TestPostAcknowledgementsApp(t *testing.T) {
t.Run("SaveAcknowledgementForPost", func(t *testing.T) { testSaveAcknowledgementForPost(t) })
t.Run("DeleteAcknowledgementForPost", func(t *testing.T) { testDeleteAcknowledgementForPost(t) })
t.Run("GetAcknowledgementsForPostList", func(t *testing.T) { testGetAcknowledgementsForPostList(t) })
}
func testSaveAcknowledgementForPost(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
t.Run("save acknowledgment for post should save acknowledgement", func(t *testing.T) {
post, err := th.App.CreatePostAsUser(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "message",
}, "", true)
require.Nil(t, err)
acknowledgment, err := th.App.SaveAcknowledgementForPost(th.Context, post.Id, th.BasicUser.Id)
require.Nil(t, err)
require.Greater(t, acknowledgment.AcknowledgedAt, int64(0))
require.Equal(t, post.Id, acknowledgment.PostId)
require.Equal(t, th.BasicUser.Id, acknowledgment.UserId)
})
}
func testDeleteAcknowledgementForPost(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
post, err := th.App.CreatePostAsUser(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
CreateAt: model.GetMillis(),
Message: "message",
}, "", true)
require.Nil(t, err)
t.Run("delete acknowledgment for post should delete acknowledgement", func(t *testing.T) {
_, err = th.App.SaveAcknowledgementForPost(th.Context, post.Id, th.BasicUser.Id)
require.Nil(t, err)
acknowledgments, err := th.App.GetAcknowledgementsForPost(post.Id)
require.Nil(t, err)
require.Len(t, acknowledgments, 1)
require.Greater(t, acknowledgments[0].AcknowledgedAt, int64(0))
err = th.App.DeleteAcknowledgementForPost(th.Context, post.Id, th.BasicUser.Id)
require.Nil(t, err)
acknowledgments, err = th.App.GetAcknowledgementsForPost(post.Id)
require.Nil(t, err)
require.Empty(t, acknowledgments)
})
t.Run("delete acknowledgment for post after 5 min after acknowledged should not delete", func(t *testing.T) {
_, nErr := th.App.Srv().Store().PostAcknowledgement().Save(post.Id, th.BasicUser.Id, model.GetMillis()-int64(6*60*1000))
require.NoError(t, nErr)
acknowledgments, err := th.App.GetAcknowledgementsForPost(post.Id)
require.Nil(t, err)
require.Len(t, acknowledgments, 1)
require.Greater(t, acknowledgments[0].AcknowledgedAt, int64(0))
err = th.App.DeleteAcknowledgementForPost(th.Context, post.Id, th.BasicUser.Id)
require.NotNil(t, err)
require.Equal(t, 403, err.StatusCode)
acknowledgments, err = th.App.GetAcknowledgementsForPost(post.Id)
require.Nil(t, err)
require.Len(t, acknowledgments, 1)
require.Greater(t, acknowledgments[0].AcknowledgedAt, int64(0))
})
}
func testGetAcknowledgementsForPostList(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
p1, err := th.App.CreatePostAsUser(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
CreateAt: model.GetMillis(),
Message: "message",
}, "", true)
require.Nil(t, err)
p2, err := th.App.CreatePostAsUser(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
CreateAt: model.GetMillis(),
Message: "message",
}, "", true)
require.Nil(t, err)
p3, err := th.App.CreatePostAsUser(th.Context, &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
CreateAt: model.GetMillis(),
Message: "message",
}, "", true)
require.Nil(t, err)
t.Run("get acknowledgments for post list should return a map", func(t *testing.T) {
_, err = th.App.SaveAcknowledgementForPost(th.Context, p1.Id, th.BasicUser.Id)
require.Nil(t, err)
_, err = th.App.SaveAcknowledgementForPost(th.Context, p2.Id, th.BasicUser.Id)
require.Nil(t, err)
_, err = th.App.SaveAcknowledgementForPost(th.Context, p1.Id, th.BasicUser2.Id)
require.Nil(t, err)
postList := model.NewPostList()
postList.AddPost(p1)
postList.AddOrder(p1.Id)
postList.AddPost(p2)
postList.AddOrder(p2.Id)
postList.AddPost(p3)
postList.AddOrder(p3.Id)
acks1, err := th.App.GetAcknowledgementsForPost(p1.Id)
require.Nil(t, err)
acks2, err := th.App.GetAcknowledgementsForPost(p2.Id)
require.Nil(t, err)
acknowledgementsMap, err := th.App.GetAcknowledgementsForPostList(postList)
require.Nil(t, err)
expected := map[string][]*model.PostAcknowledgement{
p1.Id: acks1,
p2.Id: acks2,
}
require.Equal(t, expected, acknowledgementsMap)
require.Len(t, acknowledgementsMap[p1.Id], 2)
require.Len(t, acknowledgementsMap[p2.Id], 1)
require.Nil(t, acknowledgementsMap[p3.Id])
})
}

Просмотреть файл

@@ -63,10 +63,15 @@ func (a *App) PreparePostListForClient(c request.CTX, originalList *model.PostLi
if a.isPostPriorityEnabled() {
priority, _ := a.GetPriorityForPostList(list)
acknowledgements, _ := a.GetAcknowledgementsForPostList(list)
for _, id := range list.Order {
if _, ok := priority[id]; ok {
list.Posts[id].Metadata.Priority = priority[id]
}
if _, ok := acknowledgements[id]; ok {
list.Posts[id].Metadata.Acknowledgements = acknowledgements[id]
}
}
}
@@ -139,6 +144,13 @@ func (a *App) PreparePostForClient(c request.CTX, originalPost *model.Post, isNe
} else {
post.Metadata.Priority = priority
}
// Post's acknowledgements if any
if acknowledgements, err := a.GetAcknowledgementsForPost(post.Id); err != nil {
mlog.Warn("Failed to get post acknowledgements for a post", mlog.String("post_id", post.Id), mlog.Err(err))
} else {
post.Metadata.Acknowledgements = acknowledgements
}
}
return post

Просмотреть файл

@@ -206,6 +206,10 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
if license.SkuShortName == model.LicenseShortSkuProfessional || license.SkuShortName == model.LicenseShortSkuEnterprise {
props["EnableCustomGroups"] = strconv.FormatBool(*c.ServiceSettings.EnableCustomGroups)
}
if (license.SkuShortName == model.LicenseShortSkuProfessional || license.SkuShortName == model.LicenseShortSkuEnterprise) && c.FeatureFlags.PostPriority {
props["PostAcknowledgements"] = "true"
}
}
return props

Просмотреть файл

@@ -194,6 +194,8 @@ db/migrations/mysql/000096_threads_threadteamid.down.sql
db/migrations/mysql/000096_threads_threadteamid.up.sql
db/migrations/mysql/000097_create_posts_priority.down.sql
db/migrations/mysql/000097_create_posts_priority.up.sql
db/migrations/mysql/000098_create_post_acknowledgements.down.sql
db/migrations/mysql/000098_create_post_acknowledgements.up.sql
db/migrations/postgres/000001_create_teams.down.sql
db/migrations/postgres/000001_create_teams.up.sql
db/migrations/postgres/000002_create_team_members.down.sql
@@ -388,3 +390,5 @@ db/migrations/postgres/000096_threads_threadteamid.down.sql
db/migrations/postgres/000096_threads_threadteamid.up.sql
db/migrations/postgres/000097_create_posts_priority.down.sql
db/migrations/postgres/000097_create_posts_priority.up.sql
db/migrations/postgres/000098_create_post_acknowledgements.down.sql
db/migrations/postgres/000098_create_post_acknowledgements.up.sql

Просмотреть файл

@@ -0,0 +1 @@
DROP TABLE IF EXISTS PostAcknowledgements;

Просмотреть файл

@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS PostAcknowledgements (
PostId varchar(26) NOT NULL,
UserId varchar(26) NOT NULL,
AcknowledgedAt bigint(20) DEFAULT NULL,
PRIMARY KEY (PostId, UserId)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Просмотреть файл

@@ -0,0 +1 @@
DROP TABLE IF EXISTS postacknowledgements;

Просмотреть файл

@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS postacknowledgements(
postid VARCHAR(26) NOT NULL,
userid VARCHAR(26) NOT NULL,
acknowledgedat bigint,
PRIMARY KEY (postid, userid)
);

Просмотреть файл

@@ -59,6 +59,18 @@
"id": "September",
"translation": "September"
},
{
"id": "api.acknowledgement.delete.archived_channel.app_error",
"translation": "You cannot remove an acknowledgment in an archived channel."
},
{
"id": "api.acknowledgement.delete.deadline.app_error",
"translation": "You cannot delete an acknowledgment after 5min have passed."
},
{
"id": "api.acknowledgement.save.archived_channel.app_error",
"translation": "You cannot acknowledgment in an archived channel."
},
{
"id": "api.admin.add_certificate.array.app_error",
"translation": "No file under 'certificate' in request."
@@ -4459,6 +4471,22 @@
"id": "api.websocket_handler.server_busy.app_error",
"translation": "Server is busy, non-critical services are temporarily unavailable."
},
{
"id": "app.acknowledgement.delete.app_error",
"translation": "Unable to delete acknowledgement."
},
{
"id": "app.acknowledgement.get.app_error",
"translation": "Unable to get acknowledgement."
},
{
"id": "app.acknowledgement.getforpost.get.app_error",
"translation": "Unable to get acknowledgement for post."
},
{
"id": "app.acknowledgement.save.save.app_error",
"translation": "Unable to save acknowledgement for post."
},
{
"id": "app.admin.saml.failure_decode_metadata_xml_from_idp.app_error",
"translation": "Could not decode the XML metadata information received from the Identity Provider."
@@ -7927,6 +7955,14 @@
"id": "model.access.is_valid.user_id.app_error",
"translation": "Invalid user id."
},
{
"id": "model.acknowledgement.is_valid.post_id.app_error",
"translation": "Invalid post id."
},
{
"id": "model.acknowledgement.is_valid.user_id.app_error",
"translation": "Invalid user id."
},
{
"id": "model.authorize.is_valid.auth_code.app_error",
"translation": "Invalid authorization code."

Просмотреть файл

@@ -8448,6 +8448,28 @@ func (c *Client4) GetNewTeamMembersSince(teamID string, timeRange string, page i
return newTeamMembersList, BuildResponse(r), nil
}
func (c *Client4) AcknowledgePost(postId, userId string) (*PostAcknowledgement, *Response, error) {
r, err := c.DoAPIPost(c.userRoute(userId)+c.postRoute(postId)+"/ack", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var ack *PostAcknowledgement
if jsonErr := json.NewDecoder(r.Body).Decode(&ack); jsonErr != nil {
return nil, nil, NewAppError("AcknowledgePost", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
}
return ack, BuildResponse(r), nil
}
func (c *Client4) UnacknowledgePost(postId, userId string) (*Response, error) {
r, err := c.DoAPIDelete(c.userRoute(userId) + c.postRoute(postId) + "/ack")
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
func (c *Client4) AddUserToGroupSyncables(userID string) (*Response, error) {
r, err := c.DoAPIPost(c.ldapRoute()+"/users/"+userID+"/group_sync_memberships", "")
if err != nil {

Просмотреть файл

@@ -845,7 +845,7 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) {
}
if s.PostPriority == nil {
s.PostPriority = NewBool(false)
s.PostPriority = NewBool(true)
}
}

24
model/post_acknowledgement.go Обычный файл
Просмотреть файл

@@ -0,0 +1,24 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import "net/http"
type PostAcknowledgement struct {
UserId string `json:"user_id"`
PostId string `json:"post_id"`
AcknowledgedAt int64 `json:"acknowledged_at"`
}
func (o *PostAcknowledgement) IsValid() *AppError {
if !IsValidId(o.UserId) {
return NewAppError("PostAcknowledgement.IsValid", "model.acknowledgement.is_valid.user_id.app_error", nil, "user_id="+o.UserId, http.StatusBadRequest)
}
if !IsValidId(o.PostId) {
return NewAppError("PostAcknowledgement.IsValid", "model.acknowledgement.is_valid.post_id.app_error", nil, "post_id="+o.PostId, http.StatusBadRequest)
}
return nil
}

Просмотреть файл

@@ -23,8 +23,11 @@ type PostMetadata struct {
// Reactions holds reactions made to the post.
Reactions []*Reaction `json:"reactions,omitempty"`
// Reactions holds reactions made to the post.
// Priority holds info about priority settings for the post.
Priority *PostPriority `json:"priority,omitempty"`
// Acknowledgements holds acknowledgements made by users to the post
Acknowledgements []*PostAcknowledgement `json:"acknowledgements,omitempty"`
}
type PostImage struct {
@@ -57,6 +60,9 @@ func (p *PostMetadata) Copy() *PostMetadata {
reactionsCopy := make([]*Reaction, len(p.Reactions))
copy(reactionsCopy, p.Reactions)
acknowledgementsCopy := make([]*PostAcknowledgement, len(p.Acknowledgements))
copy(acknowledgementsCopy, p.Acknowledgements)
var postPriorityCopy *PostPriority
if p.Priority != nil {
postPriorityCopy = &PostPriority{
@@ -69,11 +75,12 @@ func (p *PostMetadata) Copy() *PostMetadata {
}
return &PostMetadata{
Embeds: embedsCopy,
Emojis: emojisCopy,
Files: filesCopy,
Images: imagesCopy,
Reactions: reactionsCopy,
Priority: postPriorityCopy,
Embeds: embedsCopy,
Emojis: emojisCopy,
Files: filesCopy,
Images: imagesCopy,
Reactions: reactionsCopy,
Priority: postPriorityCopy,
Acknowledgements: acknowledgementsCopy,
}
}

Просмотреть файл

@@ -76,6 +76,8 @@ const (
WebsocketEventThreadFollowChanged = "thread_follow_changed"
WebsocketEventThreadReadChanged = "thread_read_changed"
WebsocketFirstAdminVisitMarketplaceStatusReceived = "first_admin_visit_marketplace_status_received"
WebsocketEventAcknowledgementAdded = "post_acknowledgement_added"
WebsocketEventAcknowledgementRemoved = "post_acknowledgement_removed"
)
type WebSocketMessage interface {

Просмотреть файл

@@ -37,6 +37,7 @@ type OpenTracingLayer struct {
OAuthStore store.OAuthStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
@@ -132,6 +133,10 @@ func (s *OpenTracingLayer) Post() store.PostStore {
return s.PostStore
}
func (s *OpenTracingLayer) PostAcknowledgement() store.PostAcknowledgementStore {
return s.PostAcknowledgementStore
}
func (s *OpenTracingLayer) PostPriority() store.PostPriorityStore {
return s.PostPriorityStore
}
@@ -306,6 +311,11 @@ type OpenTracingLayerPostStore struct {
Root *OpenTracingLayer
}
type OpenTracingLayerPostAcknowledgementStore struct {
store.PostAcknowledgementStore
Root *OpenTracingLayer
}
type OpenTracingLayerPostPriorityStore struct {
store.PostPriorityStore
Root *OpenTracingLayer
@@ -6544,6 +6554,96 @@ func (s *OpenTracingLayerPostStore) Update(newPost *model.Post, oldPost *model.P
return result, err
}
func (s *OpenTracingLayerPostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.Delete")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
err := s.PostAcknowledgementStore.Delete(acknowledgement)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return err
}
func (s *OpenTracingLayerPostAcknowledgementStore) Get(postID string, userID string) (*model.PostAcknowledgement, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.Get")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostAcknowledgementStore.Get(postID, userID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.GetForPost")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostAcknowledgementStore.GetForPost(postID)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.GetForPosts")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostAcknowledgementStore.GetForPosts(postIds)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostAcknowledgementStore) Save(postID string, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostAcknowledgementStore.Save")
s.Root.Store.SetContext(newCtx)
defer func() {
s.Root.Store.SetContext(origCtx)
}()
defer span.Finish()
result, err := s.PostAcknowledgementStore.Save(postID, userID, acknowledgedAt)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)
}
return result, err
}
func (s *OpenTracingLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostPriorityStore.GetForPost")
@@ -12591,6 +12691,7 @@ func New(childStore store.Store, ctx context.Context) *OpenTracingLayer {
newStore.OAuthStore = &OpenTracingLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
newStore.PluginStore = &OpenTracingLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
newStore.PostStore = &OpenTracingLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
newStore.PostAcknowledgementStore = &OpenTracingLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore}
newStore.PostPriorityStore = &OpenTracingLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore}
newStore.PreferenceStore = &OpenTracingLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
newStore.ProductNoticesStore = &OpenTracingLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}

Просмотреть файл

@@ -40,6 +40,7 @@ type RetryLayer struct {
OAuthStore store.OAuthStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
@@ -135,6 +136,10 @@ func (s *RetryLayer) Post() store.PostStore {
return s.PostStore
}
func (s *RetryLayer) PostAcknowledgement() store.PostAcknowledgementStore {
return s.PostAcknowledgementStore
}
func (s *RetryLayer) PostPriority() store.PostPriorityStore {
return s.PostPriorityStore
}
@@ -309,6 +314,11 @@ type RetryLayerPostStore struct {
Root *RetryLayer
}
type RetryLayerPostAcknowledgementStore struct {
store.PostAcknowledgementStore
Root *RetryLayer
}
type RetryLayerPostPriorityStore struct {
store.PostPriorityStore
Root *RetryLayer
@@ -7420,6 +7430,111 @@ func (s *RetryLayerPostStore) Update(newPost *model.Post, oldPost *model.Post) (
}
func (s *RetryLayerPostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error {
tries := 0
for {
err := s.PostAcknowledgementStore.Delete(acknowledgement)
if err == nil {
return nil
}
if !isRepeatableError(err) {
return err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostAcknowledgementStore) Get(postID string, userID string) (*model.PostAcknowledgement, error) {
tries := 0
for {
result, err := s.PostAcknowledgementStore.Get(postID, userID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) {
tries := 0
for {
result, err := s.PostAcknowledgementStore.GetForPost(postID)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) {
tries := 0
for {
result, err := s.PostAcknowledgementStore.GetForPosts(postIds)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostAcknowledgementStore) Save(postID string, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) {
tries := 0
for {
result, err := s.PostAcknowledgementStore.Save(postID, userID, acknowledgedAt)
if err == nil {
return result, nil
}
if !isRepeatableError(err) {
return result, err
}
tries++
if tries >= 3 {
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
return result, err
}
timepkg.Sleep(100 * timepkg.Millisecond)
}
}
func (s *RetryLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) {
tries := 0
@@ -14355,6 +14470,7 @@ func New(childStore store.Store) *RetryLayer {
newStore.OAuthStore = &RetryLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
newStore.PluginStore = &RetryLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
newStore.PostStore = &RetryLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
newStore.PostAcknowledgementStore = &RetryLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore}
newStore.PostPriorityStore = &RetryLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore}
newStore.PreferenceStore = &RetryLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
newStore.ProductNoticesStore = &RetryLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}

Просмотреть файл

@@ -55,6 +55,7 @@ func genStore() *mocks.Store {
mock.On("Webhook").Return(&mocks.WebhookStore{})
mock.On("NotifyAdmin").Return(&mocks.NotifyAdminStore{})
mock.On("PostPriority").Return(&mocks.PostPriorityStore{})
mock.On("PostAcknowledgement").Return(&mocks.PostAcknowledgementStore{})
return mock
}

144
store/sqlstore/post_acknowledgements_store.go Обычный файл
Просмотреть файл

@@ -0,0 +1,144 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"database/sql"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/store"
sq "github.com/mattermost/squirrel"
"github.com/pkg/errors"
)
type SqlPostAcknowledgementStore struct {
*SqlStore
}
func newSqlPostAcknowledgementStore(sqlStore *SqlStore) store.PostAcknowledgementStore {
return &SqlPostAcknowledgementStore{sqlStore}
}
func (s *SqlPostAcknowledgementStore) Get(postID, userID string) (*model.PostAcknowledgement, error) {
query := s.getQueryBuilder().
Select("PostId", "UserId", "AcknowledgedAt").
From("PostAcknowledgements").
Where(sq.And{
sq.Eq{"PostId": postID},
sq.Eq{"UserId": userID},
sq.NotEq{"AcknowledgedAt": 0},
})
var acknowledgement model.PostAcknowledgement
err := s.GetReplicaX().GetBuilder(&acknowledgement, query)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("PostAcknowledgement", postID)
}
return nil, err
}
return &acknowledgement, nil
}
func (s *SqlPostAcknowledgementStore) Save(postID, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) {
if acknowledgedAt == 0 {
acknowledgedAt = model.GetMillis()
}
acknowledgement := &model.PostAcknowledgement{
UserId: userID,
PostId: postID,
AcknowledgedAt: acknowledgedAt,
}
if err := acknowledgement.IsValid(); err != nil {
return nil, err
}
query := s.getQueryBuilder().
Insert("PostAcknowledgements").
Columns("PostId", "UserId", "AcknowledgedAt").
Values(acknowledgement.PostId, acknowledgement.UserId, acknowledgement.AcknowledgedAt)
if s.DriverName() == model.DatabaseDriverMysql {
query = query.SuffixExpr(sq.Expr("ON DUPLICATE KEY UPDATE AcknowledgedAt = ?", acknowledgement.AcknowledgedAt))
} else {
query = query.SuffixExpr(sq.Expr("ON CONFLICT (postid, userid) DO UPDATE SET AcknowledgedAt = ?", acknowledgement.AcknowledgedAt))
}
_, err := s.GetMasterX().ExecBuilder(query)
if err != nil {
return nil, err
}
return acknowledgement, nil
}
func (s *SqlPostAcknowledgementStore) Delete(ack *model.PostAcknowledgement) error {
query := s.getQueryBuilder().
Update("PostAcknowledgements").
Set("AcknowledgedAt", 0).
Where(sq.And{
sq.Eq{"PostId": ack.PostId},
sq.Eq{"UserId": ack.UserId},
})
_, err := s.GetMasterX().ExecBuilder(query)
if err != nil {
return err
}
return nil
}
func (s *SqlPostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) {
var acknowledgements []*model.PostAcknowledgement
query := s.getQueryBuilder().
Select("PostId", "UserId", "AcknowledgedAt").
From("PostAcknowledgements").
Where(sq.And{
sq.NotEq{"AcknowledgedAt": 0},
sq.Eq{"PostId": postID},
})
err := s.GetReplicaX().SelectBuilder(&acknowledgements, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to get PostAcknowledgements for postID=%s", postID)
}
return acknowledgements, nil
}
func (s *SqlPostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) {
var acknowledgements []*model.PostAcknowledgement
perPage := 200
for i := 0; i < len(postIds); i += perPage {
j := i + perPage
if len(postIds) < j {
j = len(postIds)
}
query := s.getQueryBuilder().
Select("PostId", "UserId", "AcknowledgedAt").
From("PostAcknowledgements").
Where(sq.And{
sq.Eq{"PostId": postIds[i:j]},
sq.NotEq{"AcknowledgedAt": 0},
})
var acknowledgementsBatch []*model.PostAcknowledgement
err := s.GetReplicaX().SelectBuilder(&acknowledgementsBatch, query)
if err != nil {
return nil, errors.Wrapf(err, "failed to get PostAcknowledgements for post list")
}
acknowledgements = append(acknowledgements, acknowledgementsBatch...)
}
return acknowledgements, nil
}

Просмотреть файл

@@ -0,0 +1,14 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package sqlstore
import (
"testing"
"github.com/mattermost/mattermost-server/v6/store/storetest"
)
func TestPostAcknowledgementsStore(t *testing.T) {
StoreTestWithSqlStore(t, storetest.TestPostAcknowledgementsStore)
}

Просмотреть файл

@@ -110,6 +110,7 @@ type SqlStoreStores struct {
sharedchannel store.SharedChannelStore
notifyAdmin store.NotifyAdminStore
postPriority store.PostPriorityStore
postAcknowledgement store.PostAcknowledgementStore
}
type SqlStore struct {
@@ -216,6 +217,7 @@ func New(settings model.SqlSettings, metrics einterfaces.MetricsInterface) *SqlS
store.stores.productNotices = newSqlProductNoticesStore(store)
store.stores.notifyAdmin = newSqlNotifyAdminStore(store)
store.stores.postPriority = newSqlPostPriorityStore(store)
store.stores.postAcknowledgement = newSqlPostAcknowledgementStore(store)
store.stores.preference.(*SqlPreferenceStore).deleteUnusedFeatures()
@@ -961,6 +963,10 @@ func (ss *SqlStore) PostPriority() store.PostPriorityStore {
return ss.stores.postPriority
}
func (ss *SqlStore) PostAcknowledgement() store.PostAcknowledgementStore {
return ss.stores.postAcknowledgement
}
func (ss *SqlStore) DropAllTables() {
if ss.DriverName() == model.DatabaseDriverPostgres {
ss.masterX.Exec(`DO

Просмотреть файл

@@ -85,6 +85,7 @@ type Store interface {
Context() context.Context
NotifyAdmin() NotifyAdminStore
PostPriority() PostPriorityStore
PostAcknowledgement() PostAcknowledgementStore
}
type RetentionPolicyStore interface {
@@ -978,6 +979,14 @@ type PostPriorityStore interface {
GetForPosts(ids []string) ([]*model.PostPriority, error)
}
type PostAcknowledgementStore interface {
Get(postID, userID string) (*model.PostAcknowledgement, error)
GetForPost(postID string) ([]*model.PostAcknowledgement, error)
GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error)
Save(postID, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error)
Delete(acknowledgement *model.PostAcknowledgement) error
}
// ChannelSearchOpts contains options for searching channels.
//
// NotAssociatedToGroup will exclude channels that have associated, active GroupChannels records.

Просмотреть файл

@@ -0,0 +1,121 @@
// Code generated by mockery v2.10.4. DO NOT EDIT.
// Regenerate this file using `make store-mocks`.
package mocks
import (
model "github.com/mattermost/mattermost-server/v6/model"
mock "github.com/stretchr/testify/mock"
)
// PostAcknowledgementStore is an autogenerated mock type for the PostAcknowledgementStore type
type PostAcknowledgementStore struct {
mock.Mock
}
// Delete provides a mock function with given fields: acknowledgement
func (_m *PostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error {
ret := _m.Called(acknowledgement)
var r0 error
if rf, ok := ret.Get(0).(func(*model.PostAcknowledgement) error); ok {
r0 = rf(acknowledgement)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: postID, userID
func (_m *PostAcknowledgementStore) Get(postID string, userID string) (*model.PostAcknowledgement, error) {
ret := _m.Called(postID, userID)
var r0 *model.PostAcknowledgement
if rf, ok := ret.Get(0).(func(string, string) *model.PostAcknowledgement); ok {
r0 = rf(postID, userID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostAcknowledgement)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(postID, userID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForPost provides a mock function with given fields: postID
func (_m *PostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) {
ret := _m.Called(postID)
var r0 []*model.PostAcknowledgement
if rf, ok := ret.Get(0).(func(string) []*model.PostAcknowledgement); ok {
r0 = rf(postID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PostAcknowledgement)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(postID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetForPosts provides a mock function with given fields: postIds
func (_m *PostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) {
ret := _m.Called(postIds)
var r0 []*model.PostAcknowledgement
if rf, ok := ret.Get(0).(func([]string) []*model.PostAcknowledgement); ok {
r0 = rf(postIds)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*model.PostAcknowledgement)
}
}
var r1 error
if rf, ok := ret.Get(1).(func([]string) error); ok {
r1 = rf(postIds)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Save provides a mock function with given fields: postID, userID, acknowledgedAt
func (_m *PostAcknowledgementStore) Save(postID string, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) {
ret := _m.Called(postID, userID, acknowledgedAt)
var r0 *model.PostAcknowledgement
if rf, ok := ret.Get(0).(func(string, string, int64) *model.PostAcknowledgement); ok {
r0 = rf(postID, userID, acknowledgedAt)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.PostAcknowledgement)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, int64) error); ok {
r1 = rf(postID, userID, acknowledgedAt)
} else {
r1 = ret.Error(1)
}
return r0, r1
}

Просмотреть файл

@@ -475,6 +475,22 @@ func (_m *Store) Post() store.PostStore {
return r0
}
// PostAcknowledgement provides a mock function with given fields:
func (_m *Store) PostAcknowledgement() store.PostAcknowledgementStore {
ret := _m.Called()
var r0 store.PostAcknowledgementStore
if rf, ok := ret.Get(0).(func() store.PostAcknowledgementStore); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.PostAcknowledgementStore)
}
}
return r0
}
// PostPriority provides a mock function with given fields:
func (_m *Store) PostPriority() store.PostPriorityStore {
ret := _m.Called()

Просмотреть файл

@@ -0,0 +1,181 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"testing"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/store"
"github.com/stretchr/testify/require"
)
func TestPostAcknowledgementsStore(t *testing.T, ss store.Store, s SqlStore) {
t.Run("Save", func(t *testing.T) { testPostAcknowledgementsStoreSave(t, ss) })
t.Run("GetForPost", func(t *testing.T) { testPostAcknowledgementsStoreGetForPost(t, ss) })
t.Run("GetForPosts", func(t *testing.T) { testPostAcknowledgementsStoreGetForPosts(t, ss) })
}
func testPostAcknowledgementsStoreSave(t *testing.T, ss store.Store) {
userId1 := model.NewId()
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(false),
},
}
_, err := ss.Post().Save(&p1)
require.NoError(t, err)
t.Run("consecutive saves should just update the acknowledged at", func(t *testing.T) {
_, err := ss.PostAcknowledgement().Save(p1.Id, userId1, 0)
require.NoError(t, err)
_, err = ss.PostAcknowledgement().Save(p1.Id, userId1, 0)
require.NoError(t, err)
ack1, err := ss.PostAcknowledgement().Save(p1.Id, userId1, 0)
require.NoError(t, err)
acknowledgements, err := ss.PostAcknowledgement().GetForPost(p1.Id)
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack1})
})
}
func testPostAcknowledgementsStoreGetForPost(t *testing.T, ss store.Store) {
userId1 := model.NewId()
userId2 := model.NewId()
userId3 := model.NewId()
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(false),
},
}
_, err := ss.Post().Save(&p1)
require.NoError(t, err)
t.Run("get acknowledgements for post", func(t *testing.T) {
ack1, err := ss.PostAcknowledgement().Save(p1.Id, userId1, 0)
require.NoError(t, err)
ack2, err := ss.PostAcknowledgement().Save(p1.Id, userId2, 0)
require.NoError(t, err)
ack3, err := ss.PostAcknowledgement().Save(p1.Id, userId3, 0)
require.NoError(t, err)
acknowledgements, err := ss.PostAcknowledgement().GetForPost(p1.Id)
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack1, ack2, ack3})
err = ss.PostAcknowledgement().Delete(ack1)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPost(p1.Id)
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack2, ack3})
err = ss.PostAcknowledgement().Delete(ack2)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPost(p1.Id)
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack3})
err = ss.PostAcknowledgement().Delete(ack3)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPost(p1.Id)
require.NoError(t, err)
require.Empty(t, acknowledgements)
})
}
func testPostAcknowledgementsStoreGetForPosts(t *testing.T, ss store.Store) {
userId1 := model.NewId()
userId2 := model.NewId()
userId3 := model.NewId()
p1 := model.Post{}
p1.ChannelId = model.NewId()
p1.UserId = model.NewId()
p1.Message = NewTestId()
p1.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString("important"),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(false),
},
}
p2 := model.Post{}
p2.ChannelId = model.NewId()
p2.UserId = model.NewId()
p2.Message = NewTestId()
p2.Metadata = &model.PostMetadata{
Priority: &model.PostPriority{
Priority: model.NewString(""),
RequestedAck: model.NewBool(true),
PersistentNotifications: model.NewBool(false),
},
}
_, errIdx, err := ss.Post().SaveMultiple([]*model.Post{&p1, &p2})
require.NoError(t, err)
require.Equal(t, -1, errIdx)
t.Run("get acknowledgements for post", func(t *testing.T) {
ack1, err := ss.PostAcknowledgement().Save(p1.Id, userId1, 0)
require.NoError(t, err)
ack2, err := ss.PostAcknowledgement().Save(p1.Id, userId2, 0)
require.NoError(t, err)
ack3, err := ss.PostAcknowledgement().Save(p2.Id, userId2, 0)
require.NoError(t, err)
ack4, err := ss.PostAcknowledgement().Save(p2.Id, userId3, 0)
require.NoError(t, err)
acknowledgements, err := ss.PostAcknowledgement().GetForPosts([]string{p1.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack1, ack2})
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p2.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack3, ack4})
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack1, ack2, ack3, ack4})
err = ss.PostAcknowledgement().Delete(ack1)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack2, ack3, ack4})
err = ss.PostAcknowledgement().Delete(ack2)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack3, ack4})
err = ss.PostAcknowledgement().Delete(ack3)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id})
require.NoError(t, err)
require.ElementsMatch(t, acknowledgements, []*model.PostAcknowledgement{ack4})
err = ss.PostAcknowledgement().Delete(ack4)
require.NoError(t, err)
acknowledgements, err = ss.PostAcknowledgement().GetForPosts([]string{p1.Id, p2.Id})
require.NoError(t, err)
require.Empty(t, acknowledgements)
})
}

Просмотреть файл

@@ -57,6 +57,7 @@ type Store struct {
context context.Context
NotifyAdminStore mocks.NotifyAdminStore
PostPriorityStore mocks.PostPriorityStore
PostAcknowledgementStore mocks.PostAcknowledgementStore
}
func (s *Store) SetContext(context context.Context) { s.context = context }
@@ -102,17 +103,20 @@ func (s *Store) Group() store.GroupStore { return &s.GroupStore
func (s *Store) LinkMetadata() store.LinkMetadataStore { return &s.LinkMetadataStore }
func (s *Store) SharedChannel() store.SharedChannelStore { return &s.SharedChannelStore }
func (s *Store) PostPriority() store.PostPriorityStore { return &s.PostPriorityStore }
func (s *Store) MarkSystemRanUnitTests() { /* do nothing */ }
func (s *Store) Close() { /* do nothing */ }
func (s *Store) LockToMaster() { /* do nothing */ }
func (s *Store) UnlockFromMaster() { /* do nothing */ }
func (s *Store) DropAllTables() { /* do nothing */ }
func (s *Store) GetDbVersion(bool) (string, error) { return "", nil }
func (s *Store) GetInternalMasterDB() *sql.DB { return nil }
func (s *Store) GetInternalReplicaDB() *sql.DB { return nil }
func (s *Store) GetInternalReplicaDBs() []*sql.DB { return nil }
func (s *Store) RecycleDBConnections(time.Duration) {}
func (s *Store) GetDBSchemaVersion() (int, error) { return 1, nil }
func (s *Store) PostAcknowledgement() store.PostAcknowledgementStore {
return &s.PostAcknowledgementStore
}
func (s *Store) MarkSystemRanUnitTests() { /* do nothing */ }
func (s *Store) Close() { /* do nothing */ }
func (s *Store) LockToMaster() { /* do nothing */ }
func (s *Store) UnlockFromMaster() { /* do nothing */ }
func (s *Store) DropAllTables() { /* do nothing */ }
func (s *Store) GetDbVersion(bool) (string, error) { return "", nil }
func (s *Store) GetInternalMasterDB() *sql.DB { return nil }
func (s *Store) GetInternalReplicaDB() *sql.DB { return nil }
func (s *Store) GetInternalReplicaDBs() []*sql.DB { return nil }
func (s *Store) RecycleDBConnections(time.Duration) {}
func (s *Store) GetDBSchemaVersion() (int, error) { return 1, nil }
func (s *Store) GetAppliedMigrations() ([]model.AppliedMigration, error) {
return []model.AppliedMigration{}, nil
}
@@ -161,5 +165,6 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool {
&s.SharedChannelStore,
&s.NotifyAdminStore,
&s.PostPriorityStore,
&s.PostAcknowledgementStore,
)
}

Просмотреть файл

@@ -36,6 +36,7 @@ type TimerLayer struct {
OAuthStore store.OAuthStore
PluginStore store.PluginStore
PostStore store.PostStore
PostAcknowledgementStore store.PostAcknowledgementStore
PostPriorityStore store.PostPriorityStore
PreferenceStore store.PreferenceStore
ProductNoticesStore store.ProductNoticesStore
@@ -131,6 +132,10 @@ func (s *TimerLayer) Post() store.PostStore {
return s.PostStore
}
func (s *TimerLayer) PostAcknowledgement() store.PostAcknowledgementStore {
return s.PostAcknowledgementStore
}
func (s *TimerLayer) PostPriority() store.PostPriorityStore {
return s.PostPriorityStore
}
@@ -305,6 +310,11 @@ type TimerLayerPostStore struct {
Root *TimerLayer
}
type TimerLayerPostAcknowledgementStore struct {
store.PostAcknowledgementStore
Root *TimerLayer
}
type TimerLayerPostPriorityStore struct {
store.PostPriorityStore
Root *TimerLayer
@@ -5917,6 +5927,86 @@ func (s *TimerLayerPostStore) Update(newPost *model.Post, oldPost *model.Post) (
return result, err
}
func (s *TimerLayerPostAcknowledgementStore) Delete(acknowledgement *model.PostAcknowledgement) error {
start := time.Now()
err := s.PostAcknowledgementStore.Delete(acknowledgement)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.Delete", success, elapsed)
}
return err
}
func (s *TimerLayerPostAcknowledgementStore) Get(postID string, userID string) (*model.PostAcknowledgement, error) {
start := time.Now()
result, err := s.PostAcknowledgementStore.Get(postID, userID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.Get", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostAcknowledgementStore) GetForPost(postID string) ([]*model.PostAcknowledgement, error) {
start := time.Now()
result, err := s.PostAcknowledgementStore.GetForPost(postID)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.GetForPost", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostAcknowledgementStore) GetForPosts(postIds []string) ([]*model.PostAcknowledgement, error) {
start := time.Now()
result, err := s.PostAcknowledgementStore.GetForPosts(postIds)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.GetForPosts", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostAcknowledgementStore) Save(postID string, userID string, acknowledgedAt int64) (*model.PostAcknowledgement, error) {
start := time.Now()
result, err := s.PostAcknowledgementStore.Save(postID, userID, acknowledgedAt)
elapsed := float64(time.Since(start)) / float64(time.Second)
if s.Root.Metrics != nil {
success := "false"
if err == nil {
success = "true"
}
s.Root.Metrics.ObserveStoreMethodDuration("PostAcknowledgementStore.Save", success, elapsed)
}
return result, err
}
func (s *TimerLayerPostPriorityStore) GetForPost(postId string) (*model.PostPriority, error) {
start := time.Now()
@@ -11344,6 +11434,7 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay
newStore.OAuthStore = &TimerLayerOAuthStore{OAuthStore: childStore.OAuth(), Root: &newStore}
newStore.PluginStore = &TimerLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
newStore.PostStore = &TimerLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
newStore.PostAcknowledgementStore = &TimerLayerPostAcknowledgementStore{PostAcknowledgementStore: childStore.PostAcknowledgement(), Root: &newStore}
newStore.PostPriorityStore = &TimerLayerPostPriorityStore{PostPriorityStore: childStore.PostPriority(), Root: &newStore}
newStore.PreferenceStore = &TimerLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
newStore.ProductNoticesStore = &TimerLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}