diff --git a/server/channels/app/migrations.go b/server/channels/app/migrations.go index 6c1615f2109..c7d1aa78127 100644 --- a/server/channels/app/migrations.go +++ b/server/channels/app/migrations.go @@ -17,15 +17,30 @@ import ( "github.com/mattermost/mattermost/server/v8/channels/store" ) -const EmojisPermissionsMigrationKey = "EmojisPermissionsMigrationComplete" -const GuestRolesCreationMigrationKey = "GuestRolesCreationMigrationComplete" -const SystemConsoleRolesCreationMigrationKey = "SystemConsoleRolesCreationMigrationComplete" -const CustomGroupAdminRoleCreationMigrationKey = "CustomGroupAdminRoleCreationMigrationComplete" -const ContentExtractionConfigDefaultTrueMigrationKey = "ContentExtractionConfigDefaultTrueMigrationComplete" -const PlaybookRolesCreationMigrationKey = "PlaybookRolesCreationMigrationComplete" -const FirstAdminSetupCompleteKey = model.SystemFirstAdminSetupComplete -const remainingSchemaMigrationsKey = "RemainingSchemaMigrations" -const postPriorityConfigDefaultTrueMigrationKey = "PostPriorityConfigDefaultTrueMigrationComplete" +const ( + EmojisPermissionsMigrationKey = "EmojisPermissionsMigrationComplete" + GuestRolesCreationMigrationKey = "GuestRolesCreationMigrationComplete" + SystemConsoleRolesCreationMigrationKey = "SystemConsoleRolesCreationMigrationComplete" + CustomGroupAdminRoleCreationMigrationKey = "CustomGroupAdminRoleCreationMigrationComplete" + ContentExtractionConfigDefaultTrueMigrationKey = "ContentExtractionConfigDefaultTrueMigrationComplete" + PlaybookRolesCreationMigrationKey = "PlaybookRolesCreationMigrationComplete" + FirstAdminSetupCompleteKey = model.SystemFirstAdminSetupComplete + remainingSchemaMigrationsKey = "RemainingSchemaMigrations" + postPriorityConfigDefaultTrueMigrationKey = "PostPriorityConfigDefaultTrueMigrationComplete" + contentFlaggingSetupDoneKey = "content_flagging_setup_done" + contentFlaggingMigrationVersion = "v1" + + contentFlaggingPropertyNameFlaggedPostId = "flagged_post_id" + contentFlaggingPropertyNameStatus = "status" + contentFlaggingPropertyNameReportingUserID = "reporting_user_id" + contentFlaggingPropertyNameReportingReason = "reporting_reason" + contentFlaggingPropertyNameReportingComment = "reporting_comment" + contentFlaggingPropertyNameReportingTime = "reporting_time" + contentFlaggingPropertyNameReviewerUserID = "reviewer_user_id" + contentFlaggingPropertyNameActorUserID = "actor_user_id" + contentFlaggingPropertyNameActorComment = "actor_comment" + contentFlaggingPropertyNameActionTime = "action_time" +) // This function migrates the default built in roles from code/config to the database. func (a *App) DoAdvancedPermissionsMigration() error { @@ -582,6 +597,125 @@ func (s *Server) doPostPriorityConfigDefaultTrueMigration() error { return nil } +func (s *Server) doSetupContentFlaggingProperties() error { + // This migration is designed in a way to allow adding more properties in the future. + // When a new property needs to be added, add it to the expectedPropertiesMap map and + // update the contentFlaggingMigrationVersion to a new value.. + + // If the migration is already marked as completed, don't do it again. + var nfErr *store.ErrNotFound + data, err := s.Store().System().GetByName(contentFlaggingSetupDoneKey) + if err != nil && !errors.As(err, &nfErr) { + return fmt.Errorf("could not query migration: %w", err) + } + + if data != nil && data.Value == contentFlaggingMigrationVersion { + return nil + } + + // RegisterPropertyGroup is idempotent, so no need to check if group is already registered + group, err := s.propertyService.RegisterPropertyGroup(model.ContentFlaggingGroupName) + if err != nil { + return fmt.Errorf("failed to register Content Flagging group: %w", err) + } + + // Using page size of 100 and not iterating through all pages because the + // number of fields are static and defined here and not expected to be more than 100 for now. + existingProperties, appErr := s.propertyService.SearchPropertyFields(group.ID, "", model.PropertyFieldSearchOpts{PerPage: 100}) + if appErr != nil { + return fmt.Errorf("failed to search for existing content flagging properties: %w", appErr) + } + + existingPropertiesMap := map[string]*model.PropertyField{} + for _, property := range existingProperties { + existingPropertiesMap[property.Name] = property + } + + expectedPropertiesMap := map[string]*model.PropertyField{ + contentFlaggingPropertyNameFlaggedPostId: { + GroupID: group.ID, + Name: contentFlaggingPropertyNameFlaggedPostId, + Type: model.PropertyFieldTypeText, + }, + contentFlaggingPropertyNameStatus: { + GroupID: group.ID, + Name: contentFlaggingPropertyNameStatus, + Type: model.PropertyFieldTypeText, + }, + contentFlaggingPropertyNameReportingUserID: { + GroupID: group.ID, + Name: contentFlaggingPropertyNameReportingUserID, + Type: model.PropertyFieldTypeUser, + }, + contentFlaggingPropertyNameReportingReason: { + GroupID: group.ID, + Name: contentFlaggingPropertyNameReportingReason, + Type: model.PropertyFieldTypeText, + }, + contentFlaggingPropertyNameReportingComment: { + GroupID: group.ID, + Name: contentFlaggingPropertyNameReportingComment, + Type: model.PropertyFieldTypeText, + }, + contentFlaggingPropertyNameReportingTime: { + GroupID: group.ID, + Name: contentFlaggingPropertyNameReportingTime, + Type: model.PropertyFieldTypeText, + }, + contentFlaggingPropertyNameReviewerUserID: { + GroupID: group.ID, + Name: contentFlaggingPropertyNameReviewerUserID, + Type: model.PropertyFieldTypeUser, + }, + contentFlaggingPropertyNameActorUserID: { + GroupID: group.ID, + Name: contentFlaggingPropertyNameActorUserID, + Type: model.PropertyFieldTypeUser, + }, + contentFlaggingPropertyNameActorComment: { + GroupID: group.ID, + Name: contentFlaggingPropertyNameActorComment, + Type: model.PropertyFieldTypeText, + }, + contentFlaggingPropertyNameActionTime: { + GroupID: group.ID, + Name: contentFlaggingPropertyNameActionTime, + Type: model.PropertyFieldTypeText, + }, + } + + var propertiesToUpdate []*model.PropertyField + var propertiesToCreate []*model.PropertyField + + for name, expectedProperty := range expectedPropertiesMap { + if _, exists := existingPropertiesMap[name]; exists { + property := existingPropertiesMap[name] + property.Type = expectedProperty.Type + propertiesToUpdate = append(propertiesToUpdate, property) + } else { + propertiesToCreate = append(propertiesToCreate, expectedProperty) + } + } + + for _, property := range propertiesToCreate { + if _, err := s.propertyService.CreatePropertyField(property); err != nil { + return fmt.Errorf("failed to create content flagging property: %q, error: %w", property.Name, err) + } + } + + if len(propertiesToUpdate) > 0 { + if _, err := s.propertyService.UpdatePropertyFields(group.ID, propertiesToUpdate); err != nil { + return fmt.Errorf("failed to update content flagging property fields: %w", err) + } + } + + if err := s.Store().System().SaveOrUpdate(&model.System{Name: contentFlaggingSetupDoneKey, Value: contentFlaggingMigrationVersion}); err != nil { + return fmt.Errorf("failed to save content flagging setup done flag in system store %w", err) + } + + return nil +} + func (s *Server) doCloudS3PathMigrations(c request.CTX) error { // This migration is only applicable for cloud environments if os.Getenv("MM_CLOUD_FILESTORE_BIFROST") == "" { @@ -695,6 +829,7 @@ func (s *Server) doAppMigrations() { {"First Admin Setup Complete Migration", s.doFirstAdminSetupCompleteMigration}, {"Remaining Schema Migrations", s.doRemainingSchemaMigrations}, {"Post Priority Config Default True Migration", s.doPostPriorityConfigDefaultTrueMigration}, + {"Content Flagging Properties Setup", s.doSetupContentFlaggingProperties}, } for i := range m1 { diff --git a/server/channels/app/migrations_test.go b/server/channels/app/migrations_test.go new file mode 100644 index 00000000000..19510f7c9e3 --- /dev/null +++ b/server/channels/app/migrations_test.go @@ -0,0 +1,52 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package app + +import ( + "testing" + + "github.com/mattermost/mattermost/server/public/model" + "github.com/stretchr/testify/require" +) + +func TestDoSetupContentFlaggingProperties(t *testing.T) { + t.Run("should register property group and fields", func(t *testing.T) { + //we need to call the Setup method and run the full setup instead of + //just creating a new server via NewServer() because the Setup method + //also care of using the correct database DSN based on environment, + //setting up the store and initializing services used in store such as property services. + th := Setup(t) + defer th.TearDown() + + group, err := th.Server.propertyService.GetPropertyGroup(model.ContentFlaggingGroupName) + require.NoError(t, err) + require.NotNil(t, group) + require.Equal(t, model.ContentFlaggingGroupName, group.Name) + + propertyFields, err := th.Server.propertyService.SearchPropertyFields(group.ID, "", model.PropertyFieldSearchOpts{PerPage: 100}) + require.NoError(t, err) + require.Len(t, propertyFields, 10) + }) + + t.Run("the migration is idempotent", func(t *testing.T) { + th := Setup(t) + defer th.TearDown() + + // Now we will remove the migration done key from systems table to allow the data migration to run again + _, err := th.Store.System().PermanentDeleteByName(contentFlaggingSetupDoneKey) + require.NoError(t, err) + + // Run the content flagging data migration again + err = th.Server.doSetupContentFlaggingProperties() + require.NoError(t, err) + + group, err := th.Server.propertyService.GetPropertyGroup(model.ContentFlaggingGroupName) + require.NoError(t, err) + require.Equal(t, model.ContentFlaggingGroupName, group.Name) + + propertyFields, err := th.Server.propertyService.SearchPropertyFields(group.ID, "", model.PropertyFieldSearchOpts{PerPage: 100}) + require.NoError(t, err) + require.Len(t, propertyFields, 10) + }) +} diff --git a/server/channels/testlib/store.go b/server/channels/testlib/store.go index f398333df32..12de139bfee 100644 --- a/server/channels/testlib/store.go +++ b/server/channels/testlib/store.go @@ -39,6 +39,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store { systemStore.On("GetByName", "SystemConsoleRolesCreationMigrationComplete").Return(&model.System{Name: "SystemConsoleRolesCreationMigrationComplete", Value: "true"}, nil) systemStore.On("GetByName", "PlaybookRolesCreationMigrationComplete").Return(&model.System{Name: "PlaybookRolesCreationMigrationComplete", Value: "true"}, nil) systemStore.On("GetByName", "PostPriorityConfigDefaultTrueMigrationComplete").Return(&model.System{Name: "PostPriorityConfigDefaultTrueMigrationComplete", Value: "true"}, nil) + systemStore.On("GetByName", "content_flagging_setup_done").Return(&model.System{Name: "content_flagging_setup_done", Value: "true"}, nil) systemStore.On("GetByName", model.MigrationKeyEmojiPermissionsSplit).Return(&model.System{Name: model.MigrationKeyEmojiPermissionsSplit, Value: "true"}, nil) systemStore.On("GetByName", model.MigrationKeyWebhookPermissionsSplit).Return(&model.System{Name: model.MigrationKeyWebhookPermissionsSplit, Value: "true"}, nil) systemStore.On("GetByName", model.MigrationKeyListJoinPublicPrivateTeams).Return(&model.System{Name: model.MigrationKeyListJoinPublicPrivateTeams, Value: "true"}, nil) @@ -91,6 +92,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store { systemStore.On("InsertIfExists", mock.AnythingOfType("*model.System")).Return(&model.System{}, nil).Once() systemStore.On("Save", mock.AnythingOfType("*model.System")).Return(nil) + systemStore.On("SaveOrUpdate", mock.AnythingOfType("*model.System")).Return(nil) userStore := mocks.UserStore{} userStore.On("Count", mock.AnythingOfType("model.UserCountOptions")).Return(int64(1), nil) @@ -125,6 +127,12 @@ func GetMockStoreForSetupFunctions() *mocks.Store { propertyFieldStore := mocks.PropertyFieldStore{} propertyValueStore := mocks.PropertyValueStore{} + propertyGroupStore.On("Register", model.ContentFlaggingGroupName).Return(&model.PropertyGroup{ID: model.NewId(), Name: model.ContentFlaggingGroupName}, nil) + + propertyFieldStore.On("SearchPropertyFields", mock.Anything).Return([]*model.PropertyField{}, nil) + propertyFieldStore.On("CreatePropertyField", mock.Anything).Return(&model.PropertyField{}, nil) + propertyFieldStore.On("Create", mock.AnythingOfType("*model.PropertyField")).Return(&model.PropertyField{}, nil) + mockStore.On("System").Return(&systemStore) mockStore.On("User").Return(&userStore) mockStore.On("Post").Return(&postStore) diff --git a/server/public/model/content_flagging.go b/server/public/model/content_flagging.go new file mode 100644 index 00000000000..542029ec869 --- /dev/null +++ b/server/public/model/content_flagging.go @@ -0,0 +1,6 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +const ContentFlaggingGroupName = "content_flagging"