mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
[MM-58834] Review user preferences export and import (#28286)
This commit is contained in:
parent
e8ebbcc980
commit
9d5993d89d
@ -29,43 +29,83 @@ import (
|
||||
|
||||
// We use this map to identify the exportable preferences.
|
||||
// Here we link the preference category and name, to the name of the relevant field in the import struct.
|
||||
var exportablePreferences = map[imports.ComparablePreference]string{{
|
||||
var exportablePreferences = map[imports.ComparablePreference]string{
|
||||
{
|
||||
Category: model.PreferenceCategoryTheme,
|
||||
Name: "",
|
||||
}: "Theme", {
|
||||
}: "Theme",
|
||||
{
|
||||
Category: model.PreferenceCategoryAdvancedSettings,
|
||||
Name: "feature_enabled_markdown_preview",
|
||||
}: "UseMarkdownPreview", {
|
||||
}: "UseMarkdownPreview",
|
||||
{
|
||||
Category: model.PreferenceCategoryAdvancedSettings,
|
||||
Name: "formatting",
|
||||
}: "UseFormatting", {
|
||||
}: "UseFormatting",
|
||||
{
|
||||
Category: model.PreferenceCategoryAdvancedSettings,
|
||||
Name: "send_on_ctrl_enter",
|
||||
}: "SendOnCtrlEnter",
|
||||
{
|
||||
Category: model.PreferenceCategoryAdvancedSettings,
|
||||
Name: "code_block_ctrl_enter",
|
||||
}: "CodeBlockCtrlEnter",
|
||||
{
|
||||
Category: model.PreferenceCategoryAdvancedSettings,
|
||||
Name: "join_leave",
|
||||
}: "ShowJoinLeave",
|
||||
{
|
||||
Category: model.PreferenceCategoryAdvancedSettings,
|
||||
Name: "unread_scroll_position",
|
||||
}: "ShowUnreadScrollPosition",
|
||||
{
|
||||
Category: model.PreferenceCategoryAdvancedSettings,
|
||||
Name: "sync_drafts",
|
||||
}: "SyncDrafts",
|
||||
{
|
||||
Category: model.PreferenceCategorySidebarSettings,
|
||||
Name: "show_unread_section",
|
||||
}: "ShowUnreadSection", {
|
||||
Name: model.PreferenceNameShowUnreadSection,
|
||||
}: "ShowUnreadSection",
|
||||
{
|
||||
Category: model.PreferenceCategorySidebarSettings,
|
||||
Name: model.PreferenceLimitVisibleDmsGms,
|
||||
}: "LimitVisibleDmsGms",
|
||||
{
|
||||
Category: model.PreferenceCategoryDisplaySettings,
|
||||
Name: model.PreferenceNameUseMilitaryTime,
|
||||
}: "UseMilitaryTime", {
|
||||
}: "UseMilitaryTime",
|
||||
{
|
||||
Category: model.PreferenceCategoryDisplaySettings,
|
||||
Name: model.PreferenceNameCollapseSetting,
|
||||
}: "CollapsePreviews", {
|
||||
}: "CollapsePreviews",
|
||||
{
|
||||
Category: model.PreferenceCategoryDisplaySettings,
|
||||
Name: model.PreferenceNameMessageDisplay,
|
||||
}: "MessageDisplay", {
|
||||
}: "MessageDisplay",
|
||||
{
|
||||
Category: model.PreferenceCategoryDisplaySettings,
|
||||
Name: "channel_display_mode",
|
||||
}: "CollapseConsecutive", {
|
||||
Name: model.PreferenceNameChannelDisplayMode,
|
||||
}: "ChannelDisplayMode",
|
||||
{
|
||||
Category: model.PreferenceCategoryDisplaySettings,
|
||||
Name: "collapse_consecutive_messages",
|
||||
}: "ColorizeUsernames", {
|
||||
Name: model.PreferenceNameCollapseConsecutive,
|
||||
}: "CollapseConsecutive",
|
||||
{
|
||||
Category: model.PreferenceCategoryDisplaySettings,
|
||||
Name: "colorize_usernames",
|
||||
}: "ChannelDisplayMode", {
|
||||
Name: model.PreferenceNameColorizeUsernames,
|
||||
}: "ColorizeUsernames",
|
||||
{
|
||||
Category: model.PreferenceCategoryDisplaySettings,
|
||||
Name: model.PreferenceNameNameFormat,
|
||||
}: "NameFormat",
|
||||
{
|
||||
Category: model.PreferenceCategoryTutorialSteps,
|
||||
Name: "",
|
||||
}: "TutorialStep", {
|
||||
}: "TutorialStep",
|
||||
{
|
||||
Category: model.PreferenceCategoryNotifications,
|
||||
Name: model.PreferenceNameEmailInterval,
|
||||
}: "EmailInterval",
|
||||
}: "EmailInterval",
|
||||
}
|
||||
|
||||
func (a *App) BulkExport(ctx request.CTX, writer io.Writer, outPath string, job *model.Job, opts model.BulkExportOpts) *model.AppError {
|
||||
|
@ -165,6 +165,13 @@ func ImportLineFromUser(user *model.User, exportedPrefs map[string]*string) *imp
|
||||
ChannelDisplayMode: exportedPrefs["ChannelDisplayMode"],
|
||||
TutorialStep: exportedPrefs["TutorialStep"],
|
||||
EmailInterval: exportedPrefs["EmailInterval"],
|
||||
NameFormat: exportedPrefs["NameFormat"],
|
||||
SendOnCtrlEnter: exportedPrefs["SendOnCtrlEnter"],
|
||||
ShowJoinLeave: exportedPrefs["ShowJoinLeave"],
|
||||
SyncDrafts: exportedPrefs["SyncDrafts"],
|
||||
ShowUnreadScrollPosition: exportedPrefs["ShowUnreadScrollPosition"],
|
||||
LimitVisibleDmsGms: exportedPrefs["LimitVisibleDmsGms"],
|
||||
CodeBlockCtrlEnter: exportedPrefs["CodeBlockCtrlEnter"],
|
||||
DeleteAt: &user.DeleteAt,
|
||||
},
|
||||
}
|
||||
@ -238,6 +245,7 @@ func ImportUserChannelDataFromChannelMemberAndPreferences(member *model.ChannelM
|
||||
}
|
||||
|
||||
func ImportLineForPost(post *model.PostForExport) *imports.LineImportData {
|
||||
f := []string(post.FlaggedBy)
|
||||
return &imports.LineImportData{
|
||||
Type: "post",
|
||||
Post: &imports.PostImportData{
|
||||
@ -249,6 +257,8 @@ func ImportLineForPost(post *model.PostForExport) *imports.LineImportData {
|
||||
Props: &post.Props,
|
||||
CreateAt: &post.CreateAt,
|
||||
EditAt: &post.EditAt,
|
||||
IsPinned: &post.IsPinned,
|
||||
FlaggedBy: &f,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -258,6 +268,7 @@ func ImportLineForDirectPost(post *model.DirectPostForExport) *imports.LineImpor
|
||||
if len(channelMembers) == 1 {
|
||||
channelMembers = []string{channelMembers[0], channelMembers[0]}
|
||||
}
|
||||
f := []string(post.FlaggedBy)
|
||||
return &imports.LineImportData{
|
||||
Type: "direct_post",
|
||||
DirectPost: &imports.DirectPostImportData{
|
||||
@ -268,17 +279,22 @@ func ImportLineForDirectPost(post *model.DirectPostForExport) *imports.LineImpor
|
||||
Props: &post.Props,
|
||||
CreateAt: &post.CreateAt,
|
||||
EditAt: &post.EditAt,
|
||||
IsPinned: &post.IsPinned,
|
||||
FlaggedBy: &f,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func ImportReplyFromPost(post *model.ReplyForExport) *imports.ReplyImportData {
|
||||
f := []string(post.FlaggedBy)
|
||||
return &imports.ReplyImportData{
|
||||
User: &post.Username,
|
||||
Type: &post.Type,
|
||||
Message: &post.Message,
|
||||
CreateAt: &post.CreateAt,
|
||||
EditAt: &post.EditAt,
|
||||
IsPinned: &post.IsPinned,
|
||||
FlaggedBy: &f,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -722,7 +722,7 @@ func (a *App) importUser(rctx request.CTX, data *imports.UserImportData, dryRun
|
||||
preferences = append(preferences, model.Preference{
|
||||
UserId: savedUser.Id,
|
||||
Category: model.PreferenceCategoryDisplaySettings,
|
||||
Name: "channel_display_mode",
|
||||
Name: model.PreferenceNameChannelDisplayMode,
|
||||
Value: *data.ChannelDisplayMode,
|
||||
})
|
||||
}
|
||||
@ -763,6 +763,69 @@ func (a *App) importUser(rctx request.CTX, data *imports.UserImportData, dryRun
|
||||
})
|
||||
}
|
||||
|
||||
if data.SendOnCtrlEnter != nil {
|
||||
preferences = append(preferences, model.Preference{
|
||||
UserId: savedUser.Id,
|
||||
Category: model.PreferenceCategoryAdvancedSettings,
|
||||
Name: "send_on_ctrl_enter",
|
||||
Value: *data.SendOnCtrlEnter,
|
||||
})
|
||||
}
|
||||
|
||||
if data.CodeBlockCtrlEnter != nil {
|
||||
preferences = append(preferences, model.Preference{
|
||||
UserId: savedUser.Id,
|
||||
Category: model.PreferenceCategoryAdvancedSettings,
|
||||
Name: "code_block_ctrl_enter",
|
||||
Value: *data.CodeBlockCtrlEnter,
|
||||
})
|
||||
}
|
||||
|
||||
if data.ShowJoinLeave != nil {
|
||||
preferences = append(preferences, model.Preference{
|
||||
UserId: savedUser.Id,
|
||||
Category: model.PreferenceCategoryAdvancedSettings,
|
||||
Name: "join_leave",
|
||||
Value: *data.ShowJoinLeave,
|
||||
})
|
||||
}
|
||||
|
||||
if data.ShowUnreadScrollPosition != nil {
|
||||
preferences = append(preferences, model.Preference{
|
||||
UserId: savedUser.Id,
|
||||
Category: model.PreferenceCategoryAdvancedSettings,
|
||||
Name: "unread_scroll_position",
|
||||
Value: *data.ShowUnreadScrollPosition,
|
||||
})
|
||||
}
|
||||
|
||||
if data.SyncDrafts != nil {
|
||||
preferences = append(preferences, model.Preference{
|
||||
UserId: savedUser.Id,
|
||||
Category: model.PreferenceCategoryAdvancedSettings,
|
||||
Name: "sync_drafts",
|
||||
Value: *data.SyncDrafts,
|
||||
})
|
||||
}
|
||||
|
||||
if data.LimitVisibleDmsGms != nil {
|
||||
preferences = append(preferences, model.Preference{
|
||||
UserId: savedUser.Id,
|
||||
Category: model.PreferenceCategorySidebarSettings,
|
||||
Name: model.PreferenceLimitVisibleDmsGms,
|
||||
Value: *data.LimitVisibleDmsGms,
|
||||
})
|
||||
}
|
||||
|
||||
if data.NameFormat != nil {
|
||||
preferences = append(preferences, model.Preference{
|
||||
UserId: savedUser.Id,
|
||||
Category: model.PreferenceCategoryDisplaySettings,
|
||||
Name: model.PreferenceNameNameFormat,
|
||||
Value: *data.NameFormat,
|
||||
})
|
||||
}
|
||||
|
||||
if data.EmailInterval != nil || savedUser.NotifyProps[model.EmailNotifyProp] == "false" {
|
||||
var intervalSeconds string
|
||||
if value := savedUser.NotifyProps[model.EmailNotifyProp]; value == "false" {
|
||||
@ -1184,6 +1247,9 @@ func (a *App) importReplies(rctx request.CTX, data []imports.ReplyImportData, po
|
||||
return err
|
||||
}
|
||||
usernames = append(usernames, *replyData.User)
|
||||
if replyData.FlaggedBy != nil {
|
||||
usernames = append(usernames, *replyData.FlaggedBy...)
|
||||
}
|
||||
}
|
||||
|
||||
users, err := a.getUsersByUsernames(usernames)
|
||||
@ -1233,6 +1299,9 @@ func (a *App) importReplies(rctx request.CTX, data []imports.ReplyImportData, po
|
||||
if replyData.EditAt != nil {
|
||||
reply.EditAt = *replyData.EditAt
|
||||
}
|
||||
if replyData.IsPinned != nil {
|
||||
reply.IsPinned = *replyData.IsPinned
|
||||
}
|
||||
|
||||
fileIDs := a.uploadAttachments(rctx, replyData.Attachments, reply, teamID, extractContent)
|
||||
for _, fileID := range reply.FileIds {
|
||||
@ -1274,6 +1343,27 @@ func (a *App) importReplies(rctx request.CTX, data []imports.ReplyImportData, po
|
||||
|
||||
for _, postWithData := range postsWithData {
|
||||
a.updateFileInfoWithPostId(rctx, postWithData.post)
|
||||
|
||||
if postWithData.replyData.FlaggedBy != nil {
|
||||
var preferences model.Preferences
|
||||
|
||||
for _, username := range *postWithData.replyData.FlaggedBy {
|
||||
user := users[strings.ToLower(username)]
|
||||
|
||||
preferences = append(preferences, model.Preference{
|
||||
UserId: user.Id,
|
||||
Category: model.PreferenceCategoryFlaggedPost,
|
||||
Name: postWithData.post.Id,
|
||||
Value: "true",
|
||||
})
|
||||
}
|
||||
|
||||
if len(preferences) > 0 {
|
||||
if err := a.Srv().Store().Preference().Save(preferences); err != nil {
|
||||
return model.NewAppError("BulkImport", "app.import.import_post.save_preferences.error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -1179,6 +1179,13 @@ func TestImportImportUser(t *testing.T) {
|
||||
UseFormatting: ptrStr("true"),
|
||||
ShowUnreadSection: ptrStr("true"),
|
||||
EmailInterval: ptrStr("immediately"),
|
||||
NameFormat: model.NewPointer("full_name"),
|
||||
SendOnCtrlEnter: model.NewPointer("true"),
|
||||
CodeBlockCtrlEnter: model.NewPointer("true"),
|
||||
ShowJoinLeave: model.NewPointer("false"),
|
||||
SyncDrafts: model.NewPointer("false"),
|
||||
ShowUnreadScrollPosition: model.NewPointer("start_from_newest"),
|
||||
LimitVisibleDmsGms: model.NewPointer("20"),
|
||||
}
|
||||
appErr = th.App.importUser(th.Context, &data, false)
|
||||
assert.Nil(t, appErr)
|
||||
@ -1197,7 +1204,13 @@ func TestImportImportUser(t *testing.T) {
|
||||
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "feature_enabled_markdown_preview", *data.UseMarkdownPreview)
|
||||
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "formatting", *data.UseFormatting)
|
||||
checkPreference(t, th.App, user.Id, model.PreferenceCategorySidebarSettings, "show_unread_section", *data.ShowUnreadSection)
|
||||
checkPreference(t, th.App, user.Id, model.PreferenceCategoryNotifications, model.PreferenceNameEmailInterval, "30")
|
||||
checkPreference(t, th.App, user.Id, model.PreferenceCategoryDisplaySettings, model.PreferenceNameNameFormat, "full_name")
|
||||
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "send_on_ctrl_enter", "true")
|
||||
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "code_block_ctrl_enter", "true")
|
||||
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "join_leave", "false")
|
||||
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "sync_drafts", "false")
|
||||
checkPreference(t, th.App, user.Id, model.PreferenceCategoryAdvancedSettings, "unread_scroll_position", "start_from_newest")
|
||||
checkPreference(t, th.App, user.Id, model.PreferenceCategorySidebarSettings, model.PreferenceLimitVisibleDmsGms, "20")
|
||||
|
||||
// Change those preferences.
|
||||
data = imports.UserImportData{
|
||||
@ -2556,6 +2569,44 @@ func TestImportimportMultiplePostLines(t *testing.T) {
|
||||
AssertAllPostsCount(t, th.App, initialPostCountForTeam2, 1, team2.Id)
|
||||
AssertAllPostsCount(t, th.App, initialPostCount, 15, team.Id)
|
||||
|
||||
t.Run("Importing a post with a reply both pinned", func(t *testing.T) {
|
||||
// Create a thread.
|
||||
importCreate := time.Now().Add(-1 * time.Minute).UnixMilli()
|
||||
replyCreate := time.Now().Add(-30 * time.Second).UnixMilli()
|
||||
data = imports.LineImportWorkerData{
|
||||
LineImportData: imports.LineImportData{
|
||||
Post: &imports.PostImportData{
|
||||
Team: &teamName,
|
||||
Channel: &channelName,
|
||||
User: &user.Username,
|
||||
Message: model.NewPointer("Thread Message"),
|
||||
CreateAt: model.NewPointer(importCreate),
|
||||
IsPinned: model.NewPointer(true),
|
||||
Replies: &[]imports.ReplyImportData{{
|
||||
User: &user.Username,
|
||||
Message: model.NewPointer("Reply"),
|
||||
CreateAt: model.NewPointer(replyCreate),
|
||||
IsPinned: model.NewPointer(true),
|
||||
}},
|
||||
},
|
||||
},
|
||||
LineNumber: 1,
|
||||
}
|
||||
|
||||
_, err = th.App.importMultiplePostLines(th.Context, []imports.LineImportWorkerData{data}, false, true)
|
||||
require.Nil(t, err)
|
||||
|
||||
resultPosts, nErr = th.App.Srv().Store().Post().GetPostsCreatedAt(channel.Id, importCreate)
|
||||
require.NoError(t, nErr)
|
||||
require.Equal(t, 1, len(resultPosts))
|
||||
require.True(t, resultPosts[0].IsPinned)
|
||||
|
||||
resultPosts, nErr = th.App.Srv().Store().Post().GetPostsCreatedAt(channel.Id, replyCreate)
|
||||
require.NoError(t, nErr)
|
||||
require.Equal(t, 1, len(resultPosts))
|
||||
require.True(t, resultPosts[0].IsPinned)
|
||||
})
|
||||
|
||||
t.Run("Importing a post with a thread", func(t *testing.T) {
|
||||
// Create a thread.
|
||||
importCreate := time.Now().Add(-1 * time.Minute).UnixMilli()
|
||||
@ -2715,6 +2766,76 @@ func TestImportimportMultiplePostLines(t *testing.T) {
|
||||
|
||||
assert.ElementsMatch(t, []string{user.Id}, followers)
|
||||
})
|
||||
|
||||
t.Run("Importing a post that someone flagged", func(t *testing.T) {
|
||||
// Create a thread.
|
||||
importCreate := time.Now().Add(-1 * time.Minute).UnixMilli()
|
||||
data = imports.LineImportWorkerData{
|
||||
LineImportData: imports.LineImportData{
|
||||
Post: &imports.PostImportData{
|
||||
Team: &teamName,
|
||||
Channel: &channelName,
|
||||
User: &user.Username,
|
||||
Message: model.NewPointer("Flagged Message"),
|
||||
CreateAt: model.NewPointer(importCreate),
|
||||
FlaggedBy: &[]string{user.Username},
|
||||
},
|
||||
},
|
||||
LineNumber: 1,
|
||||
}
|
||||
|
||||
errLine, err = th.App.importMultiplePostLines(th.Context, []imports.LineImportWorkerData{data}, false, true)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 0, errLine)
|
||||
|
||||
resultPosts, nErr = th.App.Srv().Store().Post().GetPostsCreatedAt(channel.Id, importCreate)
|
||||
require.NoError(t, nErr)
|
||||
require.Equal(t, 1, len(resultPosts))
|
||||
|
||||
pref, err := th.App.ch.srv.Store().Preference().GetCategoryAndName(model.PreferenceCategoryFlaggedPost, resultPosts[0].Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, pref, 1)
|
||||
assert.Equal(t, user.Id, pref[0].UserId)
|
||||
})
|
||||
|
||||
t.Run("Importing a post that someone flagged its replies", func(t *testing.T) {
|
||||
// Create a thread.
|
||||
importCreate := time.Now().Add(-1 * time.Minute).UnixMilli()
|
||||
replyCreate := time.Now().Add(-30 * time.Second).UnixMilli()
|
||||
data = imports.LineImportWorkerData{
|
||||
LineImportData: imports.LineImportData{
|
||||
Post: &imports.PostImportData{
|
||||
Team: &teamName,
|
||||
Channel: &channelName,
|
||||
User: &user.Username,
|
||||
Message: model.NewPointer("Flagged Message"),
|
||||
CreateAt: model.NewPointer(importCreate),
|
||||
Replies: &[]imports.ReplyImportData{{
|
||||
User: &user.Username,
|
||||
Message: model.NewPointer("Reply"),
|
||||
CreateAt: model.NewPointer(replyCreate),
|
||||
FlaggedBy: &[]string{user2.Username},
|
||||
}},
|
||||
},
|
||||
},
|
||||
LineNumber: 1,
|
||||
}
|
||||
|
||||
errLine, err = th.App.importMultiplePostLines(th.Context, []imports.LineImportWorkerData{data}, false, true)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 0, errLine)
|
||||
|
||||
resultPosts, nErr = th.App.Srv().Store().Post().GetPostsCreatedAt(channel.Id, replyCreate)
|
||||
require.NoError(t, nErr)
|
||||
require.Equal(t, 1, len(resultPosts))
|
||||
|
||||
pref, err := th.App.ch.srv.Store().Preference().GetCategoryAndName(model.PreferenceCategoryFlaggedPost, resultPosts[0].Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, pref, 1)
|
||||
assert.Equal(t, user2.Id, pref[0].UserId)
|
||||
})
|
||||
}
|
||||
|
||||
func TestImportImportPost(t *testing.T) {
|
||||
|
@ -73,6 +73,14 @@ type UserImportData struct {
|
||||
ShowUnreadSection *string `json:"show_unread_section,omitempty"`
|
||||
DeleteAt *int64 `json:"delete_at,omitempty"`
|
||||
|
||||
SendOnCtrlEnter *string `json:"send_on_ctrl_enter,omitempty"`
|
||||
CodeBlockCtrlEnter *string `json:"code_block_ctrl_enter,omitempty"`
|
||||
ShowJoinLeave *string `json:"show_join_leave,omitempty"`
|
||||
ShowUnreadScrollPosition *string `json:"show_unread_scroll_position,omitempty"`
|
||||
SyncDrafts *string `json:"sync_drafts,omitempty"`
|
||||
LimitVisibleDmsGms *string `json:"limit_visible_dms_gms,omitempty"`
|
||||
NameFormat *string `json:"name_format,omitempty"`
|
||||
|
||||
Teams *[]UserTeamImportData `json:"teams,omitempty"`
|
||||
|
||||
Theme *string `json:"theme,omitempty"`
|
||||
@ -169,6 +177,7 @@ type ReplyImportData struct {
|
||||
FlaggedBy *[]string `json:"flagged_by,omitempty"`
|
||||
Reactions *[]ReactionImportData `json:"reactions,omitempty"`
|
||||
Attachments *[]AttachmentImportData `json:"attachments,omitempty"`
|
||||
IsPinned *bool `json:"is_pinned,omitempty"`
|
||||
}
|
||||
|
||||
type PostImportData struct {
|
||||
@ -210,7 +219,7 @@ type DirectPostImportData struct {
|
||||
CreateAt *int64 `json:"create_at"`
|
||||
EditAt *int64 `json:"edit_at"`
|
||||
|
||||
FlaggedBy *[]string `json:"flagged_by"`
|
||||
FlaggedBy *[]string `json:"flagged_by,omitempty"`
|
||||
Reactions *[]ReactionImportData `json:"reactions"`
|
||||
Replies *[]ReplyImportData `json:"replies"`
|
||||
Attachments *[]AttachmentImportData `json:"attachments"`
|
||||
|
@ -295,7 +295,7 @@ func (a *App) createUserOrGuest(c request.CTX, user *model.User, guest bool) (*m
|
||||
a.sendUpdatedUserEvent(*nUser)
|
||||
}
|
||||
|
||||
recommendedNextStepsPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceRecommendedNextSteps, Name: "hide", Value: "false"}
|
||||
recommendedNextStepsPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryRecommendedNextSteps, Name: model.PreferenceNameRecommendedNextStepsHide, Value: "false"}
|
||||
tutorialStepPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategoryTutorialSteps, Name: ruser.Id, Value: "0"}
|
||||
gmASdmPref := model.Preference{UserId: ruser.Id, Category: model.PreferenceCategorySystemNotice, Name: "GMasDM", Value: "true"}
|
||||
|
||||
|
@ -107,6 +107,15 @@ func postSliceColumns() []string {
|
||||
return cols
|
||||
}
|
||||
|
||||
func postSliceColumnsWithName(name string) []string {
|
||||
colInfos := postSliceColumnsWithTypes()
|
||||
cols := make([]string, len(colInfos))
|
||||
for i, colInfo := range colInfos {
|
||||
cols[i] = name + "." + colInfo.Name
|
||||
}
|
||||
return cols
|
||||
}
|
||||
|
||||
func postSliceCoalesceQuery() string {
|
||||
colInfos := postSliceColumnsWithTypes()
|
||||
cols := make([]string, len(colInfos))
|
||||
@ -2661,13 +2670,22 @@ func (s *SqlPostStore) GetParentsForExportAfter(limit int, afterId string, inclu
|
||||
excludeDeletedCond = append(excludeDeletedCond, sq.Eq{"Channels.DeleteAt": 0})
|
||||
}
|
||||
|
||||
aggFn := "COALESCE(json_agg(u1.username) FILTER (WHERE u1.username IS NOT NULL), '[]')"
|
||||
if s.DriverName() == model.DatabaseDriverMysql {
|
||||
aggFn = "IF (COUNT(u1.Username) = 0, JSON_ARRAY(), JSON_ARRAYAGG(u1.Username))"
|
||||
}
|
||||
result := []*model.PostForExport{}
|
||||
|
||||
builder := s.getQueryBuilder().
|
||||
Select("p1.*, Users.Username as Username, Teams.Name as TeamName, Channels.Name as ChannelName").
|
||||
Select(fmt.Sprintf("%s, Users.Username as Username, Teams.Name as TeamName, Channels.Name as ChannelName, %s as FlaggedBy", strings.Join(postSliceColumnsWithName("p1"), ", "), aggFn)).
|
||||
FromSelect(sq.Select("*").From("Posts").Where(sq.Eq{"Posts.Id": rootIds}), "p1").
|
||||
LeftJoin("Preferences ON p1.Id = Preferences.Name").
|
||||
LeftJoin("Users u1 ON Preferences.UserId = u1.Id").
|
||||
InnerJoin("Channels ON p1.ChannelId = Channels.Id").
|
||||
InnerJoin("Teams ON Channels.TeamId = Teams.Id").
|
||||
InnerJoin("Users ON p1.UserId = Users.Id").
|
||||
Where(excludeDeletedCond).
|
||||
GroupBy(fmt.Sprintf("%s, Users.Username, Teams.Name, Channels.Name", strings.Join(postSliceColumnsWithName("p1"), ", "))).
|
||||
OrderBy("p1.Id")
|
||||
|
||||
query, args, err := builder.ToSql()
|
||||
@ -2675,56 +2693,72 @@ func (s *SqlPostStore) GetParentsForExportAfter(limit int, afterId string, inclu
|
||||
return nil, errors.Wrap(err, "postsForExport_toSql")
|
||||
}
|
||||
|
||||
err = s.GetSearchReplicaX().Select(&postsForExport, query, args...)
|
||||
err = s.GetSearchReplicaX().Select(&result, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find Posts")
|
||||
}
|
||||
|
||||
if len(postsForExport) == 0 {
|
||||
if len(result) == 0 {
|
||||
// All of the posts were in channels or teams that were deleted.
|
||||
// Update the afterId and try again.
|
||||
afterId = rootIds[len(rootIds)-1]
|
||||
continue
|
||||
}
|
||||
|
||||
return postsForExport, nil
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SqlPostStore) GetRepliesForExport(rootId string) ([]*model.ReplyForExport, error) {
|
||||
posts := []*model.ReplyForExport{}
|
||||
err := s.GetSearchReplicaX().Select(&posts, `
|
||||
SELECT
|
||||
Posts.*,
|
||||
Users.Username as Username
|
||||
FROM
|
||||
Posts
|
||||
INNER JOIN
|
||||
Users ON Posts.UserId = Users.Id
|
||||
WHERE
|
||||
Posts.RootId = ?
|
||||
AND Posts.DeleteAt = 0
|
||||
ORDER BY
|
||||
Posts.Id`, rootId)
|
||||
aggFn := "COALESCE(json_agg(u1.username) FILTER (WHERE u1.username IS NOT NULL), '[]')"
|
||||
if s.DriverName() == model.DatabaseDriverMysql {
|
||||
aggFn = "IF (COUNT(u1.Username) = 0, JSON_ARRAY(), JSON_ARRAYAGG(u1.Username))"
|
||||
}
|
||||
result := []*model.ReplyForExport{}
|
||||
|
||||
qb := s.getQueryBuilder().Select(fmt.Sprintf("Posts.*, u2.Username as Username, %s as FlaggedBy", aggFn)).
|
||||
From("Posts").
|
||||
LeftJoin("Preferences ON Posts.Id = Preferences.Name").
|
||||
LeftJoin("Users u1 ON Preferences.UserId = u1.Id").
|
||||
InnerJoin("Users u2 ON Posts.UserId = u2.Id").
|
||||
Where(sq.And{sq.Eq{"Posts.RootId": rootId}, sq.Eq{"Posts.DeleteAt": 0}}).
|
||||
GroupBy("Posts.Id, u2.Username").
|
||||
OrderBy("Posts.Id")
|
||||
|
||||
query, args, err := qb.ToSql()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "postsForExport_toSql")
|
||||
}
|
||||
|
||||
err = s.GetSearchReplicaX().Select(&result, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find Posts")
|
||||
}
|
||||
|
||||
return posts, nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId string, includeArchivedChannels bool) ([]*model.DirectPostForExport, error) {
|
||||
aggFn := "COALESCE(json_agg(u1.username) FILTER (WHERE u1.username IS NOT NULL), '[]')"
|
||||
if s.DriverName() == model.DatabaseDriverMysql {
|
||||
aggFn = "IF (COUNT(u1.Username) = 0, JSON_ARRAY(), JSON_ARRAYAGG(u1.Username))"
|
||||
}
|
||||
result := []*model.DirectPostForExport{}
|
||||
|
||||
query := s.getQueryBuilder().
|
||||
Select("p.*", "Users.Username as User").
|
||||
Select(fmt.Sprintf("p.*, u2.Username as User, %s as FlaggedBy", aggFn)).
|
||||
From("Posts p").
|
||||
LeftJoin("Preferences ON p.Id = Preferences.Name").
|
||||
LeftJoin("Users u1 ON Preferences.UserId = u1.Id").
|
||||
Join("Channels ON p.ChannelId = Channels.Id").
|
||||
Join("Users ON p.UserId = Users.Id").
|
||||
Join("Users u2 ON p.UserId = u2.Id").
|
||||
Where(sq.And{
|
||||
sq.Gt{"p.Id": afterId},
|
||||
sq.Eq{"p.RootId": ""},
|
||||
sq.Eq{"p.DeleteAt": 0},
|
||||
sq.Eq{"Channels.Type": []model.ChannelType{model.ChannelTypeDirect, model.ChannelTypeGroup}},
|
||||
}).
|
||||
GroupBy("p.Id, u2.Username").
|
||||
OrderBy("p.Id").
|
||||
Limit(uint64(limit))
|
||||
|
||||
@ -2739,13 +2773,12 @@ func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId str
|
||||
return nil, errors.Wrap(err, "post_tosql")
|
||||
}
|
||||
|
||||
posts := []*model.DirectPostForExport{}
|
||||
if err2 := s.GetReplicaX().Select(&posts, queryString, args...); err2 != nil {
|
||||
if err2 := s.GetReplicaX().Select(&result, queryString, args...); err2 != nil {
|
||||
return nil, errors.Wrap(err2, "failed to find Posts")
|
||||
}
|
||||
var channelIds []string
|
||||
for _, post := range posts {
|
||||
channelIds = append(channelIds, post.ChannelId)
|
||||
for _, p := range result {
|
||||
channelIds = append(channelIds, p.ChannelId)
|
||||
}
|
||||
query = s.getQueryBuilder().
|
||||
Select("u.Username as Username, ChannelId, UserId, cm.Roles as Roles, LastViewedAt, MsgCount, MentionCount, MentionCountRoot, cm.NotifyProps as NotifyProps, LastUpdateAt, SchemeUser, SchemeAdmin, (SchemeGuest IS NOT NULL AND SchemeGuest) as SchemeGuest").
|
||||
@ -2761,13 +2794,13 @@ func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId str
|
||||
}
|
||||
|
||||
channelMembers := []*model.ChannelMemberForExport{}
|
||||
if err := s.GetReplicaX().Select(&channelMembers, queryString, args...); err != nil {
|
||||
if err = s.GetReplicaX().Select(&channelMembers, queryString, args...); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to find ChannelMembers")
|
||||
}
|
||||
|
||||
// Build a map of channels and their posts
|
||||
postsChannelMap := make(map[string][]*model.DirectPostForExport)
|
||||
for _, post := range posts {
|
||||
for _, post := range result {
|
||||
post.ChannelMembers = &[]string{}
|
||||
postsChannelMap[post.ChannelId] = append(postsChannelMap[post.ChannelId], post)
|
||||
}
|
||||
@ -2784,7 +2817,8 @@ func (s *SqlPostStore) GetDirectPostParentsForExportAfter(limit int, afterId str
|
||||
*post.ChannelMembers = channelMembersMap[channelId]
|
||||
}
|
||||
}
|
||||
return posts, nil
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
//nolint:unparam
|
||||
|
@ -4349,6 +4349,28 @@ func testPostStoreGetParentsForExportAfter(t *testing.T, rctx request.CTX, ss st
|
||||
}
|
||||
assert.True(t, found)
|
||||
})
|
||||
|
||||
t.Run("with flagged post", func(t *testing.T) {
|
||||
err := ss.Preference().Save(model.Preferences([]model.Preference{
|
||||
{
|
||||
UserId: u1.Id,
|
||||
Category: model.PreferenceCategoryFlaggedPost,
|
||||
Name: p1.Id,
|
||||
Value: "true",
|
||||
},
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
|
||||
posts, err := ss.Post().GetParentsForExportAfter(10000, strings.Repeat("0", 26), false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, p := range posts {
|
||||
if p.Id == p1.Id {
|
||||
require.NotNil(t, p.FlaggedBy)
|
||||
assert.Equal(t, model.StringArray([]string{u1.Username}), p.FlaggedBy)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func testPostStoreGetRepliesForExport(t *testing.T, rctx request.CTX, ss store.Store) {
|
||||
@ -4394,7 +4416,7 @@ func testPostStoreGetRepliesForExport(t *testing.T, rctx request.CTX, ss store.S
|
||||
r1, err := ss.Post().GetRepliesForExport(p1.Id)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, r1, 1)
|
||||
require.Len(t, r1, 1)
|
||||
|
||||
reply1 := r1[0]
|
||||
assert.Equal(t, reply1.Id, p2.Id)
|
||||
@ -4409,7 +4431,7 @@ func testPostStoreGetRepliesForExport(t *testing.T, rctx request.CTX, ss store.S
|
||||
r1, err = ss.Post().GetRepliesForExport(p1.Id)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, r1, 1)
|
||||
require.Len(t, r1, 1)
|
||||
|
||||
reply1 = r1[0]
|
||||
assert.Equal(t, reply1.Id, p2.Id)
|
||||
|
@ -235,17 +235,20 @@ type PostForExport struct {
|
||||
ChannelName string
|
||||
Username string
|
||||
ReplyCount int
|
||||
FlaggedBy StringArray
|
||||
}
|
||||
|
||||
type DirectPostForExport struct {
|
||||
Post
|
||||
User string
|
||||
ChannelMembers *[]string
|
||||
FlaggedBy StringArray
|
||||
}
|
||||
|
||||
type ReplyForExport struct {
|
||||
Post
|
||||
Username string
|
||||
FlaggedBy StringArray
|
||||
}
|
||||
|
||||
type PostForIndexing struct {
|
||||
|
@ -13,15 +13,70 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// The primary key for the preference table is the combination of User.Id, Category, and Name.
|
||||
|
||||
// PreferenceCategoryDirectChannelShow and PreferenceCategoryGroupChannelShow
|
||||
// are used to store the user's preferences for which channels to show in the sidebar.
|
||||
// The Name field is the channel ID.
|
||||
PreferenceCategoryDirectChannelShow = "direct_channel_show"
|
||||
PreferenceCategoryGroupChannelShow = "group_channel_show"
|
||||
// PreferenceCategoryTutorialStep is used to store the user's progress in the tutorial.
|
||||
// The Name field is the user ID again (for whatever reason).
|
||||
PreferenceCategoryTutorialSteps = "tutorial_step"
|
||||
// PreferenceCategoryAdvancedSettings has settings for the user's advanced settings.
|
||||
// The Name field is the setting name. Possible values are:
|
||||
// - "formatting"
|
||||
// - "send_on_ctrl_enter"
|
||||
// - "join_leave"
|
||||
// - "unread_scroll_position"
|
||||
// - "sync_drafts"
|
||||
// - "feature_enabled_markdown_preview" <- deprecated in favor of "formatting"
|
||||
PreferenceCategoryAdvancedSettings = "advanced_settings"
|
||||
// PreferenceCategoryFlaggedPost is used to store the user's saved posts.
|
||||
// The Name field is the post ID.
|
||||
PreferenceCategoryFlaggedPost = "flagged_post"
|
||||
// PreferenceCategoryFavoriteChannel is used to store the user's favorite channels to be
|
||||
// shown in the sidebar. The Name field is the channel ID.
|
||||
PreferenceCategoryFavoriteChannel = "favorite_channel"
|
||||
// PreferenceCategorySidebarSettings is used to store the user's sidebar settings.
|
||||
// The Name field is the setting name. (ie. PreferenceNameShowUnreadSection or PreferenceLimitVisibleDmsGms)
|
||||
PreferenceCategorySidebarSettings = "sidebar_settings"
|
||||
|
||||
// PreferenceCategoryDisplaySettings is used to store the user's various display settings.
|
||||
// The possible Name fields are:
|
||||
// - PreferenceNameUseMilitaryTime
|
||||
// - PreferenceNameCollapseSetting
|
||||
// - PreferenceNameMessageDisplay
|
||||
// - PreferenceNameCollapseConsecutive
|
||||
// - PreferenceNameColorizeUsernames
|
||||
// - PreferenceNameChannelDisplayMode
|
||||
// - PreferenceNameNameFormat
|
||||
PreferenceCategoryDisplaySettings = "display_settings"
|
||||
// PreferenceCategorySystemNotice is used store system admin notices.
|
||||
// Possible Name values are not defined here. It can be anything with the notice name.
|
||||
PreferenceCategorySystemNotice = "system_notice"
|
||||
// Deprecated: PreferenceCategoryLast is not used anymore.
|
||||
PreferenceCategoryLast = "last"
|
||||
// PreferenceCategoryCustomStatus is used to store the user's custom status preferences.
|
||||
// Possible Name values are:
|
||||
// - PreferenceNameRecentCustomStatuses
|
||||
// - PreferenceNameCustomStatusTutorialState
|
||||
// - PreferenceCustomStatusModalViewed
|
||||
PreferenceCategoryCustomStatus = "custom_status"
|
||||
// PreferenceCategoryNotifications is used to store the user's notification settings.
|
||||
// Possible Name values are:
|
||||
// - PreferenceNameEmailInterval
|
||||
PreferenceCategoryNotifications = "notifications"
|
||||
|
||||
// Deprecated: PreferenceRecommendedNextSteps is not used anymore.
|
||||
// Use PreferenceCategoryRecommendedNextSteps instead.
|
||||
// PreferenceRecommendedNextSteps is actually a Category. The only possible
|
||||
// Name vaule is PreferenceRecommendedNextStepsHide for now.
|
||||
PreferenceRecommendedNextSteps = PreferenceCategoryRecommendedNextSteps
|
||||
PreferenceCategoryRecommendedNextSteps = "recommended_next_steps"
|
||||
|
||||
// PreferenceCategoryTheme has the name for the team id where theme is set.
|
||||
PreferenceCategoryTheme = "theme"
|
||||
|
||||
PreferenceNameCollapsedThreadsEnabled = "collapsed_reply_threads"
|
||||
PreferenceNameChannelDisplayMode = "channel_display_mode"
|
||||
PreferenceNameCollapseSetting = "collapse_previews"
|
||||
@ -30,27 +85,25 @@ const (
|
||||
PreferenceNameColorizeUsernames = "colorize_usernames"
|
||||
PreferenceNameNameFormat = "name_format"
|
||||
PreferenceNameUseMilitaryTime = "use_military_time"
|
||||
PreferenceRecommendedNextSteps = "recommended_next_steps"
|
||||
|
||||
PreferenceCategorySystemNotice = "system_notice"
|
||||
PreferenceNameShowUnreadSection = "show_unread_section"
|
||||
PreferenceLimitVisibleDmsGms = "limit_visible_dms_gms"
|
||||
|
||||
PreferenceCategoryTheme = "theme"
|
||||
// the name for theme props is the team id
|
||||
PreferenceMaxLimitVisibleDmsGmsValue = 40
|
||||
MaxPreferenceValueLength = 20000
|
||||
|
||||
PreferenceCategoryAuthorizedOAuthApp = "oauth_app"
|
||||
// the name for oauth_app is the client_id and value is the current scope
|
||||
|
||||
PreferenceCategoryLast = "last"
|
||||
// Deprecated: PreferenceCategoryLastChannel is not used anymore.
|
||||
PreferenceNameLastChannel = "channel"
|
||||
// Deprecated: PreferenceCategoryLastTeam is not used anymore.
|
||||
PreferenceNameLastTeam = "team"
|
||||
|
||||
PreferenceCategoryCustomStatus = "custom_status"
|
||||
PreferenceNameRecentCustomStatuses = "recent_custom_statuses"
|
||||
PreferenceNameCustomStatusTutorialState = "custom_status_tutorial_state"
|
||||
|
||||
PreferenceCustomStatusModalViewed = "custom_status_modal_viewed"
|
||||
|
||||
PreferenceCategoryNotifications = "notifications"
|
||||
PreferenceNameEmailInterval = "email_interval"
|
||||
|
||||
PreferenceEmailIntervalNoBatchingSeconds = "30" // the "immediate" setting is actually 30s
|
||||
@ -62,9 +115,7 @@ const (
|
||||
PreferenceEmailIntervalHourAsSeconds = "3600"
|
||||
PreferenceCloudUserEphemeralInfo = "cloud_user_ephemeral_info"
|
||||
|
||||
PreferenceLimitVisibleDmsGms = "limit_visible_dms_gms"
|
||||
PreferenceMaxLimitVisibleDmsGmsValue = 40
|
||||
MaxPreferenceValueLength = 20000
|
||||
PreferenceNameRecommendedNextStepsHide = "hide"
|
||||
)
|
||||
|
||||
type Preference struct {
|
||||
|
Loading…
Reference in New Issue
Block a user