2020-06-30 20:53:14 +00:00
// Vikunja is a to-do list application to facilitate your life.
2023-09-01 06:32:28 +00:00
// Copyright 2018-present Vikunja and contributors. All rights reserved.
2020-06-30 20:53:14 +00:00
//
// This program is free software: you can redistribute it and/or modify
2020-12-23 15:41:52 +00:00
// it under the terms of the GNU Affero General Public Licensee as published by
2020-06-30 20:53:14 +00:00
// 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
2020-12-23 15:41:52 +00:00
// GNU Affero General Public Licensee for more details.
2020-06-30 20:53:14 +00:00
//
2020-12-23 15:41:52 +00:00
// You should have received a copy of the GNU Affero General Public Licensee
2020-06-30 20:53:14 +00:00
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"code.vikunja.io/api/pkg/files"
2020-07-17 11:26:49 +00:00
"code.vikunja.io/api/pkg/log"
2020-06-30 20:53:14 +00:00
"code.vikunja.io/api/pkg/utils"
"code.vikunja.io/web"
2020-12-23 15:32:28 +00:00
"xorm.io/xorm"
2020-06-30 20:53:14 +00:00
)
2022-11-13 16:07:01 +00:00
// ProjectDuplicate holds everything needed to duplicate a project
type ProjectDuplicate struct {
// The project id of the project to duplicate
ProjectID int64 ` json:"-" param:"projectid" `
2022-12-29 16:51:55 +00:00
// The target parent project
ParentProjectID int64 ` json:"parent_project_id,omitempty" `
2020-06-30 20:53:14 +00:00
2022-11-13 16:07:01 +00:00
// The copied project
2023-07-07 10:56:15 +00:00
Project * Project ` json:"duplicated_project,omitempty" `
2020-06-30 20:53:14 +00:00
web . Rights ` json:"-" `
web . CRUDable ` json:"-" `
}
2022-11-13 16:07:01 +00:00
// CanCreate checks if a user has the right to duplicate a project
2023-07-07 10:56:15 +00:00
func ( pd * ProjectDuplicate ) CanCreate ( s * xorm . Session , a web . Auth ) ( canCreate bool , err error ) {
2022-11-13 16:07:01 +00:00
// Project Exists + user has read access to project
2023-07-07 10:56:15 +00:00
pd . Project = & Project { ID : pd . ProjectID }
canRead , _ , err := pd . Project . CanRead ( s , a )
2020-06-30 20:53:14 +00:00
if err != nil || ! canRead {
return canRead , err
}
2023-07-07 10:56:15 +00:00
if pd . ParentProjectID == 0 { // no parent project
2022-12-29 16:51:55 +00:00
return canRead , err
}
// Parent project exists + user has write access to is (-> can create new projects)
2023-07-07 10:56:15 +00:00
parent := & Project { ID : pd . ParentProjectID }
2022-12-29 16:51:55 +00:00
return parent . CanCreate ( s , a )
2020-06-30 20:53:14 +00:00
}
2022-11-13 16:07:01 +00:00
// Create duplicates a project
// @Summary Duplicate an existing project
2022-12-29 16:51:55 +00:00
// @Description Copies the project, tasks, files, kanban data, assignees, comments, attachments, lables, relations, backgrounds, user/team rights and link shares from one project to a new one. The user needs read access in the project and write access in the parent of the new project.
2022-11-13 16:07:01 +00:00
// @tags project
2020-06-30 20:53:14 +00:00
// @Accept json
// @Produce json
// @Security JWTKeyAuth
2022-11-13 16:07:01 +00:00
// @Param projectID path int true "The project ID to duplicate"
2022-12-29 16:51:55 +00:00
// @Param project body models.ProjectDuplicate true "The target parent project which should hold the copied project."
2022-11-13 16:07:01 +00:00
// @Success 201 {object} models.ProjectDuplicate "The created project."
// @Failure 400 {object} web.HTTPError "Invalid project duplicate object provided."
2022-12-29 16:51:55 +00:00
// @Failure 403 {object} web.HTTPError "The user does not have access to the project or its parent."
2020-06-30 20:53:14 +00:00
// @Failure 500 {object} models.Message "Internal error"
2022-11-13 16:07:01 +00:00
// @Router /projects/{projectID}/duplicate [put]
2022-10-01 15:05:12 +00:00
//
2020-10-11 20:10:03 +00:00
//nolint:gocyclo
2023-07-07 10:56:15 +00:00
func ( pd * ProjectDuplicate ) Create ( s * xorm . Session , doer web . Auth ) ( err error ) {
2020-06-30 20:53:14 +00:00
2023-07-07 10:56:15 +00:00
log . Debugf ( "Duplicating project %d" , pd . ProjectID )
2020-07-17 11:26:49 +00:00
2023-07-07 10:56:15 +00:00
pd . Project . ID = 0
pd . Project . Identifier = "" // Reset the identifier to trigger regenerating a new one
pd . Project . ParentProjectID = pd . ParentProjectID
2020-06-30 20:53:14 +00:00
// Set the owner to the current user
2023-07-07 10:56:15 +00:00
pd . Project . OwnerID = doer . GetID ( )
2024-03-18 22:08:14 +00:00
err = CreateProject ( s , pd . Project , doer , false , false )
if err != nil {
2022-11-13 16:07:01 +00:00
// If there is no available unique project identifier, just reset it.
if IsErrProjectIdentifierIsNotUnique ( err ) {
2023-07-07 10:56:15 +00:00
pd . Project . Identifier = ""
2020-07-17 11:26:49 +00:00
} else {
return err
}
2020-06-30 20:53:14 +00:00
}
2023-07-07 10:56:15 +00:00
log . Debugf ( "Duplicated project %d into new project %d" , pd . ProjectID , pd . Project . ID )
2020-07-17 11:26:49 +00:00
2024-03-18 22:08:14 +00:00
newTaskIDs , err := duplicateTasks ( s , doer , pd )
2020-06-30 20:53:14 +00:00
if err != nil {
return
}
2024-03-18 22:08:14 +00:00
log . Debugf ( "Duplicated all tasks from project %d into %d" , pd . ProjectID , pd . Project . ID )
2020-07-17 11:26:49 +00:00
2024-03-18 22:08:14 +00:00
err = duplicateViews ( s , pd , doer , newTaskIDs )
2021-05-26 10:01:50 +00:00
if err != nil {
return
}
2024-03-18 22:08:14 +00:00
log . Debugf ( "Duplicated all views, buckets and positions from project %d into %d" , pd . ProjectID , pd . Project . ID )
2023-09-13 09:20:40 +00:00
err = duplicateProjectBackground ( s , pd , doer )
if err != nil {
return
2021-05-26 10:01:50 +00:00
}
// Rights / Shares
2022-12-29 16:51:55 +00:00
// To keep it simple(r) we will only copy rights which are directly used with the project, not the parent
2022-11-13 16:07:01 +00:00
users := [ ] * ProjectUser { }
2023-07-07 10:56:15 +00:00
err = s . Where ( "project_id = ?" , pd . ProjectID ) . Find ( & users )
2021-05-26 10:01:50 +00:00
if err != nil {
return
}
for _ , u := range users {
u . ID = 0
2023-07-07 10:56:15 +00:00
u . ProjectID = pd . Project . ID
2021-05-26 10:01:50 +00:00
if _ , err := s . Insert ( u ) ; err != nil {
return err
}
}
2023-07-07 10:56:15 +00:00
log . Debugf ( "Duplicated user shares from project %d into %d" , pd . ProjectID , pd . Project . ID )
2021-05-26 10:01:50 +00:00
2022-11-13 16:07:01 +00:00
teams := [ ] * TeamProject { }
2023-07-07 10:56:15 +00:00
err = s . Where ( "project_id = ?" , pd . ProjectID ) . Find ( & teams )
2021-05-26 10:01:50 +00:00
if err != nil {
return
}
for _ , t := range teams {
t . ID = 0
2023-07-07 10:56:15 +00:00
t . ProjectID = pd . Project . ID
2021-05-26 10:01:50 +00:00
if _ , err := s . Insert ( t ) ; err != nil {
return err
}
}
// Generate new link shares if any are available
linkShares := [ ] * LinkSharing { }
2023-07-07 10:56:15 +00:00
err = s . Where ( "project_id = ?" , pd . ProjectID ) . Find ( & linkShares )
2021-05-26 10:01:50 +00:00
if err != nil {
return
}
for _ , share := range linkShares {
share . ID = 0
2023-07-07 10:56:15 +00:00
share . ProjectID = pd . Project . ID
2021-05-26 10:01:50 +00:00
share . Hash = utils . MakeRandomString ( 40 )
if _ , err := s . Insert ( share ) ; err != nil {
return err
}
}
2023-07-07 10:56:15 +00:00
log . Debugf ( "Duplicated all link shares from project %d into %d" , pd . ProjectID , pd . Project . ID )
2021-05-26 10:01:50 +00:00
return
}
2024-03-18 22:08:14 +00:00
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 ] ,
} )
}
2024-04-13 20:36:41 +00:00
if len ( taskBuckets ) > 0 {
_ , err = s . Insert ( & taskBuckets )
if err != nil {
return err
}
2024-03-18 22:08:14 +00:00
}
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 ,
} )
}
2024-04-13 20:36:41 +00:00
if len ( taskPositions ) > 0 {
_ , err = s . Insert ( & taskPositions )
}
2024-03-18 22:08:14 +00:00
return
}
2023-09-13 09:20:40 +00:00
func duplicateProjectBackground ( s * xorm . Session , pd * ProjectDuplicate , doer web . Auth ) ( err error ) {
if pd . Project . BackgroundFileID == 0 {
return
}
log . Debugf ( "Duplicating background %d from project %d into %d" , pd . Project . BackgroundFileID , pd . ProjectID , pd . Project . ID )
f := & files . File { ID : pd . Project . BackgroundFileID }
err = f . LoadFileMetaByID ( )
if err != nil && files . IsErrFileDoesNotExist ( err ) {
pd . Project . BackgroundFileID = 0
return nil
}
if 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 , doer )
if err != nil {
return err
}
// Get unsplash info if applicable
up , err := GetUnsplashPhotoByFileID ( s , pd . Project . BackgroundFileID )
if err != nil && ! files . IsErrFileIsNotUnsplashFile ( err ) {
return err
}
if up != nil {
up . ID = 0
up . FileID = file . ID
if err := up . Save ( s ) ; err != nil {
return err
}
}
if err := SetProjectBackground ( s , pd . Project . ID , file , pd . Project . BackgroundBlurHash ) ; err != nil {
return err
}
log . Debugf ( "Duplicated project background from project %d into %d" , pd . ProjectID , pd . Project . ID )
return
}
2024-03-18 22:08:14 +00:00
func duplicateTasks ( s * xorm . Session , doer web . Auth , ld * ProjectDuplicate ) ( newTaskIDs map [ int64 ] int64 , err error ) {
2020-06-30 20:53:14 +00:00
// Get all tasks + all task details
2024-03-16 10:48:50 +00:00
tasks , _ , _ , err := getTasksForProjects ( s , [ ] * Project { { ID : ld . ProjectID } } , doer , & taskSearchOptions { } , nil )
2020-06-30 20:53:14 +00:00
if err != nil {
2024-03-18 22:08:14 +00:00
return nil , err
2020-06-30 20:53:14 +00:00
}
2021-05-26 10:01:50 +00:00
if len ( tasks ) == 0 {
2024-03-18 22:08:14 +00:00
return
2021-05-26 10:01:50 +00:00
}
2020-08-16 21:44:16 +00:00
// 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.
2024-03-18 22:08:14 +00:00
newTaskIDs = make ( map [ int64 ] int64 , len ( tasks ) )
2020-06-30 20:53:14 +00:00
// Create + update all tasks (includes reminders)
2022-03-27 14:55:37 +00:00
oldTaskIDs := make ( [ ] int64 , 0 , len ( tasks ) )
2020-06-30 20:53:14 +00:00
for _ , t := range tasks {
oldID := t . ID
t . ID = 0
2022-11-13 16:07:01 +00:00
t . ProjectID = ld . Project . ID
2020-06-30 20:53:14 +00:00
t . UID = ""
2024-03-18 22:08:14 +00:00
err = createTask ( s , t , doer , false , false )
2020-06-30 20:53:14 +00:00
if err != nil {
2024-03-18 22:08:14 +00:00
return nil , err
2020-06-30 20:53:14 +00:00
}
2024-03-18 22:08:14 +00:00
newTaskIDs [ oldID ] = t . ID
2020-06-30 20:53:14 +00:00
oldTaskIDs = append ( oldTaskIDs , oldID )
}
2022-11-13 16:07:01 +00:00
log . Debugf ( "Duplicated all tasks from project %d into %d" , ld . ProjectID , ld . Project . ID )
2020-07-17 11:26:49 +00:00
2020-06-30 20:53:14 +00:00
// Save all attachments
2022-11-13 16:07:01 +00:00
// We also duplicate all underlying files since they could be modified in one project which would result in
// file changes in the other project which is not something we want.
2020-12-23 15:32:28 +00:00
attachments , err := getTaskAttachmentsByTaskIDs ( s , oldTaskIDs )
2020-06-30 20:53:14 +00:00
if err != nil {
2024-03-18 22:08:14 +00:00
return nil , err
2020-06-30 20:53:14 +00:00
}
for _ , attachment := range attachments {
2020-07-17 11:26:49 +00:00
oldAttachmentID := attachment . ID
2020-06-30 20:53:14 +00:00
attachment . ID = 0
2020-08-16 21:44:16 +00:00
var exists bool
2024-03-18 22:08:14 +00:00
attachment . TaskID , exists = newTaskIDs [ attachment . TaskID ]
2020-08-16 21:44:16 +00:00
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
}
2020-06-30 20:53:14 +00:00
attachment . File = & files . File { ID : attachment . FileID }
if err := attachment . File . LoadFileMetaByID ( ) ; err != nil {
if files . IsErrFileDoesNotExist ( err ) {
2022-11-13 16:07:01 +00:00
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 )
2020-06-30 20:53:14 +00:00
continue
}
2024-03-18 22:08:14 +00:00
return nil , err
2020-06-30 20:53:14 +00:00
}
if err := attachment . File . LoadFileByID ( ) ; err != nil {
2024-03-18 22:08:14 +00:00
return nil , err
2020-06-30 20:53:14 +00:00
}
2021-02-02 22:48:37 +00:00
err := attachment . NewAttachment ( s , attachment . File . File , attachment . File . Name , attachment . File . Size , doer )
2020-06-30 20:53:14 +00:00
if err != nil {
2024-03-18 22:08:14 +00:00
return nil , err
2020-06-30 20:53:14 +00:00
}
if attachment . File . File != nil {
_ = attachment . File . File . Close ( )
}
2020-07-17 11:26:49 +00:00
2022-11-13 16:07:01 +00:00
log . Debugf ( "Duplicated attachment %d into %d from project %d into %d" , oldAttachmentID , attachment . ID , ld . ProjectID , ld . Project . ID )
2020-06-30 20:53:14 +00:00
}
2022-11-13 16:07:01 +00:00
log . Debugf ( "Duplicated all attachments from project %d into %d" , ld . ProjectID , ld . Project . ID )
2020-07-17 11:26:49 +00:00
2020-06-30 20:53:14 +00:00
// Copy label tasks (not the labels)
labelTasks := [ ] * LabelTask { }
2020-12-23 15:32:28 +00:00
err = s . In ( "task_id" , oldTaskIDs ) . Find ( & labelTasks )
2020-06-30 20:53:14 +00:00
if err != nil {
return
}
for _ , lt := range labelTasks {
lt . ID = 0
2024-03-18 22:08:14 +00:00
lt . TaskID = newTaskIDs [ lt . TaskID ]
2020-12-23 15:32:28 +00:00
if _ , err := s . Insert ( lt ) ; err != nil {
2024-03-18 22:08:14 +00:00
return nil , err
2020-06-30 20:53:14 +00:00
}
}
2022-11-13 16:07:01 +00:00
log . Debugf ( "Duplicated all labels from project %d into %d" , ld . ProjectID , ld . Project . ID )
2020-07-17 11:26:49 +00:00
2020-06-30 20:53:14 +00:00
// Assignees
// Only copy those assignees who have access to the task
assignees := [ ] * TaskAssginee { }
2020-12-23 15:32:28 +00:00
err = s . In ( "task_id" , oldTaskIDs ) . Find ( & assignees )
2020-06-30 20:53:14 +00:00
if err != nil {
return
}
for _ , a := range assignees {
t := & Task {
2024-03-18 22:08:14 +00:00
ID : newTaskIDs [ a . TaskID ] ,
2022-11-13 16:07:01 +00:00
ProjectID : ld . Project . ID ,
2020-06-30 20:53:14 +00:00
}
2022-11-13 16:07:01 +00:00
if err := t . addNewAssigneeByID ( s , a . UserID , ld . Project , doer ) ; err != nil {
if IsErrUserDoesNotHaveAccessToProject ( err ) {
2020-06-30 20:53:14 +00:00
continue
}
2024-03-18 22:08:14 +00:00
return nil , err
2020-06-30 20:53:14 +00:00
}
}
2022-11-13 16:07:01 +00:00
log . Debugf ( "Duplicated all assignees from project %d into %d" , ld . ProjectID , ld . Project . ID )
2020-07-17 11:26:49 +00:00
2020-06-30 20:53:14 +00:00
// Comments
comments := [ ] * TaskComment { }
2020-12-23 15:32:28 +00:00
err = s . In ( "task_id" , oldTaskIDs ) . Find ( & comments )
2020-06-30 20:53:14 +00:00
if err != nil {
return
}
for _ , c := range comments {
c . ID = 0
2024-03-18 22:08:14 +00:00
c . TaskID = newTaskIDs [ c . TaskID ]
2020-12-23 15:32:28 +00:00
if _ , err := s . Insert ( c ) ; err != nil {
2024-03-18 22:08:14 +00:00
return nil , err
2020-06-30 20:53:14 +00:00
}
}
2022-11-13 16:07:01 +00:00
log . Debugf ( "Duplicated all comments from project %d into %d" , ld . ProjectID , ld . Project . ID )
2020-07-17 11:26:49 +00:00
2022-11-13 16:07:01 +00:00
// Relations in that project
// Low-Effort: Only copy those relations which are between tasks in the same project
2020-06-30 20:53:14 +00:00
// because we can do that without a lot of hassle
relations := [ ] * TaskRelation { }
2020-12-23 15:32:28 +00:00
err = s . In ( "task_id" , oldTaskIDs ) . Find ( & relations )
2020-06-30 20:53:14 +00:00
if err != nil {
return
}
for _ , r := range relations {
2024-03-18 22:08:14 +00:00
otherTaskID , exists := newTaskIDs [ r . OtherTaskID ]
2020-06-30 20:53:14 +00:00
if ! exists {
continue
}
r . ID = 0
r . OtherTaskID = otherTaskID
2024-03-18 22:08:14 +00:00
r . TaskID = newTaskIDs [ r . TaskID ]
2020-12-23 15:32:28 +00:00
if _ , err := s . Insert ( r ) ; err != nil {
2024-03-18 22:08:14 +00:00
return nil , err
2020-06-30 20:53:14 +00:00
}
}
2022-11-13 16:07:01 +00:00
log . Debugf ( "Duplicated all task relations from project %d into %d" , ld . ProjectID , ld . Project . ID )
2020-07-17 11:26:49 +00:00
2024-03-18 22:08:14 +00:00
return
2020-06-30 20:53:14 +00:00
}