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>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
c03eb778c8
Коммит
182ae1234a
@@ -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 != ""
|
||||
|
||||
Ссылка в новой задаче
Block a user