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 удалений

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

@@ -4,14 +4,18 @@
package model
import (
"net/http"
"time"
)
type PostCountGrouping string
const (
TimeRangeToday string = "today"
TimeRange7Day string = "7_day"
TimeRange28Day string = "28_day"
PostsByHour PostCountGrouping = "hour"
PostsByDay PostCountGrouping = "day"
)
type InsightsOpts struct {
@@ -38,7 +42,16 @@ type TopReaction struct {
// Top Channels
type TopChannelList struct {
InsightsListData
Items []*TopChannel `json:"items"`
Items []*TopChannel `json:"items"`
PostCountByDuration ChannelPostCountByDuration `json:"channel_post_counts_by_duration"`
}
func (t *TopChannelList) ChannelIDs() []string {
var ids []string
for _, item := range t.Items {
ids = append(ids, item.ID)
}
return ids
}
type TopChannel struct {
@@ -50,21 +63,116 @@ type TopChannel struct {
MessageCount int64 `json:"message_count"`
}
// GetStartUnixMilliForTimeRange gets the unix start time in milliseconds from the given time range.
// Time range can be one of: "1_day", "7_day", or "28_day".
func GetStartUnixMilliForTimeRange(timeRange string) (int64, *AppError) {
now := time.Now()
_, offset := now.Zone()
type DurationPostCount struct {
ChannelID string `db:"channelid"`
// Duration is an ISO8601 date string representing either a day or a day and hour (ex. "2022-05-26" or "2022-05-26T14").
Duration string `db:"duration"`
PostCount int `db:"postcount"`
}
func TimeRangeToNumberDays(timeRange string) int {
var n int
switch timeRange {
case TimeRangeToday:
return GetStartOfDayMillis(now, offset), nil
n = 1
case TimeRange7Day:
return GetStartOfDayMillis(now.Add(time.Hour*time.Duration(-168)), offset), nil
n = 7
case TimeRange28Day:
return GetStartOfDayMillis(now.Add(time.Hour*time.Duration(-672)), offset), nil
n = 28
}
return n
}
// ChannelPostCountByDuration contains a count of posts by channel id, grouped by ISO8601 date string.
// Example 1 (grouped by day):
// cpc := model.ChannelPostCountByDuration{
// "2009-11-11": {
// "ezbp7nqxzjgdir8riodyafr9ww": 90,
// "p949c1xdojfgzffxma3p3s3ikr": 201,
// },
// "2009-11-12": {
// "ezbp7nqxzjgdir8riodyafr9ww": 45,
// "p949c1xdojfgzffxma3p3s3ikr": 68,
// },
// }
// Example 2 (grouped by hour):
// cpc := model.ChannelPostCountByDuration{
// "2009-11-11T01": {
// "ezbp7nqxzjgdir8riodyafr9ww": 90,
// "p949c1xdojfgzffxma3p3s3ikr": 201,
// },
// "2009-11-11T02": {
// "ezbp7nqxzjgdir8riodyafr9ww": 45,
// "p949c1xdojfgzffxma3p3s3ikr": 68,
// },
// }
type ChannelPostCountByDuration map[string]map[string]int
func blankChannelCountsMap(channelIDs []string) map[string]int {
blankChannelCounts := map[string]int{}
for _, id := range channelIDs {
blankChannelCounts[id] = 0
}
return blankChannelCounts
}
func ToDailyPostCountViewModel(dpc []*DurationPostCount, startTime *time.Time, numDays int, channelIDs []string) ChannelPostCountByDuration {
viewModel := ChannelPostCountByDuration{}
keyTime := *startTime
nowAtLocation := time.Now().In(startTime.Location())
if numDays == 1 {
for keyTime.Before(nowAtLocation) {
dateTimeKey := keyTime.Format(time.RFC3339)
viewModel[dateTimeKey] = blankChannelCountsMap(channelIDs)
keyTime = keyTime.Add(time.Hour)
}
} else {
for keyTime.Before(nowAtLocation) {
dateTimeKey := keyTime.Format("2006-01-02")
viewModel[dateTimeKey] = blankChannelCountsMap(channelIDs)
keyTime = keyTime.Add(24 * time.Hour)
}
}
return GetStartOfDayMillis(now, offset), NewAppError("Insights.IsValidRequest", "model.insights.time_range.app_error", nil, "", http.StatusBadRequest)
for _, item := range dpc {
var parseFormat string
var keyFormat string
if numDays == 1 {
parseFormat = "2006-01-02T15 "
keyFormat = time.RFC3339
} else {
parseFormat = "2006-01-02"
keyFormat = parseFormat
}
durTime, err := time.ParseInLocation(parseFormat, item.Duration, startTime.Location())
if err != nil {
continue
}
localizedKey := durTime.Format(keyFormat)
_, hasKey := viewModel[localizedKey]
if !hasKey {
viewModel[localizedKey] = map[string]int{}
}
viewModel[localizedKey][item.ChannelID] = item.PostCount
}
return viewModel
}
// StartOfDayForTimeRange gets the unix start time in milliseconds from the given time range.
// Time range can be one of: "today", "7_day", or "28_day".
func StartOfDayForTimeRange(timeRange string, location *time.Location) *time.Time {
now := time.Now().In(location)
resultTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location)
switch timeRange {
case TimeRange7Day:
resultTime = resultTime.Add(time.Hour * time.Duration(-144))
case TimeRange28Day:
resultTime = resultTime.Add(time.Hour * time.Duration(-648))
}
return &resultTime
}
// GetTopReactionListWithPagination adds a rank to each item in the given list of TopReaction and checks if there is

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

@@ -9,26 +9,6 @@ import (
"github.com/stretchr/testify/assert"
)
func TestGetStartUnixMilliForTimeRang(t *testing.T) {
tc := [3]string{"today", "7_day", "28_day"}
for _, timeRange := range tc {
t.Run(timeRange, func(t *testing.T) {
_, err := GetStartUnixMilliForTimeRange(timeRange)
assert.Nil(t, err)
})
}
invalidTimeRanges := [3]string{"", "1_day", "10_day"}
for _, timeRange := range invalidTimeRanges {
t.Run(timeRange, func(t *testing.T) {
_, err := GetStartUnixMilliForTimeRange(timeRange)
assert.NotNil(t, err)
})
}
}
func TestGetTopReactionListWithPagination(t *testing.T) {
reactions := []*TopReaction{
{EmojiName: "smile", Count: 200},

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

@@ -11,6 +11,7 @@ import (
"regexp"
"sort"
"strings"
"time"
"unicode/utf8"
"golang.org/x/crypto/bcrypt"
@@ -782,6 +783,14 @@ func (u *User) GetPreferredTimezone() string {
return GetPreferredTimezone(u.Timezone)
}
func (u *User) GetTimezoneLocation() *time.Location {
loc, _ := time.LoadLocation(u.GetPreferredTimezone())
if loc == nil {
loc = time.Now().UTC().Location()
}
return loc
}
// IsRemote returns true if the user belongs to a remote cluster (has RemoteId).
func (u *User) IsRemote() bool {
return u.RemoteId != nil && *u.RemoteId != ""