[MM-46692] Channel group member count (#21270)
* tools updates * Revert "tools updates" This reverts commit 6293297b55803c5a263e200ebd80192899666ae9. * adding channel member count to groups request * fixing models * adding a new test * removing unused var Co-authored-by: Benjamin Cooke <benjamincooke@Benjamins-MacBook-Pro.local> Co-authored-by: Benjamin Cooke <benjamincooke@Benjamins-MBP.ht.home> Co-authored-by: Benjamin Cooke <benjamincooke@Benjamins-MBP.fritz.box> Co-authored-by: Benjamin Cooke <benjamincooke@Benjamins-MacBook-Pro.fritz.box> Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
@@ -950,12 +950,13 @@ func getGroupsAssociatedToChannelsByTeam(c *Context, w http.ResponseWriter, r *h
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
var teamID, NotAssociatedToChannelID, ChannelIDForMemberCount string
|
||||||
|
|
||||||
permissionErr := requireLicense(c)
|
permissionErr := requireLicense(c)
|
||||||
if permissionErr != nil {
|
if permissionErr != nil {
|
||||||
c.Err = permissionErr
|
c.Err = permissionErr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var teamID, channelID string
|
|
||||||
|
|
||||||
source := c.Params.GroupSource
|
source := c.Params.GroupSource
|
||||||
|
|
||||||
@@ -964,7 +965,11 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if id := c.Params.NotAssociatedToChannel; model.IsValidId(id) {
|
if id := c.Params.NotAssociatedToChannel; model.IsValidId(id) {
|
||||||
channelID = id
|
NotAssociatedToChannelID = id
|
||||||
|
}
|
||||||
|
|
||||||
|
if id := c.Params.IncludeChannelMemberCount; model.IsValidId(id) {
|
||||||
|
ChannelIDForMemberCount = id
|
||||||
}
|
}
|
||||||
|
|
||||||
// If they specify the group_source as custom when the feature is disabled, throw an error
|
// If they specify the group_source as custom when the feature is disabled, throw an error
|
||||||
@@ -979,6 +984,8 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
source = model.GroupSourceLdap
|
source = model.GroupSourceLdap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
includeTimezones := r.URL.Query().Get("include_timezones") == "true"
|
||||||
|
|
||||||
opts := model.GroupSearchOpts{
|
opts := model.GroupSearchOpts{
|
||||||
Q: c.Params.Q,
|
Q: c.Params.Q,
|
||||||
IncludeMemberCount: c.Params.IncludeMemberCount,
|
IncludeMemberCount: c.Params.IncludeMemberCount,
|
||||||
@@ -986,6 +993,7 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
FilterParentTeamPermitted: c.Params.FilterParentTeamPermitted,
|
FilterParentTeamPermitted: c.Params.FilterParentTeamPermitted,
|
||||||
Source: source,
|
Source: source,
|
||||||
FilterHasMember: c.Params.FilterHasMember,
|
FilterHasMember: c.Params.FilterHasMember,
|
||||||
|
IncludeTimezones: includeTimezones,
|
||||||
}
|
}
|
||||||
|
|
||||||
if teamID != "" {
|
if teamID != "" {
|
||||||
@@ -998,8 +1006,8 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
opts.NotAssociatedToTeam = teamID
|
opts.NotAssociatedToTeam = teamID
|
||||||
}
|
}
|
||||||
|
|
||||||
if channelID != "" {
|
if NotAssociatedToChannelID != "" {
|
||||||
channel, appErr := c.App.GetChannel(c.AppContext, channelID)
|
channel, appErr := c.App.GetChannel(c.AppContext, NotAssociatedToChannelID)
|
||||||
if appErr != nil {
|
if appErr != nil {
|
||||||
c.Err = appErr
|
c.Err = appErr
|
||||||
return
|
return
|
||||||
@@ -1010,11 +1018,30 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
} else {
|
} else {
|
||||||
permission = model.PermissionManagePublicChannelMembers
|
permission = model.PermissionManagePublicChannelMembers
|
||||||
}
|
}
|
||||||
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), channelID, permission) {
|
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), NotAssociatedToChannelID, permission) {
|
||||||
c.SetPermissionError(permission)
|
c.SetPermissionError(permission)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
opts.NotAssociatedToChannel = channelID
|
opts.NotAssociatedToChannel = NotAssociatedToChannelID
|
||||||
|
}
|
||||||
|
|
||||||
|
if ChannelIDForMemberCount != "" {
|
||||||
|
channel, appErr := c.App.GetChannel(c.AppContext, ChannelIDForMemberCount)
|
||||||
|
if appErr != nil {
|
||||||
|
c.Err = appErr
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var permission *model.Permission
|
||||||
|
if channel.Type == model.ChannelTypePrivate {
|
||||||
|
permission = model.PermissionManagePrivateChannelMembers
|
||||||
|
} else {
|
||||||
|
permission = model.PermissionManagePublicChannelMembers
|
||||||
|
}
|
||||||
|
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), ChannelIDForMemberCount, permission) {
|
||||||
|
c.SetPermissionError(permission)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
opts.IncludeChannelMemberCount = ChannelIDForMemberCount
|
||||||
}
|
}
|
||||||
|
|
||||||
sinceString := r.URL.Query().Get("since")
|
sinceString := r.URL.Query().Get("since")
|
||||||
|
|||||||
@@ -1291,6 +1291,28 @@ func TestGetGroups(t *testing.T) {
|
|||||||
assert.Len(t, groups, 1)
|
assert.Len(t, groups, 1)
|
||||||
assert.Equal(t, groups[0].Id, group2.Id)
|
assert.Equal(t, groups[0].Id, group2.Id)
|
||||||
|
|
||||||
|
// Test IncludeChannelMemberCount url param is working
|
||||||
|
opts.IncludeChannelMemberCount = th.BasicChannel.Id
|
||||||
|
opts.IncludeTimezones = true
|
||||||
|
opts.Q = "-fOo"
|
||||||
|
opts.IncludeMemberCount = true
|
||||||
|
|
||||||
|
groups, _, _ = th.SystemAdminClient.GetGroups(opts)
|
||||||
|
assert.Equal(t, *groups[0].MemberCount, int(0))
|
||||||
|
assert.Equal(t, *groups[0].ChannelMemberCount, int(0))
|
||||||
|
|
||||||
|
_, appErr = th.App.UpsertGroupMember(group2.Id, th.BasicUser.Id)
|
||||||
|
assert.Nil(t, appErr)
|
||||||
|
|
||||||
|
groups, _, _ = th.SystemAdminClient.GetGroups(opts)
|
||||||
|
assert.NotNil(t, groups[0].MemberCount)
|
||||||
|
assert.Equal(t, *groups[0].ChannelMemberCount, int(1))
|
||||||
|
|
||||||
|
opts.IncludeChannelMemberCount = ""
|
||||||
|
opts.IncludeTimezones = false
|
||||||
|
opts.Q = ""
|
||||||
|
opts.IncludeMemberCount = false
|
||||||
|
|
||||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||||
*cfg.ServiceSettings.EnableCustomGroups = false
|
*cfg.ServiceSettings.EnableCustomGroups = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5515,7 +5515,7 @@ func (c *Client4) GetGroupsAssociatedToChannelsByTeam(teamId string, opts GroupS
|
|||||||
// GetGroups retrieves Mattermost Groups
|
// GetGroups retrieves Mattermost Groups
|
||||||
func (c *Client4) GetGroups(opts GroupSearchOpts) ([]*Group, *Response, error) {
|
func (c *Client4) GetGroups(opts GroupSearchOpts) ([]*Group, *Response, error) {
|
||||||
path := fmt.Sprintf(
|
path := fmt.Sprintf(
|
||||||
"%s?include_member_count=%v¬_associated_to_team=%v¬_associated_to_channel=%v&filter_allow_reference=%v&q=%v&filter_parent_team_permitted=%v&group_source=%v",
|
"%s?include_member_count=%v¬_associated_to_team=%v¬_associated_to_channel=%v&filter_allow_reference=%v&q=%v&filter_parent_team_permitted=%v&group_source=%v&include_channel_member_count=%v&include_timezones=%v",
|
||||||
c.groupsRoute(),
|
c.groupsRoute(),
|
||||||
opts.IncludeMemberCount,
|
opts.IncludeMemberCount,
|
||||||
opts.NotAssociatedToTeam,
|
opts.NotAssociatedToTeam,
|
||||||
@@ -5524,6 +5524,8 @@ func (c *Client4) GetGroups(opts GroupSearchOpts) ([]*Group, *Response, error) {
|
|||||||
opts.Q,
|
opts.Q,
|
||||||
opts.FilterParentTeamPermitted,
|
opts.FilterParentTeamPermitted,
|
||||||
opts.Source,
|
opts.Source,
|
||||||
|
opts.IncludeChannelMemberCount,
|
||||||
|
opts.IncludeTimezones,
|
||||||
)
|
)
|
||||||
if opts.Since > 0 {
|
if opts.Since > 0 {
|
||||||
path = fmt.Sprintf("%s&since=%v", path, opts.Since)
|
path = fmt.Sprintf("%s&since=%v", path, opts.Since)
|
||||||
|
|||||||
@@ -31,18 +31,20 @@ var groupSourcesRequiringRemoteID = []GroupSource{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Group struct {
|
type Group struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Name *string `json:"name,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
DisplayName string `json:"display_name"`
|
DisplayName string `json:"display_name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Source GroupSource `json:"source"`
|
Source GroupSource `json:"source"`
|
||||||
RemoteId *string `json:"remote_id"`
|
RemoteId *string `json:"remote_id"`
|
||||||
CreateAt int64 `json:"create_at"`
|
CreateAt int64 `json:"create_at"`
|
||||||
UpdateAt int64 `json:"update_at"`
|
UpdateAt int64 `json:"update_at"`
|
||||||
DeleteAt int64 `json:"delete_at"`
|
DeleteAt int64 `json:"delete_at"`
|
||||||
HasSyncables bool `db:"-" json:"has_syncables"`
|
HasSyncables bool `db:"-" json:"has_syncables"`
|
||||||
MemberCount *int `db:"-" json:"member_count,omitempty"`
|
MemberCount *int `db:"-" json:"member_count,omitempty"`
|
||||||
AllowReference bool `json:"allow_reference"`
|
AllowReference bool `json:"allow_reference"`
|
||||||
|
ChannelMemberCount *int `db:"-" json:"channel_member_count,omitempty"`
|
||||||
|
ChannelMemberTimezonesCount *int `db:"-" json:"channel_member_timezones_count,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (group *Group) Auditable() map[string]interface{} {
|
func (group *Group) Auditable() map[string]interface{} {
|
||||||
@@ -113,6 +115,9 @@ type GroupSearchOpts struct {
|
|||||||
// FilterHasMember filters the groups to the intersect of the
|
// FilterHasMember filters the groups to the intersect of the
|
||||||
// set returned by the query and those that have the given user as a member.
|
// set returned by the query and those that have the given user as a member.
|
||||||
FilterHasMember string
|
FilterHasMember string
|
||||||
|
|
||||||
|
IncludeChannelMemberCount string
|
||||||
|
IncludeTimezones bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetGroupOpts struct {
|
type GetGroupOpts struct {
|
||||||
|
|||||||
@@ -1056,34 +1056,38 @@ func (s *SqlGroupStore) CountGroupsByChannel(channelId string, opts model.GroupS
|
|||||||
}
|
}
|
||||||
|
|
||||||
type group struct {
|
type group struct {
|
||||||
Id string
|
Id string
|
||||||
Name *string
|
Name *string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
Description string
|
Description string
|
||||||
Source model.GroupSource
|
Source model.GroupSource
|
||||||
RemoteId *string
|
RemoteId *string
|
||||||
CreateAt int64
|
CreateAt int64
|
||||||
UpdateAt int64
|
UpdateAt int64
|
||||||
DeleteAt int64
|
DeleteAt int64
|
||||||
HasSyncables bool
|
HasSyncables bool
|
||||||
MemberCount *int
|
MemberCount *int
|
||||||
AllowReference bool
|
AllowReference bool
|
||||||
|
ChannelMemberCount *int
|
||||||
|
ChannelMemberTimezonesCount *int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g group) ToModel() *model.Group {
|
func (g group) ToModel() *model.Group {
|
||||||
return &model.Group{
|
return &model.Group{
|
||||||
Id: g.Id,
|
Id: g.Id,
|
||||||
Name: g.Name,
|
Name: g.Name,
|
||||||
DisplayName: g.DisplayName,
|
DisplayName: g.DisplayName,
|
||||||
Description: g.Description,
|
Description: g.Description,
|
||||||
Source: g.Source,
|
Source: g.Source,
|
||||||
RemoteId: g.RemoteId,
|
RemoteId: g.RemoteId,
|
||||||
CreateAt: g.CreateAt,
|
CreateAt: g.CreateAt,
|
||||||
UpdateAt: g.UpdateAt,
|
UpdateAt: g.UpdateAt,
|
||||||
DeleteAt: g.DeleteAt,
|
DeleteAt: g.DeleteAt,
|
||||||
HasSyncables: g.HasSyncables,
|
HasSyncables: g.HasSyncables,
|
||||||
AllowReference: g.AllowReference,
|
AllowReference: g.AllowReference,
|
||||||
MemberCount: g.MemberCount,
|
MemberCount: g.MemberCount,
|
||||||
|
ChannelMemberCount: g.ChannelMemberCount,
|
||||||
|
ChannelMemberTimezonesCount: g.ChannelMemberTimezonesCount,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1416,7 +1420,20 @@ func (s *SqlGroupStore) GetGroupsAssociatedToChannelsByTeam(teamId string, opts
|
|||||||
func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) {
|
func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts, viewRestrictions *model.ViewUsersRestrictions) ([]*model.Group, error) {
|
||||||
groupsVar := groups{}
|
groupsVar := groups{}
|
||||||
|
|
||||||
groupsQuery := s.getQueryBuilder().Select("g.*")
|
selectQuery := []string{"g.*"}
|
||||||
|
|
||||||
|
if opts.IncludeMemberCount {
|
||||||
|
selectQuery = append(selectQuery, "coalesce(Members.MemberCount, 0) AS MemberCount")
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.IncludeChannelMemberCount != "" {
|
||||||
|
selectQuery = append(selectQuery, "coalesce(ChannelMembers.ChannelMemberCount, 0) AS ChannelMemberCount")
|
||||||
|
if opts.IncludeTimezones {
|
||||||
|
selectQuery = append(selectQuery, "coalesce(ChannelMembers.ChannelMemberTimezonesCount, 0) AS ChannelMemberTimezonesCount")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
groupsQuery := s.getQueryBuilder().Select(strings.Join(selectQuery, ", "))
|
||||||
|
|
||||||
if opts.IncludeMemberCount {
|
if opts.IncludeMemberCount {
|
||||||
countQuery := s.getQueryBuilder().
|
countQuery := s.getQueryBuilder().
|
||||||
@@ -1433,12 +1450,43 @@ func (s *SqlGroupStore) GetGroups(page, perPage int, opts model.GroupSearchOpts,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "get_groups_tosql")
|
return nil, errors.Wrap(err, "get_groups_tosql")
|
||||||
}
|
}
|
||||||
|
groupsQuery = groupsQuery.
|
||||||
groupsQuery = s.getQueryBuilder().
|
|
||||||
Select("g.*, coalesce(Members.MemberCount, 0) AS MemberCount").
|
|
||||||
LeftJoin("("+countString+") AS Members ON Members.GroupId = g.Id", params...)
|
LeftJoin("("+countString+") AS Members ON Members.GroupId = g.Id", params...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.IncludeChannelMemberCount != "" {
|
||||||
|
selectStr := "GroupMembers.GroupId, COUNT(ChannelMembers.UserId) AS ChannelMemberCount"
|
||||||
|
joinStr := ""
|
||||||
|
|
||||||
|
if opts.IncludeTimezones {
|
||||||
|
if s.DriverName() == model.DatabaseDriverMysql {
|
||||||
|
selectStr += `,
|
||||||
|
COUNT(DISTINCT
|
||||||
|
(
|
||||||
|
CASE WHEN JSON_EXTRACT(Timezone, '$.useAutomaticTimezone') = 'true' AND LENGTH(JSON_UNQUOTE(JSON_EXTRACT(Timezone, '$.automaticTimezone'))) > 0
|
||||||
|
THEN JSON_EXTRACT(Timezone, '$.automaticTimezone')
|
||||||
|
WHEN JSON_EXTRACT(Timezone, '$.useAutomaticTimezone') = 'false' AND LENGTH(JSON_UNQUOTE(JSON_EXTRACT(Timezone, '$.manualTimezone'))) > 0
|
||||||
|
THEN JSON_EXTRACT(Timezone, '$.manualTimezone')
|
||||||
|
END
|
||||||
|
)) AS ChannelMemberTimezonesCount`
|
||||||
|
} else if s.DriverName() == model.DatabaseDriverPostgres {
|
||||||
|
selectStr += `,
|
||||||
|
COUNT(DISTINCT
|
||||||
|
(
|
||||||
|
CASE WHEN Timezone->>'useAutomaticTimezone' = 'true' AND length(Timezone->>'automaticTimezone') > 0
|
||||||
|
THEN Timezone->>'automaticTimezone'
|
||||||
|
WHEN Timezone->>'useAutomaticTimezone' = 'false' AND length(Timezone->>'manualTimezone') > 0
|
||||||
|
THEN Timezone->>'manualTimezone'
|
||||||
|
END
|
||||||
|
)) AS ChannelMemberTimezonesCount`
|
||||||
|
}
|
||||||
|
joinStr = "LEFT JOIN Users ON Users.Id = GroupMembers.UserId"
|
||||||
|
}
|
||||||
|
|
||||||
|
groupsQuery = groupsQuery.
|
||||||
|
LeftJoin("(SELECT "+selectStr+" FROM ChannelMembers LEFT JOIN GroupMembers ON GroupMembers.UserId = ChannelMembers.UserId AND GroupMembers.DeleteAt = 0 "+joinStr+" WHERE ChannelMembers.ChannelId = ? GROUP BY GroupId) AS ChannelMembers ON ChannelMembers.GroupId = g.Id", opts.IncludeChannelMemberCount)
|
||||||
|
}
|
||||||
|
|
||||||
if opts.FilterHasMember != "" {
|
if opts.FilterHasMember != "" {
|
||||||
groupsQuery = groupsQuery.
|
groupsQuery = groupsQuery.
|
||||||
LeftJoin("GroupMembers ON GroupMembers.GroupId = g.Id").
|
LeftJoin("GroupMembers ON GroupMembers.GroupId = g.Id").
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ type Params struct {
|
|||||||
ExcludePolicyConstrained bool
|
ExcludePolicyConstrained bool
|
||||||
GroupSource model.GroupSource
|
GroupSource model.GroupSource
|
||||||
FilterHasMember string
|
FilterHasMember string
|
||||||
|
IncludeChannelMemberCount string
|
||||||
|
|
||||||
// Cloud
|
// Cloud
|
||||||
InvoiceId string
|
InvoiceId string
|
||||||
@@ -208,6 +209,7 @@ func ParamsFromRequest(r *http.Request) *Params {
|
|||||||
params.NotAssociatedToChannel = query.Get("not_associated_to_channel")
|
params.NotAssociatedToChannel = query.Get("not_associated_to_channel")
|
||||||
params.FilterAllowReference, _ = strconv.ParseBool(query.Get("filter_allow_reference"))
|
params.FilterAllowReference, _ = strconv.ParseBool(query.Get("filter_allow_reference"))
|
||||||
params.FilterParentTeamPermitted, _ = strconv.ParseBool(query.Get("filter_parent_team_permitted"))
|
params.FilterParentTeamPermitted, _ = strconv.ParseBool(query.Get("filter_parent_team_permitted"))
|
||||||
|
params.IncludeChannelMemberCount = query.Get("include_channel_member_count")
|
||||||
|
|
||||||
if val, err := strconv.ParseBool(query.Get("paginate")); err == nil {
|
if val, err := strconv.ParseBool(query.Get("paginate")); err == nil {
|
||||||
params.Paginate = &val
|
params.Paginate = &val
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user