2020-02-07 16:27:45 +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.
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 (
2020-12-21 23:13:15 +00:00
"strconv"
"strings"
2020-10-11 20:10:03 +00:00
"time"
2021-08-01 21:40:25 +00:00
"code.vikunja.io/api/pkg/db"
2021-03-24 21:46:20 +00:00
"code.vikunja.io/api/pkg/config"
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"
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" `
// The title of the project. You'll see this in the namespace 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
2020-03-22 21:09:32 +00:00
HexColor string ` xorm:"varchar(6) null" json:"hex_color" valid:"runelength(0|6)" maxLength:"6" `
2019-12-07 22:28:45 +00:00
2022-12-28 11:03:26 +00:00
OwnerID int64 ` xorm:"bigint INDEX not null" json:"-" `
NamespaceID int64 ` xorm:"bigint INDEX not null" json:"namespace_id" param:"namespace" `
ParentProjectID int64 ` xorm:"bigint INDEX null" json:"parent_project_id" `
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-11-13 16:07:01 +00:00
// Whether or not 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-11-13 16:07:01 +00:00
// True if a project is a favorite. Favorite projects show up in a separate namespace. 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" `
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
// 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.
Buckets [ ] * Bucket ` xorm:"-" json:"buckets" `
BackgroundFileID int64 ` xorm:"null" json:"background_file_id" `
}
2022-11-13 16:07:01 +00:00
// TableName returns a better name for the projects table
func ( l * Project ) TableName ( ) string {
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
2022-11-13 16:07:01 +00:00
// FavoritesPseudoProject holds all tasks marked as favorites
var FavoritesPseudoProject = Project {
2020-09-05 20:16:02 +00:00
ID : - 1 ,
Title : "Favorites" ,
2022-11-13 16:07:01 +00:00
Description : "This project has all tasks marked as favorites." ,
2020-09-05 20:16:02 +00:00
NamespaceID : FavoritesPseudoNamespace . ID ,
2020-09-06 14:20:16 +00:00
IsFavorite : true ,
2020-09-05 20:16:02 +00:00
Created : time . Now ( ) ,
Updated : time . Now ( ) ,
}
2022-11-13 16:07:01 +00:00
// GetProjectsByNamespaceID gets all projects in a namespace
func GetProjectsByNamespaceID ( s * xorm . Session , nID int64 , doer * user . User ) ( projects [ ] * Project , err error ) {
2021-03-18 20:39:38 +00:00
switch nID {
2022-11-13 16:07:01 +00:00
case SharedProjectsPseudoNamespace . ID :
nnn , err := getSharedProjectsInNamespace ( s , false , doer )
2021-03-18 20:39:38 +00:00
if err != nil {
return nil , err
}
2022-11-13 16:07:01 +00:00
if nnn != nil && nnn . Projects != nil {
projects = nnn . Projects
2021-03-18 20:39:38 +00:00
}
case FavoritesPseudoNamespace . ID :
2022-11-13 16:07:01 +00:00
namespaces := make ( map [ int64 ] * NamespaceWithProjects )
_ , err := getNamespacesWithProjects ( s , & namespaces , "" , false , 0 , - 1 , doer . ID )
2021-03-18 20:39:38 +00:00
if err != nil {
return nil , err
}
namespaceIDs , _ := getNamespaceOwnerIDs ( namespaces )
2022-11-13 16:07:01 +00:00
ls , err := getProjectsForNamespaces ( s , namespaceIDs , false )
2021-03-18 20:39:38 +00:00
if err != nil {
return nil , err
}
2022-11-13 16:07:01 +00:00
nnn , err := getFavoriteProjects ( s , ls , namespaceIDs , doer )
2021-03-18 20:39:38 +00:00
if err != nil {
return nil , err
}
2022-11-13 16:07:01 +00:00
if nnn != nil && nnn . Projects != nil {
projects = nnn . Projects
2021-03-18 20:39:38 +00:00
}
case SavedFiltersPseudoNamespace . ID :
nnn , err := getSavedFilters ( s , doer )
if err != nil {
return nil , err
}
2022-11-13 16:07:01 +00:00
if nnn != nil && nnn . Projects != nil {
projects = nnn . Projects
2021-03-18 20:39:38 +00:00
}
default :
2020-12-23 15:32:28 +00:00
err = s . Select ( "l.*" ) .
2020-03-15 21:50:39 +00:00
Alias ( "l" ) .
Join ( "LEFT" , [ ] string { "namespaces" , "n" } , "l.namespace_id = n.id" ) .
Where ( "l.is_archived = false" ) .
2022-12-23 16:48:21 +00:00
Where ( "n.is_archived = false OR n.is_archived IS NULL" ) .
2020-03-15 21:50:39 +00:00
Where ( "namespace_id = ?" , nID ) .
2022-11-13 16:07:01 +00:00
Find ( & projects )
2018-12-04 10:16:42 +00:00
}
2019-02-18 19:32:41 +00:00
if err != nil {
return nil , err
}
2018-12-04 10:16:42 +00:00
2022-11-13 16:07:01 +00:00
// get more project details
err = addProjectDetails ( s , projects , doer )
return projects , err
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]
func ( l * 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-11-13 16:07:01 +00:00
projects := [ ] * Project { project }
err = addProjectDetails ( s , projects , a )
return projects , 0 , 0 , err
2019-08-31 20:56:41 +00:00
}
2022-11-13 16:07:01 +00:00
projects , resultCount , totalItems , err := getRawProjectsForUser (
2020-12-23 15:32:28 +00:00
s ,
2022-11-13 16:07:01 +00:00
& projectOptions {
2020-12-23 15:32:28 +00:00
search : search ,
user : & user . User { ID : a . GetID ( ) } ,
page : page ,
perPage : perPage ,
isArchived : l . IsArchived ,
} )
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-11-13 16:07:01 +00:00
// Add more project details
err = addProjectDetails ( s , projects , a )
return projects , 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]
func ( l * Project ) ReadOne ( s * xorm . Session , a web . Auth ) ( err error ) {
2020-09-05 20:16:02 +00:00
2022-11-13 16:07:01 +00:00
if l . ID == FavoritesPseudoProject . ID {
// 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
2022-11-13 16:07:01 +00:00
if getSavedFilterIDFromProjectID ( l . ID ) > 0 {
sf , err := getSavedFilterSimpleByID ( s , getSavedFilterIDFromProjectID ( l . ID ) )
2020-09-26 21:02:17 +00:00
if err != nil {
return err
}
l . Title = sf . Title
l . Description = sf . Description
l . Created = sf . Created
l . Updated = sf . Updated
l . OwnerID = sf . OwnerID
}
2022-11-13 16:07:01 +00:00
// Get project owner
2020-12-23 15:32:28 +00:00
l . Owner , err = user . GetUserByID ( s , l . OwnerID )
2020-03-22 20:39:57 +00:00
if err != nil {
return err
}
// Check if the namespace is archived and set the namespace to archived if it is not already archived individually.
if ! l . IsArchived {
2020-12-23 15:32:28 +00:00
err = l . CheckIsArchived ( s )
2020-03-22 20:39:57 +00:00
if err != nil {
2022-11-13 16:07:01 +00:00
if ! IsErrNamespaceIsArchived ( err ) && ! IsErrProjectIsArchived ( err ) {
2020-03-22 20:39:57 +00:00
return
}
l . IsArchived = true
}
}
2020-05-26 20:07:55 +00:00
// Get any background information if there is one set
if l . BackgroundFileID != 0 {
2020-06-11 17:31:37 +00:00
// Unsplash image
2020-12-23 15:32:28 +00:00
l . BackgroundInformation , err = GetUnsplashPhotoByFileID ( s , l . 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-11-13 16:07:01 +00:00
l . BackgroundInformation = & ProjectBackgroundType { Type : ProjectBackgroundUpload }
2020-06-11 17:31:37 +00:00
}
2020-05-26 20:07:55 +00:00
}
2022-11-13 16:07:01 +00:00
l . IsFavorite , err = isFavorite ( s , l . ID , a , FavoriteKindProject )
2021-07-10 10:21:54 +00:00
if err != nil {
return
}
2022-11-13 16:07:01 +00:00
l . Subscription , err = GetSubscription ( s , SubscriptionEntityProject , l . ID , a )
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
}
2022-11-13 16:07:01 +00:00
// GetProjectsByIDs returns a map of projects from a slice with project ids
func GetProjectsByIDs ( s * xorm . Session , projectIDs [ ] int64 ) ( projects map [ int64 ] * Project , err error ) {
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
}
2022-11-13 16:07:01 +00:00
type projectOptions struct {
2020-03-15 21:50:39 +00:00
search string
user * user . User
page int
perPage int
isArchived bool
}
2022-11-13 16:07:01 +00:00
func getUserProjectsStatement ( userID int64 ) * builder . Builder {
2021-03-24 21:46:20 +00:00
dialect := config . DatabaseType . GetString ( )
if dialect == "sqlite" {
dialect = builder . SQLITE
}
return builder . Dialect ( dialect ) .
Select ( "l.*" ) .
2022-11-13 16:07:01 +00:00
From ( "projects" , "l" ) .
2021-03-24 21:46:20 +00:00
Join ( "INNER" , "namespaces n" , "l.namespace_id = n.id" ) .
Join ( "LEFT" , "team_namespaces tn" , "tn.namespace_id = n.id" ) .
Join ( "LEFT" , "team_members tm" , "tm.team_id = tn.team_id" ) .
2022-11-13 16:07:01 +00:00
Join ( "LEFT" , "team_projects tl" , "l.id = tl.project_id" ) .
2021-03-24 21:46:20 +00:00
Join ( "LEFT" , "team_members tm2" , "tm2.team_id = tl.team_id" ) .
2022-11-13 16:07:01 +00:00
Join ( "LEFT" , "users_projects ul" , "ul.project_id = l.id" ) .
2021-03-28 18:17:35 +00:00
Join ( "LEFT" , "users_namespaces un" , "un.namespace_id = l.namespace_id" ) .
2021-03-24 21:46:20 +00:00
Where ( builder . Or (
builder . Eq { "tm.user_id" : userID } ,
builder . Eq { "tm2.user_id" : userID } ,
builder . Eq { "ul.user_id" : userID } ,
builder . Eq { "un.user_id" : userID } ,
builder . Eq { "l.owner_id" : userID } ,
) ) .
2021-07-28 19:06:40 +00:00
OrderBy ( "position" ) .
2021-03-24 21:46:20 +00:00
GroupBy ( "l.id" )
}
2022-11-13 16:07:01 +00:00
// Gets the projects only, without any tasks or so
func getRawProjectsForUser ( s * xorm . Session , opts * projectOptions ) ( projects [ ] * Project , resultCount int , totalItems int64 , err error ) {
2020-12-23 15:32:28 +00:00
fullUser , err := user . GetUserByID ( s , opts . user . ID )
2018-11-02 16:59:49 +00:00
if err != nil {
2019-10-23 21:11:40 +00:00
return nil , 0 , 0 , err
2018-11-02 16:59:49 +00:00
}
2020-03-15 21:50:39 +00:00
// Adding a 1=1 condition by default here because xorm always needs a condition and cannot handle nil conditions
var isArchivedCond builder . Cond = builder . Eq { "1" : 1 }
if ! opts . isArchived {
isArchivedCond = builder . And (
builder . Eq { "l.is_archived" : false } ,
builder . Eq { "n.is_archived" : false } ,
)
}
2020-04-12 17:29:24 +00:00
limit , start := getLimitFromPageIndex ( opts . page , opts . perPage )
2020-12-21 23:13:15 +00:00
var filterCond builder . Cond
ids := [ ] int64 { }
2021-03-28 18:17:35 +00:00
if opts . search != "" {
vals := strings . Split ( opts . search , "," )
for _ , val := range vals {
v , err := strconv . ParseInt ( val , 10 , 64 )
if err != nil {
2022-11-13 16:07:01 +00:00
log . Debugf ( "Project search string part '%s' is not a number: %s" , val , err )
2021-03-28 18:17:35 +00:00
continue
}
ids = append ( ids , v )
2020-12-21 23:13:15 +00:00
}
}
2021-08-01 21:40:25 +00:00
filterCond = db . ILIKE ( "l.title" , opts . search )
2020-12-21 23:13:15 +00:00
if len ( ids ) > 0 {
filterCond = builder . In ( "l.id" , ids )
}
2022-11-13 16:07:01 +00:00
// Gets all Projects where the user is either owner or in a team which has access to the project
2018-11-02 16:59:49 +00:00
// Or in a team which has namespace read access
2021-03-24 21:46:20 +00:00
2022-11-13 16:07:01 +00:00
query := getUserProjectsStatement ( fullUser . ID ) .
2020-12-21 23:13:15 +00:00
Where ( filterCond ) .
2020-04-12 17:29:24 +00:00
Where ( isArchivedCond )
if limit > 0 {
query = query . Limit ( limit , start )
}
2022-11-13 16:07:01 +00:00
err = s . SQL ( query ) . Find ( & projects )
2019-10-23 21:11:40 +00:00
if err != nil {
return nil , 0 , 0 , err
}
2018-11-02 16:59:49 +00:00
2022-11-13 16:07:01 +00:00
query = getUserProjectsStatement ( fullUser . ID ) .
2020-12-21 23:13:15 +00:00
Where ( filterCond ) .
2021-03-24 21:46:20 +00:00
Where ( isArchivedCond )
totalItems , err = s .
SQL ( query . Select ( "count(*)" ) ) .
2022-11-13 16:07:01 +00:00
Count ( & Project { } )
return projects , len ( projects ) , totalItems , err
2018-11-02 16:59:49 +00:00
}
2022-11-13 16:07:01 +00:00
// addProjectDetails adds owner user objects and project tasks to all projects in the slice
func addProjectDetails ( s * xorm . Session , projects [ ] * Project , a web . Auth ) ( err error ) {
if len ( projects ) == 0 {
2021-02-02 22:48:37 +00:00
return
}
2018-10-05 17:16:14 +00:00
var ownerIDs [ ] int64
2022-11-13 16:07:01 +00:00
for _ , l := range projects {
2018-10-05 17:16:14 +00:00
ownerIDs = append ( ownerIDs , l . OwnerID )
}
2022-11-13 16:07:01 +00:00
// Get all project owners
2020-06-16 16:57:08 +00:00
owners := map [ int64 ] * user . User { }
2021-03-02 17:40:39 +00:00
if len ( ownerIDs ) > 0 {
err = s . In ( "id" , ownerIDs ) . Find ( & owners )
if err != nil {
return
}
2018-10-05 17:16:14 +00:00
}
2020-06-16 16:57:08 +00:00
var fileIDs [ ] int64
2022-11-13 16:07:01 +00:00
var projectIDs [ ] int64
for _ , l := range projects {
projectIDs = append ( projectIDs , l . ID )
2020-06-16 16:57:08 +00:00
if o , exists := owners [ l . OwnerID ] ; exists {
l . Owner = o
2018-10-05 17:16:14 +00:00
}
2020-06-16 16:57:08 +00:00
if l . BackgroundFileID != 0 {
2022-11-13 16:07:01 +00:00
l . BackgroundInformation = & ProjectBackgroundType { Type : ProjectBackgroundUpload }
2020-06-16 16:57:08 +00:00
}
fileIDs = append ( fileIDs , l . BackgroundFileID )
}
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
}
2022-11-13 16:07:01 +00:00
subscriptions , err := GetSubscriptions ( s , SubscriptionEntityProject , projectIDs , a )
2022-09-29 09:49:24 +00:00
if err != nil {
2022-11-13 16:07:01 +00:00
log . Errorf ( "An error occurred while getting project subscriptions for a namespace item: %s" , err . Error ( ) )
2022-09-29 09:49:24 +00:00
subscriptions = make ( map [ int64 ] * Subscription )
}
2022-11-13 16:07:01 +00:00
for _ , project := range projects {
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-11-13 16:07:01 +00:00
if project . IsFavorite {
2021-07-20 19:32:25 +00:00
continue
}
2022-11-13 16:07:01 +00:00
project . IsFavorite = favs [ project . ID ]
2022-09-29 09:49:24 +00:00
2022-11-13 16:07:01 +00:00
if subscription , exists := subscriptions [ project . ID ] ; exists {
project . Subscription = subscription
2022-09-29 09:49:24 +00:00
}
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-11-13 16:07:01 +00:00
// NamespaceProject is a meta type to be able to join a project with its namespace
type NamespaceProject struct {
Project Project ` xorm:"extends" `
2020-03-15 21:50:39 +00:00
Namespace Namespace ` xorm:"extends" `
}
2022-11-13 16:07:01 +00:00
// CheckIsArchived returns an ErrProjectIsArchived or ErrNamespaceIsArchived if the project or its namespace is archived.
func ( l * Project ) CheckIsArchived ( s * xorm . Session ) ( err error ) {
// When creating a new project, we check if the namespace is archived
2020-03-15 21:50:39 +00:00
if l . ID == 0 {
n := & Namespace { ID : l . NamespaceID }
2020-12-23 15:32:28 +00:00
return n . CheckIsArchived ( s )
2020-03-15 21:50:39 +00:00
}
2022-11-13 16:07:01 +00:00
nl := & NamespaceProject { }
2020-12-23 15:32:28 +00:00
exists , err := s .
2022-11-13 16:07:01 +00:00
Table ( "projects" ) .
Join ( "LEFT" , "namespaces" , "projects.namespace_id = namespaces.id" ) .
Where ( "projects.id = ? AND (projects.is_archived = true OR namespaces.is_archived = true)" , l . ID ) .
2020-03-15 21:50:39 +00:00
Get ( nl )
if err != nil {
return
}
2022-11-13 16:07:01 +00:00
if exists && nl . Project . ID != 0 && nl . Project . IsArchived {
return ErrProjectIsArchived { ProjectID : l . ID }
2020-03-15 21:50:39 +00:00
}
if exists && nl . Namespace . ID != 0 && nl . Namespace . IsArchived {
return ErrNamespaceIsArchived { NamespaceID : nl . Namespace . ID }
}
return nil
}
2022-11-13 16:07:01 +00:00
func checkProjectBeforeUpdateOrDelete ( s * xorm . Session , project * Project ) error {
if project . NamespaceID < 0 {
return & ErrProjectCannotBelongToAPseudoNamespace { ProjectID : project . ID , NamespaceID : project . NamespaceID }
2022-08-15 21:25:35 +00:00
}
2019-07-16 14:15:40 +00:00
// Check if the namespace exists
2022-11-13 16:07:01 +00:00
if project . NamespaceID > 0 {
_ , err := GetNamespaceByID ( s , project . NamespaceID )
2019-07-16 14:15:40 +00:00
if err != nil {
return err
}
}
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
2022-11-13 16:07:01 +00:00
func CreateProject ( s * xorm . Session , project * Project , auth web . Auth ) ( err error ) {
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
project . ID = 0 // Otherwise only the first time a new project would be created
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
}
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
2022-11-13 16:07:01 +00:00
// Create a new first bucket for this project
2021-11-13 16:52:14 +00:00
b := & Bucket {
2022-11-13 16:07:01 +00:00
ProjectID : project . ID ,
Title : "Backlog" ,
2021-11-13 16:52:14 +00:00
}
err = b . Create ( s , auth )
if err != nil {
return
}
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-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
}
2022-11-13 16:07:01 +00:00
if project . NamespaceID == 0 {
return & ErrProjectMustBelongToANamespace {
ProjectID : project . ID ,
NamespaceID : project . NamespaceID ,
2022-10-01 13:02:13 +00:00
}
}
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-01-23 12:59:43 +00:00
"namespace_id" ,
2021-11-13 16:52:14 +00:00
"position" ,
}
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
}
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
}
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
}
// 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]
func ( l * Project ) Update ( s * xorm . Session , a web . Auth ) ( err error ) {
fid := getSavedFilterIDFromProjectID ( l . ID )
2021-04-03 14:49:20 +00:00
if fid > 0 {
f , err := getSavedFilterSimpleByID ( s , fid )
if err != nil {
return err
}
f . Title = l . Title
f . Description = l . Description
2021-07-20 19:32:25 +00:00
f . IsFavorite = l . IsFavorite
2021-04-03 14:49:20 +00:00
err = f . Update ( s , a )
if err != nil {
return err
}
2022-11-13 16:07:01 +00:00
* l = * f . toProject ( )
2021-04-03 14:49:20 +00:00
return nil
}
2022-11-13 16:07:01 +00:00
return UpdateProject ( s , l , 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
// @Description Creates a new project in a given namespace. The user needs write-access to the namespace.
// @tags project
2019-07-16 14:15:40 +00:00
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param namespaceID path int true "Namespace ID"
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-11-13 16:07:01 +00:00
// @Router /namespaces/{namespaceID}/projects [put]
func ( l * Project ) Create ( s * xorm . Session , a web . Auth ) ( err error ) {
err = CreateProject ( s , l , a )
2021-02-02 22:48:37 +00:00
if err != nil {
return
}
2021-11-13 16:52:14 +00:00
return l . ReadOne ( s , a )
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]
func ( l * Project ) Delete ( s * xorm . Session , a web . Auth ) ( err error ) {
2019-07-16 14:15:40 +00:00
2022-11-13 16:07:01 +00:00
// Delete the project
_ , err = s . ID ( l . ID ) . Delete ( & Project { } )
2019-07-16 14:15:40 +00:00
if err != nil {
return
}
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.
2022-11-13 16:07:01 +00:00
tasks , _ , _ , err := getRawTasksForProjects ( s , [ ] * Project { l } , a , & taskOptions { } )
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
}
}
2022-11-13 16:07:01 +00:00
return events . Dispatch ( & ProjectDeletedEvent {
Project : l ,
Doer : a ,
2021-02-02 22:48:37 +00:00
} )
2019-07-16 14:15:40 +00:00
}
2020-05-26 20:07:55 +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
}