MM-43956: Adds post counts by duration. (#20131)

* MM-43956: Adds post counts by day.

* MM-43956: Test data cleanup. Switch from Unix to UnixMilli.

* MM-43956: Changes from selecting date data types to strings in SQL.

* MM-43956: Fixes date format key.

* MM-43956: Adds missing user id scope for 'my' top channels graph.

* MM-43956: Adds the ability to group post counts by hour.

* MM-43956: Require enterprise or professional license. Reject guests.

* MM-43956: Adds license for tests.

* MM-43956: Renames function.

* MM-43956: Omits future hours from post counts by hour.

* MM-43956: Adjust API response grouping to users timezone.

* MM-43956: Adds translation.

* MM-43956: Adds user's timezone to the data tier for the grouping by day and hour.

* MM-43956: Fixes layers.

* MM-43956: Lint fix.

* MM-43956: Fix store layers.

* MM-43956: Switches to default name for time package; changes parameter names to avoid naming conflict.

* MM-43956: Updates mocks.

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
Martin Kraft
2022-06-13 16:22:34 -04:00
коммит произвёл GitHub
родитель c03eb778c8
Коммит 182ae1234a
29 изменённых файлов: 2055 добавлений и 1533 удалений

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

@@ -209,3 +209,24 @@ func requireLicense(f handlerFunc) handlerFunc {
f(c, w, r)
}
}
func minimumProfessionalLicense(f handlerFunc) handlerFunc {
return func(c *Context, w http.ResponseWriter, r *http.Request) {
lic := c.App.Srv().License()
if lic == nil || (lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise) {
c.Err = model.NewAppError("", "api.license_error.professional_or_enterprise", nil, "", http.StatusNotImplemented)
return
}
f(c, w, r)
}
}
func rejectGuests(f handlerFunc) handlerFunc {
return func(c *Context, w http.ResponseWriter, r *http.Request) {
if c.AppContext.Session().Props[model.SessionPropIsGuest] == "true" {
c.Err = model.NewAppError("", "api.authorization_error.guest", nil, "", http.StatusNotImplemented)
return
}
f(c, w, r)
}
}

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

