[MM-43917] Cloud Freemium limits API: messages/posts (#20152)

* WIP - Add api and app funcs

* Add test cases

* Add utils testcases

* Exclude deleted posts

* Add doc for func

* Move api from cloud to usage

* Allow api access to authenticated users

* Change int to int64

* Fix lint issue

* Simplify err check

Co-authored-by: Ashish Bhate <ashish.bhate@mattermost.com>

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Ashish Bhate <ashish.bhate@mattermost.com>
Этот коммит содержится в:
Vishal
2022-05-17 17:00:40 +05:30
коммит произвёл GitHub
родитель 9c851e996c
Коммит fd703a365b
29 изменённых файлов: 341 добавлений и 43 удалений

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

@@ -137,6 +137,8 @@ type Routes struct {
InsightsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/top'
InsightsForUser *mux.Router // 'api/v4/users/me/top'
Usage *mux.Router // 'api/v4/usage'
}
type API struct {
@@ -261,6 +263,8 @@ func Init(srv *app.Server) (*API, error) {
api.BaseRoutes.InsightsForTeam = api.BaseRoutes.Team.PathPrefix("/top").Subrouter()
api.BaseRoutes.InsightsForUser = api.BaseRoutes.Users.PathPrefix("/me/top").Subrouter()
api.BaseRoutes.Usage = api.BaseRoutes.APIRoot.PathPrefix("/usage").Subrouter()
api.InitUser()
api.InitBot()
api.InitTeam()
@@ -303,6 +307,7 @@ func Init(srv *app.Server) (*API, error) {
api.InitPermissions()
api.InitExport()
api.InitInsights()
api.InitUsage()
if err := api.InitGraphQL(); err != nil {
return nil, err
}

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

@@ -9,11 +9,11 @@ import (
"os"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/einterfaces/mocks"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock"
)
func Test_getCloudLimits(t *testing.T) {

32
api4/usage.go Обычный файл
Просмотреть файл

@@ -0,0 +1,32 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"encoding/json"
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
)
func (api *API) InitUsage() {
// GET /api/v4/usage/posts
api.BaseRoutes.Usage.Handle("/posts", api.APISessionRequired(getPostsUsage)).Methods("GET")
}
func getPostsUsage(c *Context, w http.ResponseWriter, r *http.Request) {
count, appErr := c.App.GetPostsUsage()
if appErr != nil {
c.Err = model.NewAppError("Api4.getPostsUsage", "app.post.analytics_posts_count.app_error", nil, appErr.Error(), http.StatusInternalServerError)
return
}
json, err := json.Marshal(&model.PostsUsage{Count: count})
if err != nil {
c.Err = model.NewAppError("Api4.getPostsUsage", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return
}
w.Write(json)
}

39
api4/usage_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,39 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package api4
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetPostsUsage(t *testing.T) {
t.Run("unauthenticated users can not access", func(t *testing.T) {
th := Setup(t)
defer th.TearDown()
th.Client.Logout()
usage, r, err := th.Client.GetPostsUsage()
assert.Error(t, err)
assert.Nil(t, usage)
assert.Equal(t, http.StatusUnauthorized, r.StatusCode)
})
t.Run("good request returns response", func(t *testing.T) {
// Following calls create a total of 15 posts
th := Setup(t).InitBasic()
defer th.TearDown()
th.CreatePost()
th.CreatePost()
usage, r, err := th.Client.GetPostsUsage()
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, r.StatusCode)
assert.NotNil(t, usage)
assert.Equal(t, int64(10), usage.Count)
})
}

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

@@ -2102,7 +2102,7 @@ func TestPermanentDeleteAllUsers(t *testing.T) {
require.NoError(t, err)
require.Greater(t, len(users), 0)
postCount, err := th.App.Srv().Store.Post().AnalyticsPostCount("", false, false)
postCount, err := th.App.Srv().Store.Post().AnalyticsPostCount(&model.PostCountOptions{})
require.NoError(t, err)
require.Greater(t, postCount, int64(0))
@@ -2115,7 +2115,7 @@ func TestPermanentDeleteAllUsers(t *testing.T) {
require.NoError(t, err)
require.Len(t, users, 0)
postCount, err = th.App.Srv().Store.Post().AnalyticsPostCount("", false, false)
postCount, err = th.App.Srv().Store.Post().AnalyticsPostCount(&model.PostCountOptions{})
require.NoError(t, err)
require.Equal(t, postCount, int64(0))

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

@@ -87,7 +87,7 @@ func (a *App) GetAnalytics(name string, teamID string) (model.AnalyticsRows, *mo
if !skipIntensiveQueries {
g.Go(func() error {
var err error
if postsCount, err = a.Srv().Store.Post().AnalyticsPostCount(teamID, false, false); err != nil {
if postsCount, err = a.Srv().Store.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: teamID}); err != nil {
return model.NewAppError("GetAnalytics", "app.post.analytics_posts_count.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
@@ -269,7 +269,7 @@ func (a *App) GetAnalytics(name string, teamID string) (model.AnalyticsRows, *mo
if !skipIntensiveQueries {
g2.Go(func() error {
var err error
if filesCount, err = a.Srv().Store.Post().AnalyticsPostCount(teamID, true, false); err != nil {
if filesCount, err = a.Srv().Store.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: teamID, MustHaveFile: true}); err != nil {
return model.NewAppError("GetAnalytics", "app.post.analytics_posts_count.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
@@ -277,7 +277,7 @@ func (a *App) GetAnalytics(name string, teamID string) (model.AnalyticsRows, *mo
g2.Go(func() error {
var err error
if hashtagsCount, err = a.Srv().Store.Post().AnalyticsPostCount(teamID, false, true); err != nil {
if hashtagsCount, err = a.Srv().Store.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: teamID, MustHaveHashtag: true}); err != nil {
return model.NewAppError("GetAnalytics", "app.post.analytics_posts_count.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return nil

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

@@ -187,6 +187,8 @@ type AppIface interface {
// To get the plugins environment when the plugins are disabled, manually acquire the plugins
// lock instead.
GetPluginsEnvironment() *plugin.Environment
// GetPostsUsage returns "rounded off" total posts count like returns 900 instead of 987
GetPostsUsage() (int64, *model.AppError)
// GetProductNotices is called from the frontend to fetch the product notices that are relevant to the caller
GetProductNotices(c *request.Context, userID, teamID string, client model.NoticeClientType, clientVersion string, locale string) (model.NoticeMessages, *model.AppError)
// GetPublicKey will return the actual public key saved in the `name` file.

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

@@ -1964,7 +1964,7 @@ func TestImportimportMultiplePostLines(t *testing.T) {
require.Nil(t, err, "Failed to get user from database.")
// Count the number of posts in the testing team.
initialPostCount, nErr := th.App.Srv().Store.Post().AnalyticsPostCount(team.Id, false, false)
initialPostCount, nErr := th.App.Srv().Store.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: team.Id})
require.NoError(t, nErr)
// Try adding an invalid post in dry run mode.
@@ -2470,7 +2470,7 @@ func TestImportimportMultiplePostLines(t *testing.T) {
require.Nil(t, err, "Failed to get channel from database.")
// Count the number of posts in the team2.
initialPostCountForTeam2, nErr := th.App.Srv().Store.Post().AnalyticsPostCount(team2.Id, false, false)
initialPostCountForTeam2, nErr := th.App.Srv().Store.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: team2.Id})
require.NoError(t, nErr)
// Try adding two valid posts in apply mode.
@@ -2576,7 +2576,7 @@ func TestImportImportPost(t *testing.T) {
require.Nil(t, appErr, "Failed to get user from database.")
// Count the number of posts in the testing team.
initialPostCount, nErr := th.App.Srv().Store.Post().AnalyticsPostCount(team.Id, false, false)
initialPostCount, nErr := th.App.Srv().Store.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: team.Id})
require.NoError(t, nErr)
time := model.GetMillis()
@@ -3283,7 +3283,7 @@ func TestImportImportDirectPost(t *testing.T) {
directChannel = channel
// Get the number of posts in the system.
result, err := th.App.Srv().Store.Post().AnalyticsPostCount("", false, false)
result, err := th.App.Srv().Store.Post().AnalyticsPostCount(&model.PostCountOptions{})
require.NoError(t, err)
initialPostCount := result
initialDate := model.GetMillis()
@@ -3644,7 +3644,7 @@ func TestImportImportDirectPost(t *testing.T) {
groupChannel = channel
// Get the number of posts in the system.
result, nErr := th.App.Srv().Store.Post().AnalyticsPostCount("", false, false)
result, nErr := th.App.Srv().Store.Post().AnalyticsPostCount(&model.PostCountOptions{})
require.NoError(t, nErr)
initialPostCount = result

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

@@ -68,7 +68,7 @@ func checkNoError(t *testing.T, err *model.AppError) {
}
func AssertAllPostsCount(t *testing.T, a *App, initialCount int64, change int64, teamName string) {
result, err := a.Srv().Store.Post().AnalyticsPostCount(teamName, false, false)
result, err := a.Srv().Store.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: teamName})
require.NoError(t, err)
require.Equal(t, initialCount+change, result, "Did not find the expected number of posts.")
}

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

@@ -464,7 +464,7 @@ func (s *Server) doFirstAdminSetupCompleteMigration() {
}
// if there are teams, then if this isn't a new installation, there should be posts
postCount, err := s.Store.Post().AnalyticsPostCount("", false, false)
postCount, err := s.Store.Post().AnalyticsPostCount(&model.PostCountOptions{})
if err != nil || postCount < existingInstallationPostsThreshold {
return
}

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

@@ -7832,6 +7832,28 @@ func (a *OpenTracingAppLayer) GetPostsSince(options model.GetPostsSinceOptions)
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPostsUsage() (int64, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPostsUsage")
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.GetPostsUsage()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetPreferenceByCategoryAndNameForUser(userID string, category string, preferenceName string) (*model.Preference, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetPreferenceByCategoryAndNameForUser")

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

@@ -343,7 +343,7 @@ func (a *App) UpdateProductNotices() *model.AppError {
skip := *a.Config().AnnouncementSettings.NoticesSkipCache
mlog.Debug("Will fetch notices from", mlog.String("url", url), mlog.Bool("skip_cache", skip))
var err error
a.ch.cachedPostCount, err = a.Srv().Store.Post().AnalyticsPostCount("", false, false)
a.ch.cachedPostCount, err = a.Srv().Store.Post().AnalyticsPostCount(&model.PostCountOptions{})
if err != nil {
mlog.Warn("Failed to fetch post count", mlog.String("error", err.Error()))
}

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

@@ -0,0 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"net/http"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/utils"
)
// GetPostsUsage returns "rounded off" total posts count like returns 900 instead of 987
func (a *App) GetPostsUsage() (int64, *model.AppError) {
count, err := a.Srv().Store.Post().AnalyticsPostCount(&model.PostCountOptions{ExcludeDeleted: true})
if err != nil {
return 0, model.NewAppError("GetPostsUsage", "app.post.analytics_posts_count.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return utils.RoundOffToZeroes(float64(count)), nil
}

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

@@ -0,0 +1,49 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/mattermost/mattermost-server/v6/store/storetest/mocks"
)
func TestGetPostsUsage(t *testing.T) {
t.Run("returns error when AnalyticsPostCount fails", func(t *testing.T) {
th := SetupWithStoreMock(t)
defer th.TearDown()
errMsg := "Test posts count error"
mockStore := th.App.Srv().Store.(*mocks.Store)
mockPostStore := mocks.PostStore{}
mockPostStore.On("AnalyticsPostCount", mock.Anything).Return(int64(0), errors.New(errMsg))
mockStore.On("Post").Return(&mockPostStore)
usage, appErr := th.App.GetPostsUsage()
assert.Zero(t, usage)
assert.ErrorContains(t, appErr, errMsg)
})
t.Run("returns rounded off count when AnalyticsPostCount returns valid count", func(t *testing.T) {
th := SetupWithStoreMock(t)
defer th.TearDown()
var mockCount int64 = 4321
var expected int64 = 4000
mockStore := th.App.Srv().Store.(*mocks.Store)
mockPostStore := mocks.PostStore{}
mockPostStore.On("AnalyticsPostCount", mock.Anything).Return(mockCount, nil)
mockStore.On("Post").Return(&mockPostStore)
count, appErr := th.App.GetPostsUsage()
assert.Nil(t, appErr)
assert.Equal(t, expected, count)
})
}

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

@@ -324,6 +324,10 @@ func (c *Client4) cloudRoute() string {
return "/cloud"
}
func (c *Client4) usageRoute() string {
return "/usage"
}
func (c *Client4) testEmailRoute() string {
return "/email/test"
}
@@ -8078,3 +8082,18 @@ func (c *Client4) GetAppliedSchemaMigrations() ([]AppliedMigration, *Response, e
}
return list, BuildResponse(r), nil
}
// Usage Section
// GetPostsUsage returns rounded off total usage of posts for the instance
func (c *Client4) GetPostsUsage() (*PostsUsage, *Response, error) {
r, err := c.DoAPIGet(c.usageRoute()+"/posts", "")
if err != nil {
return nil, BuildResponse(r), err
}
defer closeBody(r)
var usage *PostsUsage
err = json.NewDecoder(r.Body).Decode(&usage)
return usage, BuildResponse(r), err
}

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

@@ -274,6 +274,14 @@ type GetPostsOptions struct {
Direction string // Only accepts up|down. Indicates the order in which to send the items.
}
type PostCountOptions struct {
// Only include posts on a specific team. "" for any team.
TeamId string
MustHaveFile bool
MustHaveHashtag bool
ExcludeDeleted bool
}
func (o *Post) Etag() string {
return Etag(o.Id, o.UpdateAt)
}

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

@@ -0,0 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
type PostsUsage struct {
Count int64 `json:"count"`
}

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

@@ -212,7 +212,7 @@ func (worker *BleveIndexerWorker) DoJob(job *model.Job) {
// Counting all posts may fail or timeout when the posts table is large. If this happens, log a warning, but carry
// on with the indexing job anyway. The only issue is that the progress % reporting will be inaccurate.
if count, err := worker.jobServer.Store.Post().AnalyticsPostCount("", false, false); err != nil {
if count, err := worker.jobServer.Store.Post().AnalyticsPostCount(&model.PostCountOptions{}); err != nil {
mlog.Warn("Worker: Failed to fetch total post count for job. An estimated value will be used for progress reporting.", mlog.String("workername", worker.name), mlog.String("job_id", job.Id), mlog.Err(err))
progress.TotalPostsCount = estimatedPostCount
} else {

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

@@ -297,7 +297,7 @@ func (ts *TelemetryService) trackActivity() {
deletedPrivateChannelCount = dpccr
}
postsCount, _ = ts.dbStore.Post().AnalyticsPostCount("", false, false)
postsCount, _ = ts.dbStore.Post().AnalyticsPostCount(&model.PostCountOptions{})
postCountsOptions := &model.AnalyticsPostCountsOptions{TeamId: "", BotsOnly: false, YesterdayOnly: true}
postCountsYesterday, _ := ts.dbStore.Post().AnalyticsPostCountsByDay(postCountsOptions)

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

@@ -112,7 +112,7 @@ func initializeMocks(cfg *model.Config) (*mocks.ServerIface, *storeMocks.Store,
channelStore.On("GroupSyncedChannelCount").Return(int64(17), nil)
postStore := storeMocks.PostStore{}
postStore.On("AnalyticsPostCount", "", false, false).Return(int64(1000), nil)
postStore.On("AnalyticsPostCount", &model.PostCountOptions{}).Return(int64(1000), nil)
postStore.On("AnalyticsPostCountsByDay", &model.AnalyticsPostCountsOptions{TeamId: "", BotsOnly: false, YesterdayOnly: true}).Return(model.AnalyticsRows{}, nil)
postStore.On("AnalyticsPostCountsByDay", &model.AnalyticsPostCountsOptions{TeamId: "", BotsOnly: true, YesterdayOnly: true}).Return(model.AnalyticsRows{}, nil)

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

@@ -5365,7 +5365,7 @@ func (s *OpenTracingLayerPluginStore) SetWithOptions(pluginID string, key string
return result, err
}
func (s *OpenTracingLayerPostStore) AnalyticsPostCount(teamID string, mustHaveFile bool, mustHaveHashtag bool) (int64, error) {
func (s *OpenTracingLayerPostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
origCtx := s.Root.Store.Context()
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "PostStore.AnalyticsPostCount")
s.Root.Store.SetContext(newCtx)
@@ -5374,7 +5374,7 @@ func (s *OpenTracingLayerPostStore) AnalyticsPostCount(teamID string, mustHaveFi
}()
defer span.Finish()
result, err := s.PostStore.AnalyticsPostCount(teamID, mustHaveFile, mustHaveHashtag)
result, err := s.PostStore.AnalyticsPostCount(options)
if err != nil {
span.LogFields(spanlog.Error(err))
ext.Error.Set(span, true)

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

@@ -6094,11 +6094,11 @@ func (s *RetryLayerPluginStore) SetWithOptions(pluginID string, key string, valu
}
func (s *RetryLayerPostStore) AnalyticsPostCount(teamID string, mustHaveFile bool, mustHaveHashtag bool) (int64, error) {
func (s *RetryLayerPostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
tries := 0
for {
result, err := s.PostStore.AnalyticsPostCount(teamID, mustHaveFile, mustHaveHashtag)
result, err := s.PostStore.AnalyticsPostCount(options)
if err == nil {
return result, nil
}

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

@@ -2128,25 +2128,29 @@ func (s *SqlPostStore) AnalyticsPostCountsByDay(options *model.AnalyticsPostCoun
return rows, nil
}
func (s *SqlPostStore) AnalyticsPostCount(teamId string, mustHaveFile bool, mustHaveHashtag bool) (int64, error) {
func (s *SqlPostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
query := s.getQueryBuilder().
Select("COUNT(p.Id) AS Value").
From("Posts p")
if teamId != "" {
if options.TeamId != "" {
query = query.
Join("Channels c ON (c.Id = p.ChannelId)").
Where(sq.Eq{"c.TeamId": teamId})
Where(sq.Eq{"c.TeamId": options.TeamId})
}
if mustHaveFile {
if options.MustHaveFile {
query = query.Where(sq.Or{sq.NotEq{"p.FileIds": "[]"}, sq.NotEq{"p.Filenames": "[]"}})
}
if mustHaveHashtag {
if options.MustHaveHashtag {
query = query.Where(sq.NotEq{"p.Hashtags": ""})
}
if options.ExcludeDeleted {
query = query.Where(sq.Eq{"p.DeleteAt": 0})
}
queryString, args, err := query.ToSql()
if err != nil {
return 0, errors.Wrap(err, "post_tosql")

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

@@ -348,7 +348,7 @@ type PostStore interface {
Search(teamID string, userID string, params *model.SearchParams) (*model.PostList, error)
AnalyticsUserCountsWithPostsByDay(teamID string) (model.AnalyticsRows, error)
AnalyticsPostCountsByDay(options *model.AnalyticsPostCountsOptions) (model.AnalyticsRows, error)
AnalyticsPostCount(teamID string, mustHaveFile bool, mustHaveHashtag bool) (int64, error)
AnalyticsPostCount(options *model.PostCountOptions) (int64, error)
ClearCaches()
InvalidateLastPostTimeCache(channelID string)
GetLastPostRowCreateAt() (int64, error)

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

@@ -16,20 +16,20 @@ type PostStore struct {
mock.Mock
}
// AnalyticsPostCount provides a mock function with given fields: teamID, mustHaveFile, mustHaveHashtag
func (_m *PostStore) AnalyticsPostCount(teamID string, mustHaveFile bool, mustHaveHashtag bool) (int64, error) {
ret := _m.Called(teamID, mustHaveFile, mustHaveHashtag)
// AnalyticsPostCount provides a mock function with given fields: options
func (_m *PostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
ret := _m.Called(options)
var r0 int64
if rf, ok := ret.Get(0).(func(string, bool, bool) int64); ok {
r0 = rf(teamID, mustHaveFile, mustHaveHashtag)
if rf, ok := ret.Get(0).(func(*model.PostCountOptions) int64); ok {
r0 = rf(options)
} else {
r0 = ret.Get(0).(int64)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool, bool) error); ok {
r1 = rf(teamID, mustHaveFile, mustHaveHashtag)
if rf, ok := ret.Get(1).(func(*model.PostCountOptions) error); ok {
r1 = rf(options)
} else {
r1 = ret.Error(1)
}

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

@@ -2115,30 +2115,39 @@ func testPostCountsByDay(t *testing.T, ss store.Store) {
require.NoError(t, err)
assert.Equal(t, float64(1), r1[0].Value)
// total
r2, err := ss.Post().AnalyticsPostCount(t1.Id, false, false)
// total for single team
r2, err := ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id})
require.NoError(t, err)
assert.Equal(t, int64(6), r2)
// total across teams
r2, err = ss.Post().AnalyticsPostCount("", false, false)
r2, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{})
require.NoError(t, err)
assert.GreaterOrEqual(t, r2, int64(6))
// total across teams with files
r2, err = ss.Post().AnalyticsPostCount("", true, false)
r2, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{MustHaveFile: true})
require.NoError(t, err)
assert.GreaterOrEqual(t, r2, int64(3))
// total across teams with hastags
r2, err = ss.Post().AnalyticsPostCount("", false, true)
// total across teams with hashtags
r2, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{MustHaveHashtag: true})
require.NoError(t, err)
assert.GreaterOrEqual(t, r2, int64(2))
// total across teams with hastags and files
r2, err = ss.Post().AnalyticsPostCount("", true, true)
r2, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{MustHaveFile: true, MustHaveHashtag: true})
require.NoError(t, err)
assert.GreaterOrEqual(t, r2, int64(1))
// delete 1 post
err = ss.Post().Delete(o1.Id, 1, o1.UserId)
require.NoError(t, err)
// total for single team with the deleted post excluded
r2, err = ss.Post().AnalyticsPostCount(&model.PostCountOptions{TeamId: t1.Id, ExcludeDeleted: true})
require.NoError(t, err)
assert.Equal(t, int64(5), r2)
}
func testPostStoreGetFlaggedPostsForTeam(t *testing.T, ss store.Store, s SqlStore) {

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

@@ -4859,10 +4859,10 @@ func (s *TimerLayerPluginStore) SetWithOptions(pluginID string, key string, valu
return result, err
}
func (s *TimerLayerPostStore) AnalyticsPostCount(teamID string, mustHaveFile bool, mustHaveHashtag bool) (int64, error) {
func (s *TimerLayerPostStore) AnalyticsPostCount(options *model.PostCountOptions) (int64, error) {
start := timemodule.Now()
result, err := s.PostStore.AnalyticsPostCount(teamID, mustHaveFile, mustHaveHashtag)
result, err := s.PostStore.AnalyticsPostCount(options)
elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second)
if s.Root.Metrics != nil {

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

@@ -5,6 +5,7 @@ package utils
import (
"io/ioutil"
"math"
"net"
"net/http"
"net/url"
@@ -215,3 +216,16 @@ func IsValidMobileAuthRedirectURL(config *model.Config, redirectURL string) bool
}
return false
}
// RoundOffToZeroes converts all digits to 0 except the 1st one.
// Special case: If there is only 1 digit, then returns 0.
func RoundOffToZeroes(n float64) int64 {
if n >= -9 && n <= 9 {
return 0
}
zeroes := int(math.Log10(math.Abs(n)))
tens := int64(math.Pow10(zeroes))
firstDigit := int64(n) / tens
return firstDigit * tens
}

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

@@ -170,3 +170,69 @@ func TestAppendQueryParamsToURL(t *testing.T) {
expected := url + "?key1=value1&key2=value2"
assert.Equal(t, redirectURL, expected)
}
func TestRoundOffToZeroes(t *testing.T) {
testCases := []struct {
desc string
n float64
expected int64
}{
{
desc: "returns 0 when n is 0",
n: 0,
expected: 0,
},
{
desc: "returns 0 when n is 9",
n: 9,
expected: 0,
},
{
desc: "returns 10 when n is 10",
n: 10,
expected: 10,
},
{
desc: "returns 90 when n is 99",
n: 99,
expected: 90,
},
{
desc: "returns 100 when n is 100",
n: 100,
expected: 100,
},
{
desc: "returns 100 when n is 101",
n: 101,
expected: 100,
},
{
desc: "returns 4000 when n is 4321",
n: 4321,
expected: 4000,
},
{
desc: "returns 0 when n is -9",
n: -9,
expected: 0,
},
{
desc: "returns -4000 when n is -4321",
n: -4321,
expected: -4000,
},
{
desc: "returns 4000 when n is 4321.235",
n: 4321.235,
expected: 4000,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.desc, func(t *testing.T) {
res := RoundOffToZeroes(tc.n)
assert.Equal(t, tc.expected, res)
})
}
}