1
0

fix(views): duplicate all views and related entities when duplicating a project

This commit is contained in:
kolaente 2024-03-18 23:08:14 +01:00
parent 9cc273d9bd
commit d4bdd2d4e8
No known key found for this signature in database
GPG Key ID: F40E70337AB24C9B
4 changed files with 139 additions and 58 deletions

View File

@ -738,7 +738,7 @@ func checkProjectBeforeUpdateOrDelete(s *xorm.Session, project *Project) (err er
return nil return nil
} }
func CreateProject(s *xorm.Session, project *Project, auth web.Auth, createBacklogBucket bool) (err error) { func CreateProject(s *xorm.Session, project *Project, auth web.Auth, createBacklogBucket bool, createDefaultViews bool) (err error) {
err = project.CheckIsArchived(s) err = project.CheckIsArchived(s)
if err != nil { if err != nil {
return err return err
@ -775,9 +775,11 @@ func CreateProject(s *xorm.Session, project *Project, auth web.Auth, createBackl
} }
} }
err = CreateDefaultViewsForProject(s, project, auth, createBacklogBucket) if createDefaultViews {
if err != nil { err = CreateDefaultViewsForProject(s, project, auth, createBacklogBucket)
return if err != nil {
return
}
} }
return events.Dispatch(&ProjectCreatedEvent{ return events.Dispatch(&ProjectCreatedEvent{
@ -987,7 +989,7 @@ func updateProjectByTaskID(s *xorm.Session, taskID int64) (err error) {
// @Failure 500 {object} models.Message "Internal error" // @Failure 500 {object} models.Message "Internal error"
// @Router /projects [put] // @Router /projects [put]
func (p *Project) Create(s *xorm.Session, a web.Auth) (err error) { func (p *Project) Create(s *xorm.Session, a web.Auth) (err error) {
err = CreateProject(s, p, a, true) err = CreateProject(s, p, a, true, true)
if err != nil { if err != nil {
return return
} }

View File

@ -81,7 +81,8 @@ func (pd *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
pd.Project.ParentProjectID = pd.ParentProjectID pd.Project.ParentProjectID = pd.ParentProjectID
// Set the owner to the current user // Set the owner to the current user
pd.Project.OwnerID = doer.GetID() pd.Project.OwnerID = doer.GetID()
if err := CreateProject(s, pd.Project, doer, false); err != nil { err = CreateProject(s, pd.Project, doer, false, false)
if err != nil {
// If there is no available unique project identifier, just reset it. // If there is no available unique project identifier, just reset it.
if IsErrProjectIdentifierIsNotUnique(err) { if IsErrProjectIdentifierIsNotUnique(err) {
pd.Project.Identifier = "" pd.Project.Identifier = ""
@ -92,32 +93,20 @@ func (pd *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
log.Debugf("Duplicated project %d into new project %d", pd.ProjectID, pd.Project.ID) log.Debugf("Duplicated project %d into new project %d", pd.ProjectID, pd.Project.ID)
// Duplicate kanban buckets newTaskIDs, err := duplicateTasks(s, doer, pd)
// Old bucket ID as key, new id as value
// Used to map the newly created tasks to their new buckets
bucketMap := make(map[int64]int64)
buckets := []*Bucket{}
err = s.Where("project_id = ?", pd.ProjectID).Find(&buckets)
if err != nil { if err != nil {
return return
} }
for _, b := range buckets {
oldID := b.ID
b.ID = 0
b.ProjectID = pd.Project.ID
if err := b.Create(s, doer); err != nil {
return err
}
bucketMap[oldID] = b.ID
}
log.Debugf("Duplicated all buckets from project %d into %d", pd.ProjectID, pd.Project.ID) log.Debugf("Duplicated all tasks from project %d into %d", pd.ProjectID, pd.Project.ID)
err = duplicateTasks(s, doer, pd, bucketMap) err = duplicateViews(s, pd, doer, newTaskIDs)
if err != nil { if err != nil {
return return
} }
log.Debugf("Duplicated all views, buckets and positions from project %d into %d", pd.ProjectID, pd.Project.ID)
err = duplicateProjectBackground(s, pd, doer) err = duplicateProjectBackground(s, pd, doer)
if err != nil { if err != nil {
return return
@ -173,6 +162,93 @@ func (pd *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
return return
} }
func duplicateViews(s *xorm.Session, pd *ProjectDuplicate, doer web.Auth, taskMap map[int64]int64) (err error) {
// Duplicate Views
views := make(map[int64]*ProjectView)
err = s.Where("project_id = ?", pd.ProjectID).Find(&views)
if err != nil {
return
}
oldViewIDs := []int64{}
viewMap := make(map[int64]int64)
for _, view := range views {
oldID := view.ID
oldViewIDs = append(oldViewIDs, oldID)
view.ID = 0
view.ProjectID = pd.Project.ID
err = view.Create(s, doer)
if err != nil {
return
}
viewMap[oldID] = view.ID
}
buckets := []*Bucket{}
err = s.In("project_view_id", oldViewIDs).Find(&buckets)
if err != nil {
return
}
// Old bucket ID as key, new id as value
// Used to map the newly created tasks to their new buckets
bucketMap := make(map[int64]int64)
oldBucketIDs := []int64{}
for _, b := range buckets {
oldID := b.ID
oldBucketIDs = append(oldBucketIDs, oldID)
b.ID = 0
b.ProjectID = pd.Project.ID
err = b.Create(s, doer)
if err != nil {
return err
}
bucketMap[oldID] = b.ID
}
oldTaskBuckets := []*TaskBucket{}
err = s.In("bucket_id", oldBucketIDs).Find(&oldTaskBuckets)
if err != nil {
return err
}
taskBuckets := []*TaskBucket{}
for _, tb := range oldTaskBuckets {
taskBuckets = append(taskBuckets, &TaskBucket{
BucketID: bucketMap[tb.BucketID],
TaskID: taskMap[tb.TaskID],
})
}
_, err = s.Insert(&taskBuckets)
if err != nil {
return err
}
oldTaskPositions := []*TaskPosition{}
err = s.In("project_view_id", oldViewIDs).Find(&oldTaskPositions)
if err != nil {
return
}
taskPositions := []*TaskPosition{}
for _, tp := range oldTaskPositions {
taskPositions = append(taskPositions, &TaskPosition{
ProjectViewID: viewMap[tp.ProjectViewID],
TaskID: taskMap[tp.TaskID],
Position: tp.Position,
})
}
return
}
func duplicateProjectBackground(s *xorm.Session, pd *ProjectDuplicate, doer web.Auth) (err error) { func duplicateProjectBackground(s *xorm.Session, pd *ProjectDuplicate, doer web.Auth) (err error) {
if pd.Project.BackgroundFileID == 0 { if pd.Project.BackgroundFileID == 0 {
return return
@ -221,33 +297,32 @@ func duplicateProjectBackground(s *xorm.Session, pd *ProjectDuplicate, doer web.
return return
} }
func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate, bucketMap map[int64]int64) (err error) { func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate) (newTaskIDs map[int64]int64, err error) {
// Get all tasks + all task details // Get all tasks + all task details
tasks, _, _, err := getTasksForProjects(s, []*Project{{ID: ld.ProjectID}}, doer, &taskSearchOptions{}, nil) tasks, _, _, err := getTasksForProjects(s, []*Project{{ID: ld.ProjectID}}, doer, &taskSearchOptions{}, nil)
if err != nil { if err != nil {
return err return nil, err
} }
if len(tasks) == 0 { if len(tasks) == 0 {
return nil return
} }
// This map contains the old task id as key and the new duplicated task id as value. // This map contains the old task id as key and the new duplicated task id as value.
// It is used to map old task items to new ones. // It is used to map old task items to new ones.
taskMap := make(map[int64]int64) newTaskIDs = make(map[int64]int64, len(tasks))
// Create + update all tasks (includes reminders) // Create + update all tasks (includes reminders)
oldTaskIDs := make([]int64, 0, len(tasks)) oldTaskIDs := make([]int64, 0, len(tasks))
for _, t := range tasks { for _, t := range tasks {
oldID := t.ID oldID := t.ID
t.ID = 0 t.ID = 0
t.ProjectID = ld.Project.ID t.ProjectID = ld.Project.ID
t.BucketID = bucketMap[t.BucketID]
t.UID = "" t.UID = ""
err := createTask(s, t, doer, false) err = createTask(s, t, doer, false, false)
if err != nil { if err != nil {
return err return nil, err
} }
taskMap[oldID] = t.ID newTaskIDs[oldID] = t.ID
oldTaskIDs = append(oldTaskIDs, oldID) oldTaskIDs = append(oldTaskIDs, oldID)
} }
@ -258,14 +333,14 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate, bucket
// file changes in the other project which is not something we want. // file changes in the other project which is not something we want.
attachments, err := getTaskAttachmentsByTaskIDs(s, oldTaskIDs) attachments, err := getTaskAttachmentsByTaskIDs(s, oldTaskIDs)
if err != nil { if err != nil {
return err return nil, err
} }
for _, attachment := range attachments { for _, attachment := range attachments {
oldAttachmentID := attachment.ID oldAttachmentID := attachment.ID
attachment.ID = 0 attachment.ID = 0
var exists bool var exists bool
attachment.TaskID, exists = taskMap[attachment.TaskID] attachment.TaskID, exists = newTaskIDs[attachment.TaskID]
if !exists { if !exists {
log.Debugf("Error duplicating attachment %d from old task %d to new task: Old task <-> new task does not seem to exist.", oldAttachmentID, attachment.TaskID) log.Debugf("Error duplicating attachment %d from old task %d to new task: Old task <-> new task does not seem to exist.", oldAttachmentID, attachment.TaskID)
continue continue
@ -276,15 +351,15 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate, bucket
log.Debugf("Not duplicating attachment %d (file %d) because it does not exist from project %d into %d", oldAttachmentID, attachment.FileID, ld.ProjectID, ld.Project.ID) log.Debugf("Not duplicating attachment %d (file %d) because it does not exist from project %d into %d", oldAttachmentID, attachment.FileID, ld.ProjectID, ld.Project.ID)
continue continue
} }
return err return nil, err
} }
if err := attachment.File.LoadFileByID(); err != nil { if err := attachment.File.LoadFileByID(); err != nil {
return err return nil, err
} }
err := attachment.NewAttachment(s, attachment.File.File, attachment.File.Name, attachment.File.Size, doer) err := attachment.NewAttachment(s, attachment.File.File, attachment.File.Name, attachment.File.Size, doer)
if err != nil { if err != nil {
return err return nil, err
} }
if attachment.File.File != nil { if attachment.File.File != nil {
@ -305,9 +380,9 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate, bucket
for _, lt := range labelTasks { for _, lt := range labelTasks {
lt.ID = 0 lt.ID = 0
lt.TaskID = taskMap[lt.TaskID] lt.TaskID = newTaskIDs[lt.TaskID]
if _, err := s.Insert(lt); err != nil { if _, err := s.Insert(lt); err != nil {
return err return nil, err
} }
} }
@ -322,14 +397,14 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate, bucket
} }
for _, a := range assignees { for _, a := range assignees {
t := &Task{ t := &Task{
ID: taskMap[a.TaskID], ID: newTaskIDs[a.TaskID],
ProjectID: ld.Project.ID, ProjectID: ld.Project.ID,
} }
if err := t.addNewAssigneeByID(s, a.UserID, ld.Project, doer); err != nil { if err := t.addNewAssigneeByID(s, a.UserID, ld.Project, doer); err != nil {
if IsErrUserDoesNotHaveAccessToProject(err) { if IsErrUserDoesNotHaveAccessToProject(err) {
continue continue
} }
return err return nil, err
} }
} }
@ -343,9 +418,9 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate, bucket
} }
for _, c := range comments { for _, c := range comments {
c.ID = 0 c.ID = 0
c.TaskID = taskMap[c.TaskID] c.TaskID = newTaskIDs[c.TaskID]
if _, err := s.Insert(c); err != nil { if _, err := s.Insert(c); err != nil {
return err return nil, err
} }
} }
@ -360,19 +435,19 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate, bucket
return return
} }
for _, r := range relations { for _, r := range relations {
otherTaskID, exists := taskMap[r.OtherTaskID] otherTaskID, exists := newTaskIDs[r.OtherTaskID]
if !exists { if !exists {
continue continue
} }
r.ID = 0 r.ID = 0
r.OtherTaskID = otherTaskID r.OtherTaskID = otherTaskID
r.TaskID = taskMap[r.TaskID] r.TaskID = newTaskIDs[r.TaskID]
if _, err := s.Insert(r); err != nil { if _, err := s.Insert(r); err != nil {
return err return nil, err
} }
} }
log.Debugf("Duplicated all task relations from project %d into %d", ld.ProjectID, ld.Project.ID) log.Debugf("Duplicated all task relations from project %d into %d", ld.ProjectID, ld.Project.ID)
return nil return
} }

View File

@ -48,11 +48,11 @@ func TestProjectDuplicate(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// assert the new project has the same number of buckets as the old one // assert the new project has the same number of buckets as the old one
numberOfOriginalBuckets, err := s.Where("project_id = ?", l.ProjectID).Count(&Bucket{}) numberOfOriginalViews, err := s.Where("project_id = ?", l.ProjectID).Count(&ProjectView{})
require.NoError(t, err) require.NoError(t, err)
numberOfDuplicatedBuckets, err := s.Where("project_id = ?", l.Project.ID).Count(&Bucket{}) numberOfDuplicatedViews, err := s.Where("project_id = ?", l.Project.ID).Count(&ProjectView{})
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, numberOfOriginalBuckets, numberOfDuplicatedBuckets, "duplicated project does not have the same amount of buckets as the original one") assert.Equal(t, numberOfOriginalViews, numberOfDuplicatedViews, "duplicated project does not have the same amount of views as the original one")
// To make this test 100% useful, it would need to assert a lot more stuff, but it is good enough for now. // To make this test 100% useful, it would need to assert a lot more stuff, but it is good enough for now.
// Also, we're lacking utility functions to do all needed assertions. // Also, we're lacking utility functions to do all needed assertions.

View File

@ -776,10 +776,10 @@ func getNextTaskIndex(s *xorm.Session, projectID int64) (nextIndex int64, err er
// @Failure 500 {object} models.Message "Internal error" // @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id}/tasks [put] // @Router /projects/{id}/tasks [put]
func (t *Task) Create(s *xorm.Session, a web.Auth) (err error) { func (t *Task) Create(s *xorm.Session, a web.Auth) (err error) {
return createTask(s, t, a, true) return createTask(s, t, a, true, true)
} }
func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err error) { func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool, updateBucket bool) (err error) {
t.ID = 0 t.ID = 0
@ -827,10 +827,12 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
for _, view := range views { for _, view := range views {
// Get the default bucket and move the task there if updateBucket {
err = setTaskBucket(s, t, nil, view, 0) // Get the default bucket and move the task there
if err != nil { err = setTaskBucket(s, t, nil, view, t.BucketID)
return if err != nil {
return
}
} }
positions = append(positions, &TaskPosition{ positions = append(positions, &TaskPosition{
@ -840,9 +842,11 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
}) })
} }
_, err = s.Insert(&positions) if updateBucket {
if err != nil { _, err = s.Insert(&positions)
return if err != nil {
return
}
} }
t.CreatedBy = createdBy t.CreatedBy = createdBy