@@ -6,18 +6,20 @@ package api4
import (
"encoding/json"
"net/http"
"time"
"github.com/mattermost/mattermost-server/v6/app"
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitInsights() {
// Reactions
api.BaseRoutes.InsightsForTeam.Handle("/reactions", api.APISessionRequired(getTopReactionsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/reactions", api.APISessionRequired(getTopReactionsForUserSince)).Methods("GET")
api.BaseRoutes.InsightsForTeam.Handle("/reactions", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopReactionsForTeamSince)))).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/reactions", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopReactionsForUserSince)))).Methods("GET")
// Channels
api.BaseRoutes.InsightsForTeam.Handle("/channels", api.APISessionRequired(getTopChannelsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/channels", api.APISessionRequired(getTopChannelsForUserSince)).Methods("GET")
api.BaseRoutes.InsightsForTeam.Handle("/channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopChannelsForTeamSince)))).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopChannelsForUserSince)))).Methods("GET")
}
// Top Reactions
@@ -39,14 +41,16 @@ func getTopReactionsForTeamSince(c *Context, w http.ResponseWriter, r *http.Requ
return
}
startTime, err := model.GetStartUnixMilliForTimeRange(c.Params.TimeRange)
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
topReactionList, err := c.App.GetTopReactionsForTeamSince(c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime,
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
@@ -86,14 +90,16 @@ func getTopReactionsForUserSince(c *Context, w http.ResponseWriter, r *http.Requ
}
}
startTime, err := model.GetStartUnixMilliForTimeRange(c.Params.TimeRange)
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
topReactionList, err := c.App.GetTopReactionsForUserSince(c.AppContext.Session().UserId, c.Params.TeamId, &model.InsightsOpts{
StartUnixMilli: startTime,
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
@@ -130,14 +136,17 @@ func getTopChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reque
return
}
startTime, err := model.GetStartUnixMilliForTimeRange(c.Params.TimeRange)
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
loc := user.GetTimezoneLocation()
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc)
topChannels, err := c.App.GetTopChannelsForTeamSince(c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime,
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
@@ -146,6 +155,12 @@ func getTopChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reque
return
}
topChannels.PostCountByDuration, err = postCountByDurationViewModel(c.App, topChannels, startTime, c.Params.TimeRange, nil, loc)
if err != nil {
c.Err = err
return
}
js, jsonErr := json.Marshal(topChannels)
if jsonErr != nil {
c.Err = model.NewAppError("getTopChannelsForTeamSince", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
@@ -177,14 +192,17 @@ func getTopChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Reque
}
}
startTime, err := model.GetStartUnixMilliForTimeRange(c.Params.TimeRange)
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
loc := user.GetTimezoneLocation()
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc)
topChannels, err := c.App.GetTopChannelsForUserSince(c.AppContext.Session().UserId, c.Params.TeamId, &model.InsightsOpts{
StartUnixMilli: startTime,
StartUnixMilli: startTime.UnixMilli(),
Page: c.Params.Page,
PerPage: c.Params.PerPage,
})
@@ -194,6 +212,12 @@ func getTopChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Reque
return
}
topChannels.PostCountByDuration, err = postCountByDurationViewModel(c.App, topChannels, startTime, c.Params.TimeRange, &c.AppContext.Session().UserId, loc)
if err != nil {
c.Err = err
return
}
js, jsonErr := json.Marshal(topChannels)
if jsonErr != nil {
c.Err = model.NewAppError("getTopChannelsForUserSince", "api.marshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
@@ -202,3 +226,23 @@ func getTopChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Reque
w.Write(js)
}
// postCountByDurationViewModel expects a list of channels that are pre-authorized for the given user to view.
func postCountByDurationViewModel(app app.AppIface, topChannelList *model.TopChannelList, startTime *time.Time, timeRange string, userID *string, location *time.Location) (model.ChannelPostCountByDuration, *model.AppError) {
if len(topChannelList.Items) == 0 {
return nil, nil
}
var postCountsByDay []*model.DurationPostCount
channelIDs := topChannelList.ChannelIDs()
var grouping model.PostCountGrouping
if timeRange == model.TimeRangeToday {
grouping = model.PostsByHour
} else {
grouping = model.PostsByDay
}
postCountsByDay, err := app.PostCountsByDuration(channelIDs, startTime.UnixMilli(), userID, grouping, location)
if err != nil {
return nil, err
}
return model.ToDailyPostCountViewModel(postCountsByDay, startTime, model.TimeRangeToNumberDays(timeRange), channelIDs), nil
}

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

@@ -21,6 +21,7 @@ func TestGetTopReactionsForTeamSince(t *testing.T) {
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
client := th.Client
@@ -246,6 +247,7 @@ func TestGetTopReactionsForUserSince(t *testing.T) {
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
client := th.Client
@@ -437,6 +439,7 @@ func TestGetTopChannelsForTeamSince(t *testing.T) {
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
client := th.Client
userId := th.BasicUser.Id
@@ -485,6 +488,10 @@ func TestGetTopChannelsForTeamSince(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, channel6.Id, topChannels.Items[0].ID)
assert.Equal(t, int64(1), topChannels.Items[0].MessageCount)
t.Run("has post count by day", func(t *testing.T) {
require.NotNil(t, topChannels.PostCountByDuration)
})
})
t.Run("get-top-channels-for-user-since exclude channels user is not member of", func(t *testing.T) {
@@ -531,6 +538,7 @@ func TestGetTopChannelsForUserSince(t *testing.T) {
th.ConfigStore.SetReadOnlyFF(false)
defer th.ConfigStore.SetReadOnlyFF(true)
th.App.UpdateConfig(func(cfg *model.Config) { cfg.FeatureFlags.InsightsEnabled = true })
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuProfessional))
client := th.Client
userId := th.BasicUser.Id
@@ -579,6 +587,10 @@ func TestGetTopChannelsForUserSince(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, channel6.Id, topChannels.Items[0].ID)
assert.Equal(t, int64(1), topChannels.Items[0].MessageCount)
t.Run("has post count by day", func(t *testing.T) {
require.NotNil(t, topChannels.PostCountByDuration)
})
})
t.Run("get-top-channels-for-user-since invalid team id", func(t *testing.T) {