diff --git a/api4/channel.go b/api4/channel.go index f999d7ff75..7eb3cffaee 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -503,7 +503,12 @@ func getAllChannels(c *Context, w http.ResponseWriter, r *http.Request) { return } - channels, err := c.App.GetAllChannels(c.Params.Page, c.Params.PerPage, false) + opts := model.ChannelSearchOpts{ + NotAssociatedToGroup: c.Params.NotAssociatedToGroup, + ExcludeDefaultChannels: c.Params.ExcludeDefaultChannels, + } + + channels, err := c.App.GetAllChannels(c.Params.Page, c.Params.PerPage, opts) if err != nil { c.Err = err return @@ -727,9 +732,13 @@ func searchAllChannels(c *Context, w http.ResponseWriter, r *http.Request) { return } - includeDeleted := r.URL.Query().Get("include_deleted") == "true" + opts := model.ChannelSearchOpts{ + NotAssociatedToGroup: props.NotAssociatedToGroup, + ExcludeDefaultChannels: props.ExcludeDefaultChannels, + IncludeDeleted: r.URL.Query().Get("include_deleted") == "true", + } - channels, err := c.App.SearchAllChannels(props.Term, includeDeleted) + channels, err := c.App.SearchAllChannels(props.Term, opts) if err != nil { c.Err = err return diff --git a/app/channel.go b/app/channel.go index e38a7b5c3d..9a4ae013aa 100644 --- a/app/channel.go +++ b/app/channel.go @@ -16,23 +16,52 @@ import ( "github.com/mattermost/mattermost-server/utils" ) -func (a *App) CreateDefaultChannels(teamId string) ([]*model.Channel, *model.AppError) { - townSquare := &model.Channel{DisplayName: utils.T("api.channel.create_default_channels.town_square"), Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: teamId} - - if _, err := a.CreateChannel(townSquare, false); err != nil { - return nil, err +// CreateDefaultChannels creates channels in the given team for each channel returned by (*App).DefaultChannelNames. +// +func (a *App) CreateDefaultChannels(teamID string) ([]*model.Channel, *model.AppError) { + displayNames := map[string]string{ + "town-square": utils.T("api.channel.create_default_channels.town_square"), + "off-topic": utils.T("api.channel.create_default_channels.off_topic"), } - - offTopic := &model.Channel{DisplayName: utils.T("api.channel.create_default_channels.off_topic"), Name: "off-topic", Type: model.CHANNEL_OPEN, TeamId: teamId} - - if _, err := a.CreateChannel(offTopic, false); err != nil { - return nil, err + channels := []*model.Channel{} + defaultChannelNames := a.DefaultChannelNames() + for _, name := range defaultChannelNames { + displayName := utils.TDefault(displayNames[name], name) + channel := &model.Channel{DisplayName: displayName, Name: name, Type: model.CHANNEL_OPEN, TeamId: teamID} + if _, err := a.CreateChannel(channel, false); err != nil { + return nil, err + } + channels = append(channels, channel) } - - channels := []*model.Channel{townSquare, offTopic} return channels, nil } +// DefaultChannelNames returns the list of system-wide default channel names. +// +// By default the list will be (not necessarily in this order): +// ['town-square', 'off-topic'] +// However, if TeamSettings.ExperimentalDefaultChannels contains a list of channels then that list will replace +// 'off-topic' and be included in the return results in addition to 'town-square'. For example: +// ['town-square', 'game-of-thrones', 'wow'] +// +func (a *App) DefaultChannelNames() []string { + names := []string{"town-square"} + + if len(a.Config().TeamSettings.ExperimentalDefaultChannels) == 0 { + names = append(names, "off-topic") + } else { + seenChannels := map[string]bool{} + for _, channelName := range a.Config().TeamSettings.ExperimentalDefaultChannels { + if !seenChannels[channelName] { + names = append(names, channelName) + seenChannels[channelName] = true + } + } + } + + return names +} + func (a *App) JoinDefaultChannels(teamId string, user *model.User, shouldBeAdmin bool, userRequestorId string) *model.AppError { var requestor *model.User if userRequestorId != "" { @@ -43,22 +72,8 @@ func (a *App) JoinDefaultChannels(teamId string, user *model.User, shouldBeAdmin } } - defaultChannelList := []string{"town-square"} - - if len(a.Config().TeamSettings.ExperimentalDefaultChannels) == 0 { - defaultChannelList = append(defaultChannelList, "off-topic") - } else { - seenChannels := map[string]bool{} - for _, channelName := range a.Config().TeamSettings.ExperimentalDefaultChannels { - if !seenChannels[channelName] { - defaultChannelList = append(defaultChannelList, channelName) - seenChannels[channelName] = true - } - } - } - var err *model.AppError - for _, channelName := range defaultChannelList { + for _, channelName := range a.DefaultChannelNames() { if result := <-a.Srv.Store.Channel().GetByName(teamId, channelName, true); result.Err != nil { err = result.Err } else { @@ -1230,8 +1245,16 @@ func (a *App) GetChannelsForUser(teamId string, userId string, includeDeleted bo return result.Data.(*model.ChannelList), nil } -func (a *App) GetAllChannels(page, perPage int, includeDeleted bool) (*model.ChannelListWithTeamData, *model.AppError) { - result := <-a.Srv.Store.Channel().GetAllChannels(page*perPage, perPage, includeDeleted) +func (a *App) GetAllChannels(page, perPage int, opts model.ChannelSearchOpts) (*model.ChannelListWithTeamData, *model.AppError) { + if opts.ExcludeDefaultChannels { + opts.ExcludeChannelNames = a.DefaultChannelNames() + } + storeOpts := store.ChannelSearchOpts{ + ExcludeChannelNames: opts.ExcludeChannelNames, + NotAssociatedToGroup: opts.NotAssociatedToGroup, + IncludeDeleted: opts.IncludeDeleted, + } + result := <-a.Srv.Store.Channel().GetAllChannels(page*perPage, perPage, storeOpts) if result.Err != nil { return nil, result.Err } @@ -1784,8 +1807,17 @@ func (a *App) AutocompleteChannelsForSearch(teamId string, userId string, term s return result.Data.(*model.ChannelList), nil } -func (a *App) SearchAllChannels(term string, includeDeleted bool) (*model.ChannelListWithTeamData, *model.AppError) { - result := <-a.Srv.Store.Channel().SearchAllChannels(term, *a.Config().TeamSettings.ExperimentalViewArchivedChannels && includeDeleted) +func (a *App) SearchAllChannels(term string, opts model.ChannelSearchOpts) (*model.ChannelListWithTeamData, *model.AppError) { + opts.IncludeDeleted = *a.Config().TeamSettings.ExperimentalViewArchivedChannels && opts.IncludeDeleted + if opts.ExcludeDefaultChannels { + opts.ExcludeChannelNames = a.DefaultChannelNames() + } + storeOpts := store.ChannelSearchOpts{ + ExcludeChannelNames: opts.ExcludeChannelNames, + NotAssociatedToGroup: opts.NotAssociatedToGroup, + IncludeDeleted: opts.IncludeDeleted, + } + result := <-a.Srv.Store.Channel().SearchAllChannels(term, storeOpts) if result.Err != nil { return nil, result.Err } diff --git a/app/channel_test.go b/app/channel_test.go index a303475c5a..114493bc52 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -895,3 +895,20 @@ func TestUpdateChannelMemberRolesChangingGuest(t *testing.T) { } }) } + +func TestDefaultChannelNames(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + actual := th.App.DefaultChannelNames() + expect := []string{"town-square", "off-topic"} + require.ElementsMatch(t, expect, actual) + + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.TeamSettings.ExperimentalDefaultChannels = []string{"foo", "bar"} + }) + + actual = th.App.DefaultChannelNames() + expect = []string{"town-square", "foo", "bar"} + require.ElementsMatch(t, expect, actual) +} diff --git a/i18n/en.json b/i18n/en.json index 59fbd48517..ed24c876cf 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -5274,6 +5274,10 @@ "id": "store.select_error", "translation": "select error" }, + { + "id": "store.sql.build_query.app_error", + "translation": "failed to build query" + }, { "id": "store.sql.convert_string_array", "translation": "FromDb: Unable to convert StringArray to *string" diff --git a/model/channel.go b/model/channel.go index d3b35bfc93..942fa6d778 100644 --- a/model/channel.go +++ b/model/channel.go @@ -80,6 +80,20 @@ type DirectChannelForExport struct { Members *[]string } +// ChannelSearchOpts contains options for searching channels. +// +// NotAssociatedToGroup will exclude channels that have associated, active GroupChannels records. +// ExcludeDefaultChannels will exclude the configured default channels (ex 'town-square' and 'off-topic'). +// IncludeDeleted will include channel records where DeleteAt != 0. +// ExcludeChannelNames will exclude channels from the results by name. +// +type ChannelSearchOpts struct { + NotAssociatedToGroup string + ExcludeDefaultChannels bool + IncludeDeleted bool + ExcludeChannelNames []string +} + func (o *Channel) DeepCopy() *Channel { copy := *o if copy.SchemeId != nil { diff --git a/model/channel_search.go b/model/channel_search.go index 2567722329..7502372a6d 100644 --- a/model/channel_search.go +++ b/model/channel_search.go @@ -11,7 +11,9 @@ import ( const CHANNEL_SEARCH_DEFAULT_LIMIT = 50 type ChannelSearch struct { - Term string `json:"term"` + Term string `json:"term"` + ExcludeDefaultChannels bool `json:"exclude_default_channels"` + NotAssociatedToGroup string `json:"not_associated_to_group"` } // ToJson convert a Channel to a json string diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index ce018b3f8a..cf63c706db 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -949,17 +949,37 @@ func (s SqlChannelStore) GetChannels(teamId string, userId string, includeDelete }) } -func (s SqlChannelStore) GetAllChannels(offset int, limit int, includeDeleted bool) store.StoreChannel { +func (s SqlChannelStore) GetAllChannels(offset int, limit int, opts store.ChannelSearchOpts) store.StoreChannel { return store.Do(func(result *store.StoreResult) { - deleteFilter := "AND c.DeleteAt = 0" - if includeDeleted { - deleteFilter = "" + query := s.getQueryBuilder(). + Select("c.*, Teams.DisplayName AS TeamDisplayName, Teams.Name AS TeamName, Teams.UpdateAt AS TeamUpdateAt"). + From("Channels AS c"). + Join("Teams ON Teams.Id = c.TeamId"). + Where(sq.Eq{"c.Type": []string{model.CHANNEL_PRIVATE, model.CHANNEL_OPEN}}). + OrderBy("c.DisplayName, Teams.DisplayName"). + Limit(uint64(limit)). + Offset(uint64(offset)) + + if !opts.IncludeDeleted { + query = query.Where(sq.Eq{"c.DeleteAt": int(0)}) } - query := "SELECT c.*, Teams.DisplayName AS TeamDisplayName, Teams.Name AS TeamName, Teams.UpdateAt as TeamUpdateAt FROM Channels AS c JOIN Teams ON Teams.Id = c.TeamId WHERE (c.Type = 'P' OR c.Type = 'O') " + deleteFilter + " ORDER BY c.DisplayName, Teams.DisplayName LIMIT :Limit OFFSET :Offset" + if len(opts.NotAssociatedToGroup) > 0 { + query = query.Where("c.Id NOT IN (SELECT ChannelId FROM GroupChannels WHERE GroupChannels.GroupId = ? AND GroupChannels.DeleteAt = 0)", opts.NotAssociatedToGroup) + } + + if len(opts.ExcludeChannelNames) > 0 { + query = query.Where(fmt.Sprintf("c.Name NOT IN ('%s')", strings.Join(opts.ExcludeChannelNames, "', '"))) + } + + queryString, args, err := query.ToSql() + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.GetAllChannels", "store.sql.build_query.app_error", nil, err.Error(), http.StatusInternalServerError) + return + } data := &model.ChannelListWithTeamData{} - _, err := s.GetReplica().Select(data, query, map[string]interface{}{"Limit": limit, "Offset": offset}) + _, err = s.GetReplica().Select(data, queryString, args...) if err != nil { result.Err = model.NewAppError("SqlChannelStore.GetAllChannels", "store.sql_channel.get_all_channels.get.app_error", nil, err.Error(), http.StatusInternalServerError) @@ -2120,29 +2140,45 @@ func (s SqlChannelStore) SearchInTeam(teamId string, term string, includeDeleted }) } -func (s SqlChannelStore) SearchAllChannels(term string, includeDeleted bool) store.StoreChannel { +func (s SqlChannelStore) SearchAllChannels(term string, opts store.ChannelSearchOpts) store.StoreChannel { return store.Do(func(result *store.StoreResult) { - parameters := map[string]interface{}{} - deleteFilter := "AND c.DeleteAt = 0" - if includeDeleted { - deleteFilter = "" + query := s.getQueryBuilder(). + Select("c.*, t.DisplayName AS TeamDisplayName, t.Name AS TeamName, t.UpdateAt as TeamUpdateAt"). + From("Channels AS c"). + Join("Teams AS t ON t.Id = c.TeamId"). + Where(sq.Eq{"c.Type": []string{model.CHANNEL_PRIVATE, model.CHANNEL_OPEN}}). + OrderBy("c.DisplayName, t.DisplayName"). + Limit(uint64(100)) + + if !opts.IncludeDeleted { + query = query.Where(sq.Eq{"c.DeleteAt": int(0)}) } - searchQuery := `SELECT c.*, t.DisplayName AS TeamDisplayName, t.Name AS TeamName, t.UpdateAt as TeamUpdateAt FROM Channels AS c JOIN Teams AS t ON t.Id = c.TeamId WHERE (c.Type = 'P' OR c.Type = 'O') ` + deleteFilter + ` SEARCH_CLAUSE ORDER BY c.DisplayName, t.DisplayName LIMIT 100` likeClause, likeTerm := s.buildLIKEClause(term, "c.Name, c.DisplayName, c.Purpose") - if likeTerm == "" { - // If the likeTerm is empty after preparing, then don't bother searching. - searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "", 1) - } else { - parameters["LikeTerm"] = likeTerm + if len(likeTerm) > 0 { + likeClause = strings.ReplaceAll(likeClause, ":LikeTerm", "'"+likeTerm+"'") fulltextClause, fulltextTerm := s.buildFulltextClause(term, "c.Name, c.DisplayName, c.Purpose") - parameters["FulltextTerm"] = fulltextTerm - searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", "AND ("+likeClause+" OR "+fulltextClause+")", 1) + fulltextClause = strings.ReplaceAll(fulltextClause, ":FulltextTerm", "'"+fulltextTerm+"'") + query = query.Where("(" + likeClause + " OR " + fulltextClause + ")") + } + + if len(opts.ExcludeChannelNames) > 0 { + query = query.Where(fmt.Sprintf("c.Name NOT IN ('%s')", strings.Join(opts.ExcludeChannelNames, "', '"))) + } + + if len(opts.NotAssociatedToGroup) > 0 { + query = query.Where("c.Id NOT IN (SELECT ChannelId FROM GroupChannels WHERE GroupChannels.GroupId = ? AND GroupChannels.DeleteAt = 0)", opts.NotAssociatedToGroup) + } + + queryString, args, err := query.ToSql() + if err != nil { + result.Err = model.NewAppError("SqlChannelStore.SearchAllChannels", "store.sql.build_query.app_error", nil, err.Error(), http.StatusInternalServerError) + return } var channels model.ChannelListWithTeamData - if _, err := s.GetReplica().Select(&channels, searchQuery, parameters); err != nil { + if _, err := s.GetReplica().Select(&channels, queryString, args...); err != nil { result.Err = model.NewAppError("SqlChannelStore.Search", "store.sql_channel.search.app_error", nil, "term="+term+", "+", "+err.Error(), http.StatusInternalServerError) } @@ -2501,7 +2537,7 @@ func (s SqlChannelStore) GetAllChannelsForExportAfter(limit int, afterId string) Id LIMIT :Limit`, map[string]interface{}{"AfterId": afterId, "Limit": limit}); err != nil { - result.Err = model.NewAppError("SqlTeamStore.GetAllChannelsForExportAfter", "store.sql_channel.get_all.app_error", nil, err.Error(), http.StatusInternalServerError) + result.Err = model.NewAppError("SqlChannelStore.GetAllChannelsForExportAfter", "store.sql_channel.get_all.app_error", nil, err.Error(), http.StatusInternalServerError) return } @@ -2561,12 +2597,12 @@ func (s SqlChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterId s queryString, args, err := query.ToSql() if err != nil { - result.Err = model.NewAppError("SqlTeamStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError) + result.Err = model.NewAppError("SqlChannelStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError) return } if _, err = s.GetReplica().Select(&directChannelsForExport, queryString, args...); err != nil { - result.Err = model.NewAppError("SqlTeamStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError) + result.Err = model.NewAppError("SqlChannelStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError) return } @@ -2585,13 +2621,13 @@ func (s SqlChannelStore) GetAllDirectChannelsForExportAfter(limit int, afterId s queryString, args, err = query.ToSql() if err != nil { - result.Err = model.NewAppError("SqlTeamStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError) + result.Err = model.NewAppError("SqlChannelStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError) return } var channelMembers []*model.ChannelMemberForExport if _, err := s.GetReplica().Select(&channelMembers, queryString, args...); err != nil { - result.Err = model.NewAppError("SqlTeamStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError) + result.Err = model.NewAppError("SqlChannelStore.GetAllDirectChannelsForExportAfter", "store.sql_channel.get_all_direct.app_error", nil, err.Error(), http.StatusInternalServerError) } // Populate each channel with its members diff --git a/store/store.go b/store/store.go index 1f8fb03d6f..1cf91fe941 100644 --- a/store/store.go +++ b/store/store.go @@ -148,7 +148,7 @@ type ChannelStore interface { GetDeletedByName(team_id string, name string) StoreChannel GetDeleted(team_id string, offset int, limit int) StoreChannel GetChannels(teamId string, userId string, includeDeleted bool) StoreChannel - GetAllChannels(page, perPage int, includeDeleted bool) StoreChannel + GetAllChannels(page, perPage int, opts ChannelSearchOpts) StoreChannel GetMoreChannels(teamId string, userId string, offset int, limit int) StoreChannel GetPublicChannelsForTeam(teamId string, offset int, limit int) StoreChannel GetPublicChannelsByIdsForTeam(teamId string, channelIds []string) StoreChannel @@ -182,7 +182,7 @@ type ChannelStore interface { GetMembersForUserWithPagination(teamId, userId string, page, perPage int) StoreChannel AutocompleteInTeam(teamId string, term string, includeDeleted bool) StoreChannel AutocompleteInTeamForSearch(teamId string, userId string, term string, includeDeleted bool) StoreChannel - SearchAllChannels(term string, includeDeleted bool) StoreChannel + SearchAllChannels(term string, opts ChannelSearchOpts) StoreChannel SearchInTeam(teamId string, term string, includeDeleted bool) StoreChannel SearchMore(userId string, teamId string, term string) StoreChannel GetMembersByIds(channelId string, userIds []string) StoreChannel @@ -607,3 +607,15 @@ type LinkMetadataStore interface { Save(linkMetadata *model.LinkMetadata) StoreChannel Get(url string, timestamp int64) StoreChannel } + +// ChannelSearchOpts contains options for searching channels. +// +// NotAssociatedToGroup will exclude channels that have associated, active GroupChannels records. +// IncludeDeleted will include channel records where DeleteAt != 0. +// ExcludeChannelNames will exclude channels from the results by name. +// +type ChannelSearchOpts struct { + NotAssociatedToGroup string + IncludeDeleted bool + ExcludeChannelNames []string +} diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 6fda98957f..43ec111e83 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -23,9 +23,9 @@ type SqlSupplier interface { } func cleanupChannels(t *testing.T, ss store.Store) { - result := <-ss.Channel().GetAllChannels(0, 100000, true) + result := <-ss.Channel().GetAllChannels(0, 100000, store.ChannelSearchOpts{IncludeDeleted: true}) if result.Err != nil { - t.Fatal("error cleaning all channels") + t.Fatalf("error cleaning all channels: %v", result.Err) } list := result.Data.(*model.ChannelListWithTeamData) for _, channel := range *list { @@ -1073,6 +1073,15 @@ func testChannelStoreGetAllChannels(t *testing.T, ss store.Store, s SqlSupplier) c1.Type = model.CHANNEL_OPEN store.Must(ss.Channel().Save(&c1, -1)) + group := &model.Group{ + Name: model.NewId(), + DisplayName: model.NewId(), + Source: model.GroupSourceLdap, + RemoteId: model.NewId(), + } + store.Must(ss.Group().Create(group)) + store.Must(ss.Group().CreateGroupSyncable(model.NewGroupChannel(group.Id, c1.Id, true))) + c2 := model.Channel{} c2.TeamId = t1.Id c2.DisplayName = "Channel2" + model.NewId() @@ -1103,7 +1112,7 @@ func testChannelStoreGetAllChannels(t *testing.T, ss store.Store, s SqlSupplier) c5.Type = model.CHANNEL_GROUP store.Must(ss.Channel().Save(&c5, -1)) - cresult := <-ss.Channel().GetAllChannels(0, 10, false) + cresult := <-ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{}) list := cresult.Data.(*model.ChannelListWithTeamData) assert.Len(t, *list, 2) assert.Equal(t, (*list)[0].Id, c1.Id) @@ -1111,7 +1120,7 @@ func testChannelStoreGetAllChannels(t *testing.T, ss store.Store, s SqlSupplier) assert.Equal(t, (*list)[1].Id, c3.Id) assert.Equal(t, (*list)[1].TeamDisplayName, "Name2") - cresult = <-ss.Channel().GetAllChannels(0, 10, true) + cresult = <-ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{IncludeDeleted: true}) list = cresult.Data.(*model.ChannelListWithTeamData) assert.Len(t, *list, 3) assert.Equal(t, (*list)[0].Id, c1.Id) @@ -1119,12 +1128,22 @@ func testChannelStoreGetAllChannels(t *testing.T, ss store.Store, s SqlSupplier) assert.Equal(t, (*list)[1].Id, c2.Id) assert.Equal(t, (*list)[2].Id, c3.Id) - cresult = <-ss.Channel().GetAllChannels(0, 1, true) + cresult = <-ss.Channel().GetAllChannels(0, 1, store.ChannelSearchOpts{IncludeDeleted: true}) list = cresult.Data.(*model.ChannelListWithTeamData) assert.Len(t, *list, 1) assert.Equal(t, (*list)[0].Id, c1.Id) assert.Equal(t, (*list)[0].TeamDisplayName, "Name") + // Not associated to group + cresult = <-ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{NotAssociatedToGroup: group.Id}) + list = cresult.Data.(*model.ChannelListWithTeamData) + assert.Len(t, *list, 1) + + // Exclude channel names + cresult = <-ss.Channel().GetAllChannels(0, 10, store.ChannelSearchOpts{ExcludeChannelNames: []string{c1.Name}}) + list = cresult.Data.(*model.ChannelListWithTeamData) + assert.Len(t, *list, 1) + // Manually truncate Channels table until testlib can handle cleanups s.GetMaster().Exec("TRUNCATE Channels") } @@ -2325,6 +2344,15 @@ func testChannelStoreSearchAllChannels(t *testing.T, ss store.Store) { } store.Must(ss.Channel().Save(&o7, -1)) + group := &model.Group{ + Name: model.NewId(), + DisplayName: model.NewId(), + Source: model.GroupSourceLdap, + RemoteId: model.NewId(), + } + store.Must(ss.Group().Create(group)) + store.Must(ss.Group().CreateGroupSyncable(model.NewGroupChannel(group.Id, o7.Id, true))) + o8 := model.Channel{ TeamId: t1.Id, DisplayName: "Off-Limit", @@ -2381,28 +2409,31 @@ func testChannelStoreSearchAllChannels(t *testing.T, ss store.Store) { testCases := []struct { Description string Term string - IncludeDeleted bool + Opts store.ChannelSearchOpts ExpectedResults *model.ChannelList }{ - {"ChannelA", "ChannelA", false, &model.ChannelList{&o1, &o2, &o3}}, - {"ChannelA, include deleted", "ChannelA", true, &model.ChannelList{&o1, &o2, &o3, &o13}}, - {"empty string", "", false, &model.ChannelList{&o1, &o2, &o3, &o4, &o5, &o12, &o11, &o8, &o7, &o6, &o10, &o9}}, - {"no matches", "blargh", false, &model.ChannelList{}}, - {"prefix", "off-", false, &model.ChannelList{&o8, &o7, &o6}}, - {"full match with dash", "off-topic", false, &model.ChannelList{&o6}}, - {"town square", "town square", false, &model.ChannelList{&o9}}, - {"the in name", "the", false, &model.ChannelList{&o10}}, - {"Mobile", "Mobile", false, &model.ChannelList{&o11}}, - {"search purpose", "now searchable", false, &model.ChannelList{&o12}}, - {"pipe ignored", "town square |", false, &model.ChannelList{&o9}}, + {"ChannelA", "ChannelA", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o1, &o2, &o3}}, + {"ChannelA, include deleted", "ChannelA", store.ChannelSearchOpts{IncludeDeleted: true}, &model.ChannelList{&o1, &o2, &o3, &o13}}, + {"empty string", "", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o1, &o2, &o3, &o4, &o5, &o12, &o11, &o8, &o7, &o6, &o10, &o9}}, + {"no matches", "blargh", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{}}, + {"prefix", "off-", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o8, &o7, &o6}}, + {"full match with dash", "off-topic", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o6}}, + {"town square", "town square", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o9}}, + {"the in name", "the", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o10}}, + {"Mobile", "Mobile", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o11}}, + {"search purpose", "now searchable", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o12}}, + {"pipe ignored", "town square |", store.ChannelSearchOpts{IncludeDeleted: false}, &model.ChannelList{&o9}}, + {"exclude defaults search 'off'", "off-", store.ChannelSearchOpts{IncludeDeleted: false, ExcludeChannelNames: []string{"off-topic"}}, &model.ChannelList{&o8, &o7}}, + {"exclude defaults search 'town'", "town", store.ChannelSearchOpts{IncludeDeleted: false, ExcludeChannelNames: []string{"town-square"}}, &model.ChannelList{}}, + {"exclude by group association", "off", store.ChannelSearchOpts{IncludeDeleted: false, NotAssociatedToGroup: group.Id}, &model.ChannelList{&o8, &o6}}, } for _, testCase := range testCases { t.Run(testCase.Description, func(t *testing.T) { - result := <-ss.Channel().SearchAllChannels(testCase.Term, testCase.IncludeDeleted) + result := <-ss.Channel().SearchAllChannels(testCase.Term, testCase.Opts) require.Nil(t, result.Err) channels := result.Data.(*model.ChannelListWithTeamData) - require.Equal(t, len(*channels), len(*testCase.ExpectedResults)) + require.Equal(t, len(*testCase.ExpectedResults), len(*channels)) for i, expected := range *testCase.ExpectedResults { require.Equal(t, (*channels)[i].Id, expected.Id) } diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 9c92634405..bc38cd120f 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -212,13 +212,13 @@ func (_m *ChannelStore) GetAllChannelMembersNotifyPropsForChannel(channelId stri return r0 } -// GetAllChannels provides a mock function with given fields: page, perPage, includeDeleted -func (_m *ChannelStore) GetAllChannels(page int, perPage int, includeDeleted bool) store.StoreChannel { - ret := _m.Called(page, perPage, includeDeleted) +// GetAllChannels provides a mock function with given fields: page, perPage, opts +func (_m *ChannelStore) GetAllChannels(page int, perPage int, opts store.ChannelSearchOpts) store.StoreChannel { + ret := _m.Called(page, perPage, opts) var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func(int, int, bool) store.StoreChannel); ok { - r0 = rf(page, perPage, includeDeleted) + if rf, ok := ret.Get(0).(func(int, int, store.ChannelSearchOpts) store.StoreChannel); ok { + r0 = rf(page, perPage, opts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(store.StoreChannel) @@ -1003,13 +1003,13 @@ func (_m *ChannelStore) SaveMember(member *model.ChannelMember) store.StoreChann return r0 } -// SearchAllChannels provides a mock function with given fields: term, includeDeleted -func (_m *ChannelStore) SearchAllChannels(term string, includeDeleted bool) store.StoreChannel { - ret := _m.Called(term, includeDeleted) +// SearchAllChannels provides a mock function with given fields: term, opts +func (_m *ChannelStore) SearchAllChannels(term string, opts store.ChannelSearchOpts) store.StoreChannel { + ret := _m.Called(term, opts) var r0 store.StoreChannel - if rf, ok := ret.Get(0).(func(string, bool) store.StoreChannel); ok { - r0 = rf(term, includeDeleted) + if rf, ok := ret.Get(0).(func(string, store.ChannelSearchOpts) store.StoreChannel); ok { + r0 = rf(term, opts) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(store.StoreChannel) diff --git a/web/params.go b/web/params.go index b457800dcd..7fafe4bbbf 100644 --- a/web/params.go +++ b/web/params.go @@ -66,6 +66,8 @@ type Params struct { NotAssociatedToChannel string Paginate *bool IncludeMemberCount bool + NotAssociatedToGroup string + ExcludeDefaultChannels bool } func ParamsFromRequest(r *http.Request) *Params { @@ -260,5 +262,11 @@ func ParamsFromRequest(r *http.Request) *Params { params.IncludeMemberCount = val } + params.NotAssociatedToGroup = query.Get("not_associated_to_group") + + if val, err := strconv.ParseBool(query.Get("exclude_default_channels")); err == nil { + params.ExcludeDefaultChannels = val + } + return params }