// Vikunja is a to-do list application to facilitate your life. // Copyright 2018-2020 Vikunja and contributors. All rights reserved. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . package models import ( "code.vikunja.io/api/pkg/files" "code.vikunja.io/api/pkg/utils" "code.vikunja.io/web" ) // ListDuplicate holds everything needed to duplicate a list type ListDuplicate struct { // The list id of the list to duplicate ListID int64 `json:"-" param:"listid"` // The target namespace ID NamespaceID int64 `json:"namespace_id,omitempty"` // The copied list List *List `json:",omitempty"` web.Rights `json:"-"` web.CRUDable `json:"-"` } // CanCreate checks if a user has the right to duplicate a list func (ld *ListDuplicate) CanCreate(a web.Auth) (canCreate bool, err error) { // List Exists + user has read access to list ld.List = &List{ID: ld.ListID} canRead, err := ld.List.CanRead(a) if err != nil || !canRead { return canRead, err } // Namespace exists + user has write access to is (-> can create new lists) ld.List.NamespaceID = ld.NamespaceID return ld.List.CanCreate(a) } // Create duplicates a list // @Summary Duplicate an existing list // @Description Copies the list, tasks, files, kanban data, assignees, comments, attachments, lables, relations, backgrounds, user/team rights and link shares from one list to a new namespace. The user needs read access in the list and write access in the namespace of the new list. // @tags list // @Accept json // @Produce json // @Security JWTKeyAuth // @Param listID path int true "The list ID to duplicate" // @Param list body models.ListDuplicate true "The target namespace which should hold the copied list." // @Success 200 {object} models.ListDuplicate "The created list." // @Failure 400 {object} web.HTTPError "Invalid list duplicate object provided." // @Failure 403 {object} web.HTTPError "The user does not have access to the list or namespace" // @Failure 500 {object} models.Message "Internal error" // @Router /lists/{listID}/duplicate [put] func (ld *ListDuplicate) Create(a web.Auth) (err error) { ld.List.ID = 0 ld.List.Identifier = "" // Reset the identifier to trigger regenerating a new one // Set the owner to the current user ld.List.OwnerID = a.GetID() if err := CreateOrUpdateList(ld.List); err != nil { return err } // 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 = x.Where("list_id = ?", ld.ListID).Find(&buckets) if err != nil { return } for _, b := range buckets { oldID := b.ID b.ID = 0 b.ListID = ld.List.ID if err := b.Create(a); err != nil { return err } bucketMap[oldID] = b.ID } // Get all tasks + all task details tasks, _, _, err := getTasksForLists([]*List{{ID: ld.ListID}}, &taskOptions{}) if err != nil { return err } taskMap := make(map[int64]int64) // Create + update all tasks (includes reminders) oldTaskIDs := make([]int64, len(tasks)) for _, t := range tasks { oldID := t.ID t.ID = 0 t.ListID = ld.List.ID t.BucketID = bucketMap[t.BucketID] t.UID = "" err := createTask(t, a, false) if err != nil { return err } taskMap[oldID] = t.ID oldTaskIDs = append(oldTaskIDs, oldID) } // Save all attachments // We also duplicate all underlying files since they could be modified in one list which would result in // file changes in the other list which is not something we want. attachments, err := getTaskAttachmentsByTaskIDs(oldTaskIDs) if err != nil { return err } for _, attachment := range attachments { attachment.ID = 0 attachment.TaskID = oldTaskIDs[attachment.TaskID] attachment.File = &files.File{ID: attachment.FileID} if err := attachment.File.LoadFileMetaByID(); err != nil { if files.IsErrFileDoesNotExist(err) { continue } return err } if err := attachment.File.LoadFileByID(); err != nil { return err } err := attachment.NewAttachment(attachment.File.File, attachment.File.Name, attachment.File.Size, a) if err != nil { return err } if attachment.File.File != nil { _ = attachment.File.File.Close() } } // Copy label tasks (not the labels) labelTasks := []*LabelTask{} err = x.In("task_id", oldTaskIDs).Find(&labelTasks) if err != nil { return } for _, lt := range labelTasks { lt.ID = 0 lt.TaskID = taskMap[lt.TaskID] if _, err := x.Insert(lt); err != nil { return err } } // Assignees // Only copy those assignees who have access to the task assignees := []*TaskAssginee{} err = x.In("task_id", oldTaskIDs).Find(&assignees) if err != nil { return } for _, a := range assignees { t := &Task{ ID: taskMap[a.TaskID], ListID: ld.List.ID, } if err := t.addNewAssigneeByID(a.UserID, ld.List); err != nil { if IsErrUserDoesNotHaveAccessToList(err) { continue } return err } } // Comments comments := []*TaskComment{} err = x.In("task_id", oldTaskIDs).Find(&comments) if err != nil { return } for _, c := range comments { c.ID = 0 c.TaskID = taskMap[c.TaskID] if _, err := x.Insert(c); err != nil { return err } } // Relations in that list // Low-Effort: Only copy those relations which are between tasks in the same list // because we can do that without a lot of hassle relations := []*TaskRelation{} err = x.In("task_id", oldTaskIDs).Find(&relations) if err != nil { return } for _, r := range relations { otherTaskID, exists := taskMap[r.OtherTaskID] if !exists { continue } r.ID = 0 r.OtherTaskID = otherTaskID r.TaskID = taskMap[r.TaskID] if _, err := x.Insert(r); err != nil { return err } } // Background files + unsplash info if ld.List.BackgroundFileID != 0 { f := &files.File{ID: ld.List.BackgroundFileID} if err := f.LoadFileMetaByID(); err != nil { return err } if err := f.LoadFileByID(); err != nil { return err } defer f.File.Close() file, err := files.Create(f.File, f.Name, f.Size, a) if err != nil { return err } // Get unsplash info if applicable up, err := GetUnsplashPhotoByFileID(ld.List.BackgroundFileID) if err != nil && files.IsErrFileIsNotUnsplashFile(err) { return err } if up != nil { up.ID = 0 up.FileID = file.ID if err := up.Save(); err != nil { return err } } ld.List.BackgroundFileID = file.ID if err := CreateOrUpdateList(ld.List); err != nil { return err } } // Rights / Shares // To keep it simple(r) we will only copy rights which are directly used with the list, no namespace changes. users := []*ListUser{} err = x.Where("list_id = ?", ld.ListID).Find(&users) if err != nil { return } for _, u := range users { u.ID = 0 u.ListID = ld.List.ID if _, err := x.Insert(u); err != nil { return err } } teams := []*TeamList{} err = x.Where("list_id = ?", ld.ListID).Find(&teams) if err != nil { return } for _, t := range teams { t.ID = 0 t.ListID = ld.List.ID if _, err := x.Insert(t); err != nil { return err } } // Generate new link shares if any are available linkShares := []*LinkSharing{} err = x.Where("list_id = ?", ld.ListID).Find(&linkShares) if err != nil { return } for _, share := range linkShares { share.ID = 0 share.ListID = ld.List.ID share.Hash = utils.MakeRandomString(40) if _, err := x.Insert(share); err != nil { return err } } return }