2020-02-07 16:27:45 +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.
2018-11-26 20:17:33 +00:00
//
2019-12-04 19:39:56 +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
2019-12-04 19:39:56 +00:00
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
2018-11-26 20:17:33 +00:00
//
2019-12-04 19:39:56 +00:00
// 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.
2018-11-26 20:17:33 +00:00
//
2020-12-23 15:41:52 +00:00
// You should have received a copy of the GNU Affero General Public Licensee
2019-12-04 19:39:56 +00:00
// along with this program. If not, see <https://www.gnu.org/licenses/>.
2018-11-26 20:17:33 +00:00
2018-06-10 12:14:10 +00:00
package models
2018-11-30 23:26:56 +00:00
import (
2024-02-28 11:02:12 +00:00
"fmt"
2023-03-30 11:49:01 +00:00
"math"
2020-12-21 23:13:15 +00:00
"strconv"
"strings"
2020-10-11 20:10:03 +00:00
"time"
2023-10-24 14:12:22 +00:00
"code.vikunja.io/api/pkg/db"
2021-02-02 22:48:37 +00:00
"code.vikunja.io/api/pkg/events"
2020-05-26 20:07:55 +00:00
"code.vikunja.io/api/pkg/files"
2021-03-24 21:46:20 +00:00
"code.vikunja.io/api/pkg/log"
2020-01-26 17:08:06 +00:00
"code.vikunja.io/api/pkg/user"
2023-10-24 14:12:22 +00:00
"code.vikunja.io/api/pkg/utils"
2018-11-30 23:26:56 +00:00
"code.vikunja.io/web"
2020-03-15 21:50:39 +00:00
"xorm.io/builder"
2020-05-16 10:58:37 +00:00
"xorm.io/xorm"
2018-11-30 23:26:56 +00:00
)
2018-11-02 16:59:49 +00:00
2022-11-13 16:07:01 +00:00
// Project represents a project of tasks
type Project struct {
// The unique, numeric id of this project.
ID int64 ` xorm:"bigint autoincr not null unique pk" json:"id" param:"project" `
2022-12-29 16:51:55 +00:00
// The title of the project. You'll see this in the overview.
2020-09-26 21:02:17 +00:00
Title string ` xorm:"varchar(250) not null" json:"title" valid:"required,runelength(1|250)" minLength:"1" maxLength:"250" `
2022-11-13 16:07:01 +00:00
// The description of the project.
2019-07-21 21:57:19 +00:00
Description string ` xorm:"longtext null" json:"description" `
2022-11-13 16:07:01 +00:00
// The unique project short identifier. Used to build task identifiers.
2019-12-07 22:28:45 +00:00
Identifier string ` xorm:"varchar(10) null" json:"identifier" valid:"runelength(0|10)" minLength:"0" maxLength:"10" `
2022-11-13 16:07:01 +00:00
// The hex color of this project
2023-10-24 14:12:22 +00:00
HexColor string ` xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|7)" maxLength:"7" `
2019-12-07 22:28:45 +00:00
2023-01-04 17:29:22 +00:00
OwnerID int64 ` xorm:"bigint INDEX not null" json:"-" `
ParentProjectID int64 ` xorm:"bigint INDEX null" json:"parent_project_id" `
ParentProject * Project ` xorm:"-" json:"-" `
2018-06-10 13:55:56 +00:00
2022-11-13 16:07:01 +00:00
// The user who created this project.
2020-01-26 17:08:06 +00:00
Owner * user . User ` xorm:"-" json:"owner" valid:"-" `
2020-12-16 14:19:09 +00:00
2022-12-29 15:40:06 +00:00
// Whether a project is archived.
2020-03-15 21:50:39 +00:00
IsArchived bool ` xorm:"not null default false" json:"is_archived" query:"is_archived" `
2022-11-13 16:07:01 +00:00
// The id of the file this project has set as background
2020-05-26 20:07:55 +00:00
BackgroundFileID int64 ` xorm:"null" json:"-" `
2022-11-13 16:07:01 +00:00
// Holds extra information about the background set since some background providers require attribution or similar. If not null, the background can be accessed at /projects/{projectID}/background
2020-05-26 20:07:55 +00:00
BackgroundInformation interface { } ` xorm:"-" json:"background_information" `
2022-11-13 16:07:01 +00:00
// Contains a very small version of the project background to use as a blurry preview until the actual background is loaded. Check out https://blurha.sh/ to learn how it works.
2021-12-12 20:26:51 +00:00
BackgroundBlurHash string ` xorm:"varchar(50) null" json:"background_blur_hash" `
2020-05-26 20:07:55 +00:00
2022-12-29 16:51:55 +00:00
// True if a project is a favorite. Favorite projects show up in a separate parent project. This value depends on the user making the call to the api.
2021-07-10 10:21:54 +00:00
IsFavorite bool ` xorm:"-" json:"is_favorite" `
2020-09-06 14:20:16 +00:00
2022-11-13 16:07:01 +00:00
// The subscription status for the user reading this project. You can only read this property, use the subscription endpoints to modify it.
// Will only returned when retreiving one project.
2021-02-14 19:18:14 +00:00
Subscription * Subscription ` xorm:"-" json:"subscription,omitempty" `
2022-11-13 16:07:01 +00:00
// The position this project has when querying all projects. See the tasks.position property on how to use this.
2021-07-28 19:06:40 +00:00
Position float64 ` xorm:"double null" json:"position" `
2024-03-14 08:36:39 +00:00
Views [ ] * ProjectView ` xorm:"-" json:"views" `
2022-11-13 16:07:01 +00:00
// A timestamp when this project was created. You cannot change this value.
2020-06-27 17:04:01 +00:00
Created time . Time ` xorm:"created not null" json:"created" `
2022-11-13 16:07:01 +00:00
// A timestamp when this project was last updated. You cannot change this value.
2020-06-27 17:04:01 +00:00
Updated time . Time ` xorm:"updated not null" json:"updated" `
2018-07-08 20:50:01 +00:00
2018-11-30 23:26:56 +00:00
web . CRUDable ` xorm:"-" json:"-" `
web . Rights ` xorm:"-" json:"-" `
2018-06-10 12:14:10 +00:00
}
2022-11-13 16:07:01 +00:00
type ProjectWithTasksAndBuckets struct {
Project
2022-12-29 17:11:15 +00:00
ChildProjects [ ] * ProjectWithTasksAndBuckets ` xorm:"-" json:"child_projects" `
2022-11-13 16:07:01 +00:00
// An array of tasks which belong to the project.
2021-09-04 19:26:31 +00:00
Tasks [ ] * TaskWithComments ` xorm:"-" json:"tasks" `
// Only used for migration.
2024-04-13 19:07:06 +00:00
Buckets [ ] * Bucket ` xorm:"-" json:"buckets" `
TaskBuckets [ ] * TaskBucket ` xorm:"-" json:"task_buckets" `
Positions [ ] * TaskPosition ` xorm:"-" json:"positions" `
BackgroundFileID int64 ` xorm:"null" json:"background_file_id" `
2021-09-04 19:26:31 +00:00
}
2022-11-13 16:07:01 +00:00
// TableName returns a better name for the projects table
2022-12-29 15:40:06 +00:00
func ( p * Project ) TableName ( ) string {
2022-11-13 16:07:01 +00:00
return "projects"
2021-03-28 18:17:35 +00:00
}
2022-11-13 16:07:01 +00:00
// ProjectBackgroundType holds a project background type
type ProjectBackgroundType struct {
2020-06-11 17:31:37 +00:00
Type string
}
2022-11-13 16:07:01 +00:00
// ProjectBackgroundUpload represents the project upload background type
const ProjectBackgroundUpload string = "upload"
2020-06-11 17:31:37 +00:00
2024-04-14 15:12:16 +00:00
const FavoritesPseudoProjectID = - 1
2022-12-29 15:40:06 +00:00
// FavoritesPseudoProject holds all tasks marked as favorites
var FavoritesPseudoProject = Project {
2024-04-14 15:12:16 +00:00
ID : FavoritesPseudoProjectID ,
2023-03-27 14:22:44 +00:00
Title : "Favorites" ,
Description : "This project has all tasks marked as favorites." ,
IsFavorite : true ,
Position : - 1 ,
2024-04-14 15:12:16 +00:00
Views : [ ] * ProjectView {
{
ID : - 1 ,
ProjectID : FavoritesPseudoProjectID ,
Title : "List" ,
ViewKind : ProjectViewKindList ,
Position : 100 ,
Filter : "done = false" ,
} ,
{
ID : - 2 ,
ProjectID : FavoritesPseudoProjectID ,
Title : "Gantt" ,
ViewKind : ProjectViewKindGantt ,
Position : 200 ,
} ,
{
ID : - 3 ,
ProjectID : FavoritesPseudoProjectID ,
Title : "Table" ,
ViewKind : ProjectViewKindTable ,
Position : 300 ,
} ,
} ,
Created : time . Now ( ) ,
Updated : time . Now ( ) ,
2018-07-03 06:48:28 +00:00
}
2018-07-08 20:50:01 +00:00
2022-11-13 16:07:01 +00:00
// ReadAll gets all projects a user has access to
// @Summary Get all projects a user has access to
// @Description Returns all projects a user has access to.
// @tags project
2018-11-12 15:46:35 +00:00
// @Accept json
// @Produce json
2019-10-23 21:11:40 +00:00
// @Param page query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param per_page query int false "The maximum number of items per page. Note this parameter is limited by the configured maximum of items per page."
2022-11-13 16:07:01 +00:00
// @Param s query string false "Search projects by title."
// @Param is_archived query bool false "If true, also returns all archived projects."
2019-01-03 22:22:06 +00:00
// @Security JWTKeyAuth
2022-11-13 16:07:01 +00:00
// @Success 200 {array} models.Project "The projects"
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
2018-11-12 15:46:35 +00:00
// @Failure 500 {object} models.Message "Internal error"
2022-11-13 16:07:01 +00:00
// @Router /projects [get]
2022-12-29 15:40:06 +00:00
func ( p * Project ) ReadAll ( s * xorm . Session , a web . Auth , search string , page int , perPage int ) ( result interface { } , resultCount int , totalItems int64 , err error ) {
2019-08-31 20:56:41 +00:00
// Check if we're dealing with a share auth
shareAuth , ok := a . ( * LinkSharing )
if ok {
2022-11-13 16:07:01 +00:00
project , err := GetProjectSimpleByID ( s , shareAuth . ProjectID )
2019-08-31 20:56:41 +00:00
if err != nil {
2019-10-23 21:11:40 +00:00
return nil , 0 , 0 , err
2019-08-31 20:56:41 +00:00
}
2022-12-29 21:11:07 +00:00
projects := [ ] * Project { project }
2022-11-13 16:07:01 +00:00
err = addProjectDetails ( s , projects , a )
2024-04-12 16:02:39 +00:00
if err == nil && len ( projects ) > 0 {
projects [ 0 ] . ParentProjectID = 0
}
2022-11-13 16:07:01 +00:00
return projects , 0 , 0 , err
2019-08-31 20:56:41 +00:00
}
2022-12-29 15:40:06 +00:00
doer , err := user . GetFromAuth ( a )
if err != nil {
return nil , 0 , 0 , err
}
2022-12-29 21:11:07 +00:00
prs , resultCount , totalItems , err := getRawProjectsForUser (
2020-12-23 15:32:28 +00:00
s ,
2022-11-13 16:07:01 +00:00
& projectOptions {
2022-12-29 15:40:06 +00:00
search : search ,
user : doer ,
page : page ,
perPage : perPage ,
getArchived : p . IsArchived ,
2020-12-23 15:32:28 +00:00
} )
2018-07-08 20:50:01 +00:00
if err != nil {
2019-10-23 21:11:40 +00:00
return nil , 0 , 0 , err
2018-07-08 20:50:01 +00:00
}
2022-12-29 15:40:06 +00:00
/////////////////
// Saved Filters
savedFiltersProject , err := getSavedFilterProjects ( s , doer )
if err != nil {
return nil , 0 , 0 , err
}
2023-06-07 16:55:36 +00:00
if len ( savedFiltersProject ) > 0 {
prs = append ( prs , savedFiltersProject ... )
2022-12-29 15:40:06 +00:00
}
/////////////////
// Add project details (favorite state, among other things)
2022-12-29 21:11:07 +00:00
err = addProjectDetails ( s , prs , a )
2022-12-29 15:40:06 +00:00
if err != nil {
return
}
//////////////////////////
// Putting it all together
2023-04-03 13:36:47 +00:00
return prs , resultCount , totalItems , err
2018-07-08 20:50:01 +00:00
}
2022-11-13 16:07:01 +00:00
// ReadOne gets one project by its ID
// @Summary Gets one project
// @Description Returns a project by its ID.
// @tags project
2018-11-12 15:46:35 +00:00
// @Accept json
// @Produce json
2019-01-03 22:22:06 +00:00
// @Security JWTKeyAuth
2022-11-13 16:07:01 +00:00
// @Param id path int true "Project ID"
// @Success 200 {object} models.Project "The project"
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
2018-11-12 15:46:35 +00:00
// @Failure 500 {object} models.Message "Internal error"
2022-11-13 16:07:01 +00:00
// @Router /projects/{id} [get]
2022-12-29 15:40:06 +00:00
func ( p * Project ) ReadOne ( s * xorm . Session , a web . Auth ) ( err error ) {
2020-09-05 20:16:02 +00:00
2022-12-29 15:40:06 +00:00
if p . ID == FavoritesPseudoProject . ID {
2024-04-14 15:12:16 +00:00
p . Views = FavoritesPseudoProject . Views
2022-11-13 16:07:01 +00:00
// Already "built" the project in CanRead
2020-09-05 20:16:02 +00:00
return nil
}
2020-09-26 21:02:17 +00:00
// Check for saved filters
2023-06-07 18:41:59 +00:00
filterID := getSavedFilterIDFromProjectID ( p . ID )
isFilter := filterID > 0
if isFilter {
sf , err := getSavedFilterSimpleByID ( s , filterID )
2020-09-26 21:02:17 +00:00
if err != nil {
return err
}
2022-12-29 15:40:06 +00:00
p . Title = sf . Title
p . Description = sf . Description
p . Created = sf . Created
p . Updated = sf . Updated
p . OwnerID = sf . OwnerID
2020-09-26 21:02:17 +00:00
}
2024-04-12 16:02:39 +00:00
_ , isShareAuth := a . ( * LinkSharing )
if isShareAuth {
p . ParentProjectID = 0
}
2022-11-13 16:07:01 +00:00
// Get project owner
2022-12-29 15:40:06 +00:00
p . Owner , err = user . GetUserByID ( s , p . OwnerID )
2020-03-22 20:39:57 +00:00
if err != nil {
return err
}
2022-12-29 16:51:55 +00:00
// Check if the project is archived and set it to archived if it is not already archived individually.
2023-06-07 18:41:59 +00:00
if ! p . IsArchived && ! isFilter {
2022-12-29 15:40:06 +00:00
err = p . CheckIsArchived ( s )
2020-03-22 20:39:57 +00:00
if err != nil {
2022-12-29 15:40:06 +00:00
p . IsArchived = true
2020-03-22 20:39:57 +00:00
}
}
2020-05-26 20:07:55 +00:00
// Get any background information if there is one set
2022-12-29 15:40:06 +00:00
if p . BackgroundFileID != 0 {
2020-06-11 17:31:37 +00:00
// Unsplash image
2022-12-29 15:40:06 +00:00
p . BackgroundInformation , err = GetUnsplashPhotoByFileID ( s , p . BackgroundFileID )
2020-05-26 20:07:55 +00:00
if err != nil && ! files . IsErrFileIsNotUnsplashFile ( err ) {
return
}
2020-06-11 17:31:37 +00:00
if err != nil && files . IsErrFileIsNotUnsplashFile ( err ) {
2022-12-29 15:40:06 +00:00
p . BackgroundInformation = & ProjectBackgroundType { Type : ProjectBackgroundUpload }
2020-06-11 17:31:37 +00:00
}
2020-05-26 20:07:55 +00:00
}
2022-12-29 15:40:06 +00:00
p . IsFavorite , err = isFavorite ( s , p . ID , a , FavoriteKindProject )
2021-07-10 10:21:54 +00:00
if err != nil {
return
}
2022-12-29 15:40:06 +00:00
p . Subscription , err = GetSubscription ( s , SubscriptionEntityProject , p . ID , a )
2023-06-07 18:41:59 +00:00
if err != nil && IsErrProjectDoesNotExist ( err ) && isFilter {
return nil
}
2024-03-14 08:36:39 +00:00
err = s .
Where ( "project_id = ?" , p . ID ) .
Find ( & p . Views )
2021-02-14 19:18:14 +00:00
return
2018-10-06 11:05:29 +00:00
}
2022-11-13 16:07:01 +00:00
// GetProjectSimpleByID gets a project with only the basic items, aka no tasks or user objects. Returns an error if the project does not exist.
func GetProjectSimpleByID ( s * xorm . Session , projectID int64 ) ( project * Project , err error ) {
2020-12-23 15:32:28 +00:00
2022-11-13 16:07:01 +00:00
project = & Project { }
2020-08-01 16:54:38 +00:00
2022-11-13 16:07:01 +00:00
if projectID < 1 {
return nil , ErrProjectDoesNotExist { ID : projectID }
2018-10-06 11:05:29 +00:00
}
2021-07-28 19:06:40 +00:00
exists , err := s .
2022-11-13 16:07:01 +00:00
Where ( "id = ?" , projectID ) .
2021-07-28 19:06:40 +00:00
OrderBy ( "position" ) .
2022-11-13 16:07:01 +00:00
Get ( project )
2018-10-06 11:05:29 +00:00
if err != nil {
return
}
if ! exists {
2022-11-13 16:07:01 +00:00
return nil , ErrProjectDoesNotExist { ID : projectID }
2018-10-06 11:05:29 +00:00
}
2018-07-08 20:50:01 +00:00
return
}
2018-10-05 17:16:14 +00:00
2022-11-13 16:07:01 +00:00
// GetProjectSimplByTaskID gets a project by a task id
func GetProjectSimplByTaskID ( s * xorm . Session , taskID int64 ) ( l * Project , err error ) {
// We need to re-init our project object, because otherwise xorm creates a "where for every item in that project object,
2019-01-08 19:13:07 +00:00
// leading to not finding anything if the id is good, but for example the title is different.
2022-11-13 16:07:01 +00:00
var project Project
2020-12-23 15:32:28 +00:00
exists , err := s .
2022-11-13 16:07:01 +00:00
Select ( "projects.*" ) .
Table ( Project { } ) .
Join ( "INNER" , "tasks" , "projects.id = tasks.project_id" ) .
2019-01-08 19:13:07 +00:00
Where ( "tasks.id = ?" , taskID ) .
2022-11-13 16:07:01 +00:00
Get ( & project )
2019-01-08 19:13:07 +00:00
if err != nil {
return
}
if ! exists {
2022-11-13 16:07:01 +00:00
return & Project { } , ErrProjectDoesNotExist { ID : l . ID }
2019-01-08 19:13:07 +00:00
}
2022-11-13 16:07:01 +00:00
return & project , nil
2019-01-08 19:13:07 +00:00
}
2024-03-02 13:27:11 +00:00
// GetProjectsMapSimplByTaskIDs gets a list of projects by a task ids
func GetProjectsMapSimplByTaskIDs ( s * xorm . Session , taskIDs [ ] int64 ) ( ps map [ int64 ] * Project , err error ) {
2023-10-20 11:56:14 +00:00
ps = make ( map [ int64 ] * Project )
err = s .
Select ( "projects.*" ) .
Table ( Project { } ) .
Join ( "INNER" , "tasks" , "projects.id = tasks.project_id" ) .
In ( "tasks.id" , taskIDs ) .
Find ( & ps )
return
}
2024-03-02 13:27:11 +00:00
func GetProjectsSimplByTaskIDs ( s * xorm . Session , taskIDs [ ] int64 ) ( ps [ ] * Project , err error ) {
err = s .
Select ( "projects.*" ) .
Table ( Project { } ) .
Join ( "INNER" , "tasks" , "projects.id = tasks.project_id" ) .
In ( "tasks.id" , taskIDs ) .
Find ( & ps )
return
}
// GetProjectsMapByIDs returns a map of projects from a slice with project ids
func GetProjectsMapByIDs ( s * xorm . Session , projectIDs [ ] int64 ) ( projects map [ int64 ] * Project , err error ) {
2022-11-13 16:07:01 +00:00
projects = make ( map [ int64 ] * Project , len ( projectIDs ) )
2021-03-02 17:40:39 +00:00
2022-11-13 16:07:01 +00:00
if len ( projectIDs ) == 0 {
2021-03-02 17:40:39 +00:00
return
}
2022-11-13 16:07:01 +00:00
err = s . In ( "id" , projectIDs ) . Find ( & projects )
2020-12-18 13:54:36 +00:00
return
}
2024-03-02 13:27:11 +00:00
func GetProjectsByIDs ( s * xorm . Session , projectIDs [ ] int64 ) ( projects [ ] * Project , err error ) {
projects = make ( [ ] * Project , 0 , len ( projectIDs ) )
if len ( projectIDs ) == 0 {
return
}
err = s . In ( "id" , projectIDs ) . Find ( & projects )
return
}
2022-11-13 16:07:01 +00:00
type projectOptions struct {
2022-12-29 15:40:06 +00:00
search string
user * user . User
page int
perPage int
getArchived bool
2020-03-15 21:50:39 +00:00
}
2024-04-07 10:10:20 +00:00
func getUserProjectsStatement ( userID int64 , search string , getArchived bool ) * builder . Builder {
2024-03-12 19:25:58 +00:00
dialect := db . GetDialect ( )
2021-03-24 21:46:20 +00:00
2022-12-29 15:40:06 +00:00
// Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions
var getArchivedCond builder . Cond = builder . Eq { "1" : 1 }
if ! getArchived {
getArchivedCond = builder . And (
builder . Eq { "l.is_archived" : false } ,
)
}
var filterCond builder . Cond
ids := [ ] int64 { }
if search != "" {
vals := strings . Split ( search , "," )
for _ , val := range vals {
v , err := strconv . ParseInt ( val , 10 , 64 )
if err != nil {
log . Debugf ( "Project search string part '%s' is not a number: %s" , val , err )
continue
}
ids = append ( ids , v )
}
}
filterCond = db . ILIKE ( "l.title" , search )
if len ( ids ) > 0 {
filterCond = builder . In ( "l.id" , ids )
}
2022-12-29 18:25:09 +00:00
var parentCondition builder . Cond
2023-09-07 08:56:59 +00:00
if search == "" {
parentCondition = builder . Or (
builder . IsNull { "l.parent_project_id" } ,
builder . Eq { "l.parent_project_id" : 0 } ,
// else check for shared sub projects with a parent
builder . And (
builder . Or (
builder . NotNull { "tm2.user_id" } ,
builder . NotNull { "ul.user_id" } ,
) ,
builder . NotNull { "l.parent_project_id" } ,
2023-07-03 09:45:29 +00:00
) ,
2023-09-07 08:56:59 +00:00
)
}
2022-12-29 18:25:09 +00:00
2021-03-24 21:46:20 +00:00
return builder . Dialect ( dialect ) .
Select ( "l.*" ) .
2022-11-13 16:07:01 +00:00
From ( "projects" , "l" ) .
2024-04-07 10:10:20 +00:00
Join ( "LEFT" , "team_projects tl" , "tl.project_id = l.id" ) .
2021-03-24 21:46:20 +00:00
Join ( "LEFT" , "team_members tm2" , "tm2.team_id = tl.team_id" ) .
2024-04-07 10:10:20 +00:00
Join ( "LEFT" , "users_projects ul" , "ul.project_id = l.id" ) .
2022-12-29 15:40:06 +00:00
Where ( builder . And (
builder . Or (
builder . Eq { "tm2.user_id" : userID } ,
builder . Eq { "ul.user_id" : userID } ,
builder . Eq { "l.owner_id" : userID } ,
) ,
filterCond ,
getArchivedCond ,
2022-12-29 18:25:09 +00:00
parentCondition ,
2021-03-24 21:46:20 +00:00
) ) .
GroupBy ( "l.id" )
}
2024-02-28 11:02:12 +00:00
func getAllProjectsForUser ( s * xorm . Session , userID int64 , opts * projectOptions ) ( projects [ ] * Project , totalCount int64 , err error ) {
2018-11-02 16:59:49 +00:00
2020-04-12 17:29:24 +00:00
limit , start := getLimitFromPageIndex ( opts . page , opts . perPage )
2024-04-07 10:10:20 +00:00
query := getUserProjectsStatement ( userID , opts . search , opts . getArchived )
2024-02-28 11:02:12 +00:00
querySQLString , args , err := query . ToSQL ( )
if err != nil {
return nil , 0 , err
}
var limitSQL string
2020-04-12 17:29:24 +00:00
if limit > 0 {
2024-02-28 11:02:12 +00:00
limitSQL = fmt . Sprintf ( "LIMIT %d OFFSET %d" , limit , start )
2020-04-12 17:29:24 +00:00
}
2022-12-29 18:25:09 +00:00
2024-02-28 11:02:12 +00:00
baseQuery := querySQLString + `
UNION ALL
SELECT p . * FROM projects p
INNER JOIN all_projects ap ON p . parent_project_id = ap . id `
2024-04-07 10:10:20 +00:00
columnStr := strings . Join ( [ ] string {
"all_projects.id" ,
"all_projects.title" ,
"all_projects.description" ,
"all_projects.identifier" ,
"all_projects.hex_color" ,
"all_projects.owner_id" ,
"CASE WHEN np.id IS NULL THEN 0 ELSE all_projects.parent_project_id END AS parent_project_id" ,
"all_projects.is_archived" ,
"all_projects.background_file_id" ,
"all_projects.background_blur_hash" ,
"all_projects.position" ,
"all_projects.created" ,
"all_projects.updated" ,
} , ", " )
2022-12-29 18:25:09 +00:00
currentProjects := [ ] * Project { }
2024-02-28 13:35:09 +00:00
err = s . SQL ( ` WITH RECURSIVE all_projects as ( ` + baseQuery + ` )
2024-04-07 10:10:20 +00:00
SELECT DISTINCT ` +columnStr+ ` FROM all_projects
LEFT JOIN all_projects np on all_projects . parent_project_id = np . id
ORDER BY all_projects . position ` + limitSQL , args ... ) . Find ( & currentProjects )
2019-10-23 21:11:40 +00:00
if err != nil {
2024-02-28 11:02:12 +00:00
return
2019-10-23 21:11:40 +00:00
}
2018-11-02 16:59:49 +00:00
2022-12-29 18:25:09 +00:00
if len ( currentProjects ) == 0 {
2024-02-28 11:02:12 +00:00
return nil , 0 , err
2022-12-29 18:25:09 +00:00
}
totalCount , err = s .
2024-02-28 11:02:12 +00:00
SQL ( ` WITH RECURSIVE all_projects as ( ` + baseQuery + ` )
2024-03-02 12:30:34 +00:00
SELECT COUNT ( DISTINCT all_projects . id ) FROM all_projects ` , args ... ) .
2022-11-13 16:07:01 +00:00
Count ( & Project { } )
2022-12-29 18:25:09 +00:00
if err != nil {
2024-02-28 11:02:12 +00:00
return nil , 0 , err
2023-07-03 09:45:29 +00:00
}
2024-02-28 11:02:12 +00:00
return currentProjects , totalCount , err
2022-12-29 18:25:09 +00:00
}
// Gets the projects with their children without any tasks
2022-12-29 21:11:07 +00:00
func getRawProjectsForUser ( s * xorm . Session , opts * projectOptions ) ( projects [ ] * Project , resultCount int , totalItems int64 , err error ) {
2022-12-29 18:25:09 +00:00
fullUser , err := user . GetUserByID ( s , opts . user . ID )
if err != nil {
return nil , 0 , 0 , err
}
2024-02-28 11:02:12 +00:00
allProjects , totalItems , err := getAllProjectsForUser ( s , fullUser . ID , opts )
2022-12-29 18:25:09 +00:00
if err != nil {
return
}
2022-12-29 15:40:06 +00:00
2023-03-27 14:22:44 +00:00
favoriteCount , err := s .
Where ( builder . And (
builder . Eq { "user_id" : opts . user . ID } ,
builder . Eq { "kind" : FavoriteKindTask } ,
) ) .
Count ( & Favorite { } )
if err != nil {
return
}
if favoriteCount > 0 {
favoritesProject := & Project { }
* favoritesProject = FavoritesPseudoProject
allProjects = append ( allProjects , favoritesProject )
}
2022-12-29 15:40:06 +00:00
if len ( allProjects ) == 0 {
return nil , 0 , totalItems , nil
}
2022-12-29 21:11:07 +00:00
return allProjects , len ( allProjects ) , totalItems , err
2018-11-02 16:59:49 +00:00
}
2023-06-07 16:55:36 +00:00
func getSavedFilterProjects ( s * xorm . Session , doer * user . User ) ( savedFiltersProjects [ ] * Project , err error ) {
2022-12-29 15:40:06 +00:00
savedFilters , err := getSavedFiltersForUser ( s , doer )
if err != nil {
2021-02-02 22:48:37 +00:00
return
}
2022-12-29 15:40:06 +00:00
if len ( savedFilters ) == 0 {
return nil , nil
2018-10-05 17:16:14 +00:00
}
2022-12-29 15:40:06 +00:00
for _ , filter := range savedFilters {
filterProject := filter . toProject ( )
filterProject . Owner = doer
2023-06-07 16:55:36 +00:00
savedFiltersProjects = append ( savedFiltersProjects , filterProject )
2018-10-05 17:16:14 +00:00
}
2022-12-29 15:40:06 +00:00
return
}
2023-01-04 17:29:22 +00:00
// GetAllParentProjects returns all parents of a given project
2024-03-03 14:31:42 +00:00
func GetAllParentProjects ( s * xorm . Session , projectID int64 ) ( allProjects map [ int64 ] * Project , err error ) {
allProjects = make ( map [ int64 ] * Project )
err = s . SQL ( ` WITH RECURSIVE all_projects AS (
SELECT
p . *
FROM
projects p
WHERE
p . id = ?
UNION ALL
SELECT
p . *
FROM
projects p
INNER JOIN all_projects pc ON p . ID = pc . parent_project_id
)
SELECT DISTINCT * FROM all_projects ` , projectID ) . Find ( & allProjects )
return
2023-01-04 17:29:22 +00:00
}
2022-12-29 15:40:06 +00:00
// addProjectDetails adds owner user objects and project tasks to all projects in the slice
2022-12-29 21:11:07 +00:00
func addProjectDetails ( s * xorm . Session , projects [ ] * Project , a web . Auth ) ( err error ) {
2022-12-29 15:40:06 +00:00
if len ( projects ) == 0 {
return
}
var ownerIDs [ ] int64
2022-11-13 16:07:01 +00:00
var projectIDs [ ] int64
2022-12-29 15:40:06 +00:00
var fileIDs [ ] int64
for _ , p := range projects {
ownerIDs = append ( ownerIDs , p . OwnerID )
projectIDs = append ( projectIDs , p . ID )
fileIDs = append ( fileIDs , p . BackgroundFileID )
}
owners , err := user . GetUsersByIDs ( s , ownerIDs )
if err != nil {
return err
2020-06-16 16:57:08 +00:00
}
2022-11-13 16:07:01 +00:00
favs , err := getFavorites ( s , projectIDs , a , FavoriteKindProject )
2021-07-10 10:21:54 +00:00
if err != nil {
return err
}
2024-03-02 13:27:11 +00:00
subscriptions , err := GetSubscriptionsForProjects ( s , projects , a )
2022-09-29 09:49:24 +00:00
if err != nil {
2022-12-29 15:40:06 +00:00
log . Errorf ( "An error occurred while getting project subscriptions for a project: %s" , err . Error ( ) )
2023-01-04 17:29:22 +00:00
subscriptions = make ( map [ int64 ] [ ] * Subscription )
2022-09-29 09:49:24 +00:00
}
2024-03-14 08:36:39 +00:00
views := [ ] * ProjectView { }
err = s .
In ( "project_id" , projectIDs ) .
Find ( & views )
if err != nil {
return
}
viewMap := make ( map [ int64 ] [ ] * ProjectView )
for _ , v := range views {
if _ , has := viewMap [ v . ProjectID ] ; ! has {
viewMap [ v . ProjectID ] = [ ] * ProjectView { }
}
viewMap [ v . ProjectID ] = append ( viewMap [ v . ProjectID ] , v )
}
2022-12-29 15:40:06 +00:00
for _ , p := range projects {
if o , exists := owners [ p . OwnerID ] ; exists {
p . Owner = o
}
if p . BackgroundFileID != 0 {
p . BackgroundInformation = & ProjectBackgroundType { Type : ProjectBackgroundUpload }
}
2021-07-20 19:32:25 +00:00
// Don't override the favorite state if it was already set from before (favorite saved filters do this)
2022-12-29 15:40:06 +00:00
if p . IsFavorite {
2021-07-20 19:32:25 +00:00
continue
}
2022-12-29 15:40:06 +00:00
p . IsFavorite = favs [ p . ID ]
2022-09-29 09:49:24 +00:00
2023-01-04 17:29:22 +00:00
if subscription , exists := subscriptions [ p . ID ] ; exists && len ( subscription ) > 0 {
p . Subscription = subscription [ 0 ]
2022-09-29 09:49:24 +00:00
}
2024-03-14 08:36:39 +00:00
vs , has := viewMap [ p . ID ]
if has {
p . Views = vs
}
2021-07-10 10:21:54 +00:00
}
2021-02-02 22:48:37 +00:00
if len ( fileIDs ) == 0 {
return
}
2020-06-16 16:57:08 +00:00
// Unsplash background file info
us := [ ] * UnsplashPhoto { }
2020-12-23 15:32:28 +00:00
err = s . In ( "file_id" , fileIDs ) . Find ( & us )
2020-06-16 16:57:08 +00:00
if err != nil {
return
}
unsplashPhotos := make ( map [ int64 ] * UnsplashPhoto , len ( us ) )
for _ , u := range us {
unsplashPhotos [ u . FileID ] = u
}
2022-11-13 16:07:01 +00:00
// Build it all into the projects slice
for _ , l := range projects {
2020-09-28 18:53:17 +00:00
// Only override the file info if we have info for unsplash backgrounds
if _ , exists := unsplashPhotos [ l . BackgroundFileID ] ; exists {
l . BackgroundInformation = unsplashPhotos [ l . BackgroundFileID ]
}
2018-10-05 17:16:14 +00:00
}
return
}
2019-07-16 14:15:40 +00:00
2022-12-29 16:51:55 +00:00
// CheckIsArchived returns an ErrProjectIsArchived if the project or any of its parent projects is archived.
2022-12-29 15:40:06 +00:00
func ( p * Project ) CheckIsArchived ( s * xorm . Session ) ( err error ) {
2022-12-29 20:06:29 +00:00
if p . ParentProjectID > 0 {
2022-12-29 15:40:06 +00:00
p := & Project { ID : p . ParentProjectID }
return p . CheckIsArchived ( s )
2020-03-15 21:50:39 +00:00
}
2022-12-29 21:11:37 +00:00
if p . ID == 0 { // don't check new projects
return nil
}
2022-12-29 16:51:55 +00:00
project , err := GetProjectSimpleByID ( s , p . ID )
2020-03-15 21:50:39 +00:00
if err != nil {
2022-12-29 15:40:06 +00:00
return err
2020-03-15 21:50:39 +00:00
}
2022-12-29 15:40:06 +00:00
2022-12-29 16:51:55 +00:00
if project . IsArchived {
2022-12-29 15:40:06 +00:00
return ErrProjectIsArchived { ProjectID : p . ID }
2020-03-15 21:50:39 +00:00
}
2022-12-29 15:40:06 +00:00
2020-03-15 21:50:39 +00:00
return nil
}
2023-04-03 09:46:08 +00:00
func checkProjectBeforeUpdateOrDelete ( s * xorm . Session , project * Project ) ( err error ) {
2022-12-29 15:40:06 +00:00
if project . ParentProjectID < 0 {
2022-12-29 16:51:55 +00:00
return & ErrProjectCannotBelongToAPseudoParentProject { ProjectID : project . ID , ParentProjectID : project . ParentProjectID }
2022-08-15 21:25:35 +00:00
}
2022-12-29 15:40:06 +00:00
// Check if the parent project exists
if project . ParentProjectID > 0 {
2023-03-27 10:56:01 +00:00
if project . ParentProjectID == project . ID {
return & ErrProjectCannotBeChildOfItself {
ProjectID : project . ID ,
}
}
2024-03-03 14:31:42 +00:00
allProjects , err := GetAllParentProjects ( s , project . ParentProjectID )
2019-07-16 14:15:40 +00:00
if err != nil {
2024-03-03 14:31:42 +00:00
return err
2019-07-16 14:15:40 +00:00
}
2023-04-03 09:46:08 +00:00
2024-03-03 10:40:30 +00:00
var parent * Project
parent = allProjects [ project . ParentProjectID ]
2023-04-03 09:46:08 +00:00
// Check if there's a cycle in the parent relation
parentsVisited := make ( map [ int64 ] bool )
parentsVisited [ project . ID ] = true
for {
if parent . ParentProjectID == 0 {
break
}
2024-03-03 10:40:30 +00:00
parent = allProjects [ parent . ParentProjectID ]
2023-04-03 09:46:08 +00:00
if parentsVisited [ parent . ID ] {
return & ErrProjectCannotHaveACyclicRelationship {
ProjectID : project . ID ,
}
}
parentsVisited [ parent . ID ] = true
}
2019-07-16 14:15:40 +00:00
}
2019-12-07 22:28:45 +00:00
// Check if the identifier is unique and not empty
2022-11-13 16:07:01 +00:00
if project . Identifier != "" {
2020-12-23 15:32:28 +00:00
exists , err := s .
2022-11-13 16:07:01 +00:00
Where ( "identifier = ?" , project . Identifier ) .
And ( "id != ?" , project . ID ) .
Exist ( & Project { } )
2019-12-07 22:28:45 +00:00
if err != nil {
return err
}
if exists {
2022-11-13 16:07:01 +00:00
return ErrProjectIdentifierIsNotUnique { Identifier : project . Identifier }
2019-12-07 22:28:45 +00:00
}
}
2021-11-13 16:52:14 +00:00
return nil
}
2021-07-28 19:06:40 +00:00
2024-03-18 22:08:14 +00:00
func CreateProject ( s * xorm . Session , project * Project , auth web . Auth , createBacklogBucket bool , createDefaultViews bool ) ( err error ) {
2022-11-13 16:07:01 +00:00
err = project . CheckIsArchived ( s )
2021-11-13 16:52:14 +00:00
if err != nil {
return err
}
2020-03-15 21:50:39 +00:00
2021-11-13 16:52:14 +00:00
doer , err := user . GetFromAuth ( auth )
if err != nil {
return err
}
2022-11-13 16:07:01 +00:00
project . OwnerID = doer . ID
project . Owner = doer
2021-11-13 16:52:14 +00:00
2022-11-13 16:07:01 +00:00
err = checkProjectBeforeUpdateOrDelete ( s , project )
2021-11-13 16:52:14 +00:00
if err != nil {
return
}
2023-10-24 14:12:22 +00:00
project . HexColor = utils . NormalizeHex ( project . HexColor )
2022-11-13 16:07:01 +00:00
_ , err = s . Insert ( project )
2021-11-13 16:52:14 +00:00
if err != nil {
return
}
2022-11-13 16:07:01 +00:00
project . Position = calculateDefaultPosition ( project . ID , project . Position )
_ , err = s . Where ( "id = ?" , project . ID ) . Update ( project )
2021-11-13 16:52:14 +00:00
if err != nil {
return
}
2022-11-13 16:07:01 +00:00
if project . IsFavorite {
if err := addToFavorites ( s , project . ID , auth , FavoriteKindProject ) ; err != nil {
2021-07-10 10:21:54 +00:00
return err
}
2021-11-13 16:52:14 +00:00
}
2021-07-10 10:21:54 +00:00
2024-03-18 22:08:14 +00:00
if createDefaultViews {
err = CreateDefaultViewsForProject ( s , project , auth , createBacklogBucket )
if err != nil {
return
}
2024-03-14 08:41:55 +00:00
}
2022-11-13 16:07:01 +00:00
return events . Dispatch ( & ProjectCreatedEvent {
Project : project ,
Doer : doer ,
2021-11-13 16:52:14 +00:00
} )
}
2022-12-29 15:40:06 +00:00
// CreateNewProjectForUser creates a new inbox project for a user. To prevent import cycles, we can't do that
// directly in the user.Create function.
2023-03-25 14:00:35 +00:00
func CreateNewProjectForUser ( s * xorm . Session , u * user . User ) ( err error ) {
2022-12-29 15:40:06 +00:00
p := & Project {
Title : "Inbox" ,
}
2023-03-25 14:00:35 +00:00
err = p . Create ( s , u )
if err != nil {
return err
}
if u . DefaultProjectID != 0 {
return err
}
u . DefaultProjectID = p . ID
_ , err = user . UpdateUser ( s , u , false )
return err
2022-12-29 15:40:06 +00:00
}
2022-11-13 16:07:01 +00:00
func UpdateProject ( s * xorm . Session , project * Project , auth web . Auth , updateProjectBackground bool ) ( err error ) {
err = checkProjectBeforeUpdateOrDelete ( s , project )
2021-11-13 16:52:14 +00:00
if err != nil {
return
}
2023-06-07 19:29:46 +00:00
if project . IsArchived {
isDefaultProject , err := project . isDefaultProject ( s )
if err != nil {
return err
}
if isDefaultProject {
return & ErrCannotArchiveDefaultProject { ProjectID : project . ID }
}
}
2022-11-13 16:07:01 +00:00
// We need to specify the cols we want to update here to be able to un-archive projects
2021-11-13 16:52:14 +00:00
colsToUpdate := [ ] string {
"title" ,
"is_archived" ,
"identifier" ,
"hex_color" ,
2022-12-29 17:17:53 +00:00
"parent_project_id" ,
2021-11-13 16:52:14 +00:00
"position" ,
2023-09-03 13:50:47 +00:00
"done_bucket_id" ,
"default_bucket_id" ,
2021-11-13 16:52:14 +00:00
}
2022-11-13 16:07:01 +00:00
if project . Description != "" {
2021-11-13 16:52:14 +00:00
colsToUpdate = append ( colsToUpdate , "description" )
}
2022-11-13 16:07:01 +00:00
if updateProjectBackground {
2021-12-12 20:42:35 +00:00
colsToUpdate = append ( colsToUpdate , "background_file_id" , "background_blur_hash" )
2021-11-13 16:52:14 +00:00
}
2023-03-30 11:49:01 +00:00
if project . Position < 0.1 {
err = recalculateProjectPositions ( s , project . ParentProjectID )
if err != nil {
return err
}
}
2022-11-13 16:07:01 +00:00
wasFavorite , err := isFavorite ( s , project . ID , auth , FavoriteKindProject )
2021-11-13 16:52:14 +00:00
if err != nil {
return err
}
2022-11-13 16:07:01 +00:00
if project . IsFavorite && ! wasFavorite {
if err := addToFavorites ( s , project . ID , auth , FavoriteKindProject ) ; err != nil {
2021-11-13 16:52:14 +00:00
return err
2021-07-10 10:21:54 +00:00
}
2021-11-13 16:52:14 +00:00
}
2021-07-10 10:21:54 +00:00
2022-11-13 16:07:01 +00:00
if ! project . IsFavorite && wasFavorite {
if err := removeFromFavorite ( s , project . ID , auth , FavoriteKindProject ) ; err != nil {
2021-07-10 10:21:54 +00:00
return err
}
2019-07-16 14:15:40 +00:00
}
2023-10-24 14:12:22 +00:00
project . HexColor = utils . NormalizeHex ( project . HexColor )
2021-11-13 16:52:14 +00:00
_ , err = s .
2022-11-13 16:07:01 +00:00
ID ( project . ID ) .
2021-11-13 16:52:14 +00:00
Cols ( colsToUpdate ... ) .
2022-11-13 16:07:01 +00:00
Update ( project )
2021-11-13 16:52:14 +00:00
if err != nil {
return err
}
2022-11-13 16:07:01 +00:00
err = events . Dispatch ( & ProjectUpdatedEvent {
Project : project ,
Doer : auth ,
2021-11-13 16:52:14 +00:00
} )
if err != nil {
return err
}
2022-11-13 16:07:01 +00:00
l , err := GetProjectSimpleByID ( s , project . ID )
2019-07-16 14:15:40 +00:00
if err != nil {
2020-12-23 15:32:28 +00:00
return err
2019-07-16 14:15:40 +00:00
}
2022-11-13 16:07:01 +00:00
* project = * l
err = project . ReadOne ( s , auth )
2019-07-16 14:15:40 +00:00
return
}
2023-03-30 11:49:01 +00:00
func recalculateProjectPositions ( s * xorm . Session , parentProjectID int64 ) ( err error ) {
allProjects := [ ] * Project { }
err = s .
Where ( "parent_project_id = ?" , parentProjectID ) .
OrderBy ( "position asc" ) .
Find ( & allProjects )
if err != nil {
return
}
maxPosition := math . Pow ( 2 , 32 )
for i , project := range allProjects {
currentPosition := maxPosition / float64 ( len ( allProjects ) ) * ( float64 ( i + 1 ) )
_ , err = s . Cols ( "position" ) .
Where ( "id = ?" , project . ID ) .
Update ( & Project { Position : currentPosition } )
if err != nil {
return
}
}
return
}
2019-07-16 14:15:40 +00:00
// Update implements the update method of CRUDable
2022-11-13 16:07:01 +00:00
// @Summary Updates a project
// @Description Updates a project. This does not include adding a task (see below).
// @tags project
2019-07-16 14:15:40 +00:00
// @Accept json
// @Produce json
// @Security JWTKeyAuth
2022-11-13 16:07:01 +00:00
// @Param id path int true "Project ID"
// @Param project body models.Project true "The project with updated values you want to update."
// @Success 200 {object} models.Project "The updated project."
// @Failure 400 {object} web.HTTPError "Invalid project object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
2019-07-16 14:15:40 +00:00
// @Failure 500 {object} models.Message "Internal error"
2022-11-13 16:07:01 +00:00
// @Router /projects/{id} [post]
2022-12-29 15:40:06 +00:00
func ( p * Project ) Update ( s * xorm . Session , a web . Auth ) ( err error ) {
fid := getSavedFilterIDFromProjectID ( p . ID )
2021-04-03 14:49:20 +00:00
if fid > 0 {
f , err := getSavedFilterSimpleByID ( s , fid )
if err != nil {
return err
}
2022-12-29 15:40:06 +00:00
f . Title = p . Title
f . Description = p . Description
f . IsFavorite = p . IsFavorite
2021-04-03 14:49:20 +00:00
err = f . Update ( s , a )
if err != nil {
return err
}
2022-12-29 15:40:06 +00:00
* p = * f . toProject ( )
2021-04-03 14:49:20 +00:00
return nil
}
2022-12-29 15:40:06 +00:00
return UpdateProject ( s , p , a , false )
2020-08-01 16:54:38 +00:00
}
2022-11-13 16:07:01 +00:00
func updateProjectLastUpdated ( s * xorm . Session , project * Project ) error {
_ , err := s . ID ( project . ID ) . Cols ( "updated" ) . Update ( project )
2019-07-16 14:15:40 +00:00
return err
}
2022-11-13 16:07:01 +00:00
func updateProjectByTaskID ( s * xorm . Session , taskID int64 ) ( err error ) {
// need to get the task to update the project last updated timestamp
2020-12-23 15:32:28 +00:00
task , err := GetTaskByIDSimple ( s , taskID )
2019-07-16 14:15:40 +00:00
if err != nil {
return err
}
2022-11-13 16:07:01 +00:00
return updateProjectLastUpdated ( s , & Project { ID : task . ProjectID } )
2019-07-16 14:15:40 +00:00
}
// Create implements the create method of CRUDable
2022-11-13 16:07:01 +00:00
// @Summary Creates a new project
2022-12-29 17:17:53 +00:00
// @Description Creates a new project. If a parent project is provided the user needs to have write access to that project.
2022-11-13 16:07:01 +00:00
// @tags project
2019-07-16 14:15:40 +00:00
// @Accept json
// @Produce json
// @Security JWTKeyAuth
2022-11-13 16:07:01 +00:00
// @Param project body models.Project true "The project you want to create."
// @Success 201 {object} models.Project "The created project."
// @Failure 400 {object} web.HTTPError "Invalid project object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
2019-07-16 14:15:40 +00:00
// @Failure 500 {object} models.Message "Internal error"
2022-12-29 17:17:53 +00:00
// @Router /projects [put]
2022-12-29 15:40:06 +00:00
func ( p * Project ) Create ( s * xorm . Session , a web . Auth ) ( err error ) {
2024-03-18 22:08:14 +00:00
err = CreateProject ( s , p , a , true , true )
2021-02-02 22:48:37 +00:00
if err != nil {
return
}
2024-03-29 18:28:17 +00:00
fullProject , err := GetProjectSimpleByID ( s , p . ID )
if err != nil {
return
}
return fullProject . ReadOne ( s , a )
2019-07-16 14:15:40 +00:00
}
2023-06-07 19:29:46 +00:00
func ( p * Project ) isDefaultProject ( s * xorm . Session ) ( is bool , err error ) {
return s .
Where ( "default_project_id = ?" , p . ID ) .
Exist ( & user . User { } )
}
2019-07-16 14:15:40 +00:00
// Delete implements the delete method of CRUDable
2022-11-13 16:07:01 +00:00
// @Summary Deletes a project
// @Description Delets a project
// @tags project
2019-07-16 14:15:40 +00:00
// @Produce json
// @Security JWTKeyAuth
2022-11-13 16:07:01 +00:00
// @Param id path int true "Project ID"
// @Success 200 {object} models.Message "The project was successfully deleted."
// @Failure 400 {object} web.HTTPError "Invalid project object provided."
// @Failure 403 {object} web.HTTPError "The user does not have access to the project"
2019-07-16 14:15:40 +00:00
// @Failure 500 {object} models.Message "Internal error"
2022-11-13 16:07:01 +00:00
// @Router /projects/{id} [delete]
2022-12-29 15:40:06 +00:00
func ( p * Project ) Delete ( s * xorm . Session , a web . Auth ) ( err error ) {
2019-07-16 14:15:40 +00:00
2023-06-07 19:29:46 +00:00
isDefaultProject , err := p . isDefaultProject ( s )
if err != nil {
return err
}
2023-08-23 14:10:51 +00:00
// Owners should be allowed to delete the default project
if isDefaultProject && p . OwnerID != a . GetID ( ) {
2023-06-07 19:29:46 +00:00
return & ErrCannotDeleteDefaultProject { ProjectID : p . ID }
}
2022-11-13 16:07:01 +00:00
// Delete all tasks on that project
2021-08-11 19:08:10 +00:00
// Using the loop to make sure all related entities to all tasks are properly deleted as well.
2023-08-28 10:14:50 +00:00
tasks , _ , _ , err := getRawTasksForProjects ( s , [ ] * Project { p } , a , & taskSearchOptions { } )
2021-02-02 22:48:37 +00:00
if err != nil {
return
}
2021-08-11 19:08:10 +00:00
for _ , task := range tasks {
err = task . Delete ( s , a )
if err != nil {
return err
}
}
2023-06-07 18:28:36 +00:00
fullProject , err := GetProjectSimpleByID ( s , p . ID )
if err != nil {
return
}
err = fullProject . DeleteBackgroundFileIfExists ( )
if err != nil {
return
}
2023-08-23 14:10:51 +00:00
// If we're deleting a default project, remove it as default
if isDefaultProject {
_ , err = s . Where ( "default_project_id = ?" , p . ID ) .
Cols ( "default_project_id" ) .
Update ( & user . User { DefaultProjectID : 0 } )
if err != nil {
return
}
}
2024-04-13 19:43:44 +00:00
// Delete related project entities
views , err := getViewsForProject ( s , p . ID )
if err != nil {
return
}
viewIDs := [ ] int64 { }
for _ , v := range views {
viewIDs = append ( viewIDs , v . ID )
}
_ , err = s . In ( "project_view_id" , viewIDs ) . Delete ( & Bucket { } )
if err != nil {
return
}
_ , err = s . In ( "id" , viewIDs ) . Delete ( & ProjectView { } )
if err != nil {
return
}
err = removeFromFavorite ( s , p . ID , a , FavoriteKindProject )
if err != nil {
return
}
_ , err = s . Where ( "project_id = ?" , p . ID ) . Delete ( & LinkSharing { } )
if err != nil {
return
}
_ , err = s . Where ( "project_id = ?" , p . ID ) . Delete ( & ProjectUser { } )
if err != nil {
return
}
_ , err = s . Where ( "project_id = ?" , p . ID ) . Delete ( & TeamProject { } )
if err != nil {
return
}
2023-06-07 18:28:36 +00:00
// Delete the project
_ , err = s . ID ( p . ID ) . Delete ( & Project { } )
2023-02-01 11:38:23 +00:00
if err != nil {
return
}
2023-12-01 16:27:40 +00:00
err = events . Dispatch ( & ProjectDeletedEvent {
2023-06-07 18:28:36 +00:00
Project : fullProject ,
2022-11-13 16:07:01 +00:00
Doer : a ,
2021-02-02 22:48:37 +00:00
} )
2023-12-01 16:27:40 +00:00
if err != nil {
return
}
childProjects := [ ] * Project { }
err = s . Where ( "parent_project_id = ?" , fullProject . ID ) . Find ( & childProjects )
if err != nil {
return
}
for _ , child := range childProjects {
err = child . Delete ( s , a )
if err != nil {
return
}
}
return
2019-07-16 14:15:40 +00:00
}
2020-05-26 20:07:55 +00:00
2023-02-01 11:38:23 +00:00
// DeleteBackgroundFileIfExists deletes the list's background file from the db and the filesystem,
// if one exists
2023-03-14 16:43:56 +00:00
func ( p * Project ) DeleteBackgroundFileIfExists ( ) ( err error ) {
if p . BackgroundFileID == 0 {
2023-02-01 11:38:23 +00:00
return
}
2023-03-14 16:43:56 +00:00
file := files . File { ID : p . BackgroundFileID }
2023-09-13 09:20:40 +00:00
err = file . Delete ( )
if err != nil && files . IsErrFileDoesNotExist ( err ) {
return nil
}
return err
2023-02-01 11:38:23 +00:00
}
2022-11-13 16:07:01 +00:00
// SetProjectBackground sets a background file as project background in the db
func SetProjectBackground ( s * xorm . Session , projectID int64 , background * files . File , blurHash string ) ( err error ) {
l := & Project {
ID : projectID ,
2021-12-12 20:42:35 +00:00
BackgroundFileID : background . ID ,
BackgroundBlurHash : blurHash ,
2020-05-26 20:07:55 +00:00
}
2020-12-23 15:32:28 +00:00
_ , err = s .
2020-05-26 20:07:55 +00:00
Where ( "id = ?" , l . ID ) .
2021-12-12 20:42:35 +00:00
Cols ( "background_file_id" , "background_blur_hash" ) .
2020-05-26 20:07:55 +00:00
Update ( l )
return
}