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
Signed by: konrad
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
}
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)
if err != nil {
return err
@ -775,9 +775,11 @@ func CreateProject(s *xorm.Session, project *Project, auth web.Auth, createBackl
}
}
err = CreateDefaultViewsForProject(s, project, auth, createBacklogBucket)
if err != nil {
return
if createDefaultViews {
err = CreateDefaultViewsForProject(s, project, auth, createBacklogBucket)
if err != nil {
return
}
}
return events.Dispatch(&ProjectCreatedEvent{
@ -987,7 +989,7 @@ func updateProjectByTaskID(s *xorm.Session, taskID int64) (err error) {
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects [put]
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 {
return
}

View File

@ -81,7 +81,8 @@ func (pd *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
pd.Project.ParentProjectID = pd.ParentProjectID
// Set the owner to the current user
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 IsErrProjectIdentifierIsNotUnique(err) {
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)
// Duplicate kanban buckets
// 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)
newTaskIDs, err := duplicateTasks(s, doer, pd)
if err != nil {
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 {
return
}
log.Debugf("Duplicated all views, buckets and positions from project %d into %d", pd.ProjectID, pd.Project.ID)
err = duplicateProjectBackground(s, pd, doer)
if err != nil {
return
@ -173,6 +162,93 @@ func (pd *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
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) {
if pd.Project.BackgroundFileID == 0 {
return
@ -221,33 +297,32 @@ func duplicateProjectBackground(s *xorm.Session, pd *ProjectDuplicate, doer web.
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
tasks, _, _, err := getTasksForProjects(s, []*Project{{ID: ld.ProjectID}}, doer, &taskSearchOptions{}, nil)
if err != nil {
return err
return nil, err
}
if len(tasks) == 0 {
return nil
return
}
// 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.
taskMap := make(map[int64]int64)
newTaskIDs = make(map[int64]int64, len(tasks))
// Create + update all tasks (includes reminders)
oldTaskIDs := make([]int64, 0, len(tasks))
for _, t := range tasks {
oldID := t.ID
t.ID = 0
t.ProjectID = ld.Project.ID
t.BucketID = bucketMap[t.BucketID]
t.UID = ""
err := createTask(s, t, doer, false)
err = createTask(s, t, doer, false, false)
if err != nil {
return err
return nil, err
}
taskMap[oldID] = t.ID
newTaskIDs[oldID] = t.ID
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.
attachments, err := getTaskAttachmentsByTaskIDs(s, oldTaskIDs)
if err != nil {
return err
return nil, err
}
for _, attachment := range attachments {
oldAttachmentID := attachment.ID
attachment.ID = 0
var exists bool
attachment.TaskID, exists = taskMap[attachment.TaskID]
attachment.TaskID, exists = newTaskIDs[attachment.TaskID]
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)
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)
continue
}
return err
return nil, err
}
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)
if err != nil {
return err
return nil, err
}
if attachment.File.File != nil {
@ -305,9 +380,9 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate, bucket
for _, lt := range labelTasks {
lt.ID = 0
lt.TaskID = taskMap[lt.TaskID]
lt.TaskID = newTaskIDs[lt.TaskID]
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 {
t := &Task{
ID: taskMap[a.TaskID],
ID: newTaskIDs[a.TaskID],
ProjectID: ld.Project.ID,
}
if err := t.addNewAssigneeByID(s, a.UserID, ld.Project, doer); err != nil {
if IsErrUserDoesNotHaveAccessToProject(err) {
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 {
c.ID = 0
c.TaskID = taskMap[c.TaskID]
c.TaskID = newTaskIDs[c.TaskID]
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
}
for _, r := range relations {
otherTaskID, exists := taskMap[r.OtherTaskID]
otherTaskID, exists := newTaskIDs[r.OtherTaskID]
if !exists {
continue
}
r.ID = 0
r.OtherTaskID = otherTaskID
r.TaskID = taskMap[r.TaskID]
r.TaskID = newTaskIDs[r.TaskID]
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)
return nil
return
}

View File

@ -48,11 +48,11 @@ func TestProjectDuplicate(t *testing.T) {
require.NoError(t, err)
// 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)
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)
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.
// 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"
// @Router /projects/{id}/tasks [put]
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
@ -827,10 +827,12 @@ func createTask(s *xorm.Session, t *Task, a web.Auth, updateAssignees bool) (err
for _, view := range views {
// Get the default bucket and move the task there
err = setTaskBucket(s, t, nil, view, 0)
if err != nil {
return
if updateBucket {
// Get the default bucket and move the task there
err = setTaskBucket(s, t, nil, view, t.BucketID)
if err != nil {
return
}
}
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 err != nil {
return
if updateBucket {
_, err = s.Insert(&positions)
if err != nil {
return
}
}
t.CreatedBy = createdBy