2020-06-30 20:53:14 +00:00
// Vikunja is a to-do list application to facilitate your life.
2021-02-02 19:19:13 +00:00
// Copyright 2018-2021 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" `
2020-06-30 20:53:14 +00:00
// The target namespace ID
NamespaceID int64 ` json:"namespace_id,omitempty" `
2022-11-13 16:07:01 +00:00
// The copied project
Project * Project ` json:",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
func ( ld * ProjectDuplicate ) CanCreate ( s * xorm . Session , a web . Auth ) ( canCreate bool , err error ) {
// Project Exists + user has read access to project
ld . Project = & Project { ID : ld . ProjectID }
canRead , _ , err := ld . Project . CanRead ( s , a )
2020-06-30 20:53:14 +00:00
if err != nil || ! canRead {
return canRead , err
}
2022-11-13 16:07:01 +00:00
// Namespace exists + user has write access to is (-> can create new projects)
ld . Project . NamespaceID = ld . NamespaceID
return ld . Project . 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
// @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 namespace. The user needs read access in the project and write access in the namespace of the new project.
// @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"
// @Param project body models.ProjectDuplicate true "The target namespace which should hold the copied project."
// @Success 201 {object} models.ProjectDuplicate "The created project."
// @Failure 400 {object} web.HTTPError "Invalid project duplicate object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the project or namespace"
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
2022-11-13 16:07:01 +00:00
func ( ld * ProjectDuplicate ) Create ( s * xorm . Session , doer web . Auth ) ( err error ) {
2020-06-30 20:53:14 +00:00
2022-11-13 16:07:01 +00:00
log . Debugf ( "Duplicating project %d" , ld . ProjectID )
2020-07-17 11:26:49 +00:00
2022-11-13 16:07:01 +00:00
ld . Project . ID = 0
ld . Project . Identifier = "" // Reset the identifier to trigger regenerating a new one
2020-06-30 20:53:14 +00:00
// Set the owner to the current user
2022-11-13 16:07:01 +00:00
ld . Project . OwnerID = doer . GetID ( )
if err := CreateProject ( s , ld . Project , doer ) ; err != nil {
// If there is no available unique project identifier, just reset it.
if IsErrProjectIdentifierIsNotUnique ( err ) {
ld . Project . Identifier = ""
2020-07-17 11:26:49 +00:00
} else {
return err
}
2020-06-30 20:53:14 +00:00
}
2022-11-13 16:07:01 +00:00
log . Debugf ( "Duplicated project %d into new project %d" , ld . ProjectID , ld . Project . ID )
2020-07-17 11:26:49 +00:00
2020-06-30 20:53:14 +00:00
// 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 { }
2022-11-13 16:07:01 +00:00
err = s . Where ( "project_id = ?" , ld . ProjectID ) . Find ( & buckets )
2020-06-30 20:53:14 +00:00
if err != nil {
return
}
for _ , b := range buckets {
oldID := b . ID
b . ID = 0
2022-11-13 16:07:01 +00:00
b . ProjectID = ld . Project . ID
2021-02-02 22:48:37 +00:00
if err := b . Create ( s , doer ) ; err != nil {
2020-06-30 20:53:14 +00:00
return err
}
bucketMap [ oldID ] = b . ID
}
2022-11-13 16:07:01 +00:00
log . Debugf ( "Duplicated all buckets from project %d into %d" , ld . ProjectID , ld . Project . ID )
2020-07-17 11:26:49 +00:00
2021-05-26 10:01:50 +00:00
err = duplicateTasks ( s , doer , ld , bucketMap )
if err != nil {
return
}
// Background files + unsplash info
2022-11-13 16:07:01 +00:00
if ld . Project . BackgroundFileID != 0 {
2021-05-26 10:01:50 +00:00
2022-11-13 16:07:01 +00:00
log . Debugf ( "Duplicating background %d from project %d into %d" , ld . Project . BackgroundFileID , ld . ProjectID , ld . Project . ID )
2021-05-26 10:01:50 +00:00
2022-11-13 16:07:01 +00:00
f := & files . File { ID : ld . Project . BackgroundFileID }
2021-05-26 10:01:50 +00:00
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 , doer )
if err != nil {
return err
}
// Get unsplash info if applicable
2022-11-13 16:07:01 +00:00
up , err := GetUnsplashPhotoByFileID ( s , ld . Project . BackgroundFileID )
2021-05-26 10:01:50 +00:00
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
}
}
2022-11-13 16:07:01 +00:00
if err := SetProjectBackground ( s , ld . Project . ID , file , ld . Project . BackgroundBlurHash ) ; err != nil {
2021-05-26 10:01:50 +00:00
return err
}
2022-11-13 16:07:01 +00:00
log . Debugf ( "Duplicated project background from project %d into %d" , ld . ProjectID , ld . Project . ID )
2021-05-26 10:01:50 +00:00
}
// Rights / Shares
2022-11-13 16:07:01 +00:00
// To keep it simple(r) we will only copy rights which are directly used with the project, no namespace changes.
users := [ ] * ProjectUser { }
err = s . Where ( "project_id = ?" , ld . ProjectID ) . Find ( & users )
2021-05-26 10:01:50 +00:00
if err != nil {
return
}
for _ , u := range users {
u . ID = 0
2022-11-13 16:07:01 +00:00
u . ProjectID = ld . Project . ID
2021-05-26 10:01:50 +00:00
if _ , err := s . Insert ( u ) ; err != nil {
return err
}
}
2022-11-13 16:07:01 +00:00
log . Debugf ( "Duplicated user shares from project %d into %d" , ld . ProjectID , ld . Project . ID )
2021-05-26 10:01:50 +00:00
2022-11-13 16:07:01 +00:00
teams := [ ] * TeamProject { }
err = s . Where ( "project_id = ?" , ld . ProjectID ) . Find ( & teams )
2021-05-26 10:01:50 +00:00
if err != nil {
return
}
for _ , t := range teams {
t . ID = 0
2022-11-13 16:07:01 +00:00
t . ProjectID = ld . 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 { }
2022-11-13 16:07:01 +00:00
err = s . Where ( "project_id = ?" , ld . ProjectID ) . Find ( & linkShares )
2021-05-26 10:01:50 +00:00
if err != nil {
return
}
for _ , share := range linkShares {
share . ID = 0
2022-11-13 16:07:01 +00:00
share . ProjectID = ld . Project . ID
2021-05-26 10:01:50 +00:00
share . Hash = utils . MakeRandomString ( 40 )
if _ , err := s . Insert ( share ) ; err != nil {
return err
}
}
2022-11-13 16:07:01 +00:00
log . Debugf ( "Duplicated all link shares from project %d into %d" , ld . ProjectID , ld . Project . ID )
2021-05-26 10:01:50 +00:00
return
}
2022-11-13 16:07:01 +00:00
func duplicateTasks ( s * xorm . Session , doer web . Auth , ld * ProjectDuplicate , bucketMap map [ int64 ] int64 ) ( err error ) {
2020-06-30 20:53:14 +00:00
// Get all tasks + all task details
2022-11-13 16:07:01 +00:00
tasks , _ , _ , err := getTasksForProjects ( s , [ ] * Project { { ID : ld . ProjectID } } , doer , & taskOptions { } )
2020-06-30 20:53:14 +00:00
if err != nil {
return err
}
2021-05-26 10:01:50 +00:00
if len ( tasks ) == 0 {
return nil
}
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.
2020-06-30 20:53:14 +00:00
taskMap := make ( map [ int64 ] int64 )
// 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 . BucketID = bucketMap [ t . BucketID ]
t . UID = ""
2021-02-02 22:48:37 +00:00
err := createTask ( s , t , doer , false )
2020-06-30 20:53:14 +00:00
if err != nil {
return err
}
taskMap [ oldID ] = t . ID
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 {
return err
}
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
attachment . TaskID , exists = taskMap [ 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
}
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
}
return err
}
if err := attachment . File . LoadFileByID ( ) ; err != nil {
return err
}
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 {
return err
}
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
lt . TaskID = taskMap [ lt . TaskID ]
2020-12-23 15:32:28 +00:00
if _ , err := s . Insert ( lt ) ; err != nil {
2020-06-30 20:53:14 +00:00
return err
}
}
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 {
2022-11-13 16:07:01 +00:00
ID : taskMap [ a . TaskID ] ,
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
}
return err
}
}
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
c . TaskID = taskMap [ c . TaskID ]
2020-12-23 15:32:28 +00:00
if _ , err := s . Insert ( c ) ; err != nil {
2020-06-30 20:53:14 +00:00
return err
}
}
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 {
otherTaskID , exists := taskMap [ r . OtherTaskID ]
if ! exists {
continue
}
r . ID = 0
r . OtherTaskID = otherTaskID
r . TaskID = taskMap [ r . TaskID ]
2020-12-23 15:32:28 +00:00
if _ , err := s . Insert ( r ) ; err != nil {
2020-06-30 20:53:14 +00:00
return err
}
}
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
2021-05-26 10:01:50 +00:00
return nil
2020-06-30 20:53:14 +00:00
}