2020-04-19 07:27:28 +00:00
// Vikunja is a to-do list application to facilitate your life.
2023-09-01 06:32:28 +00:00
// Copyright 2018-present Vikunja and contributors. All rights reserved.
2020-04-19 07:27:28 +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-04-19 07:27:28 +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-04-19 07:27:28 +00:00
//
2020-12-23 15:41:52 +00:00
// You should have received a copy of the GNU Affero General Public Licensee
2020-04-19 07:27:28 +00:00
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
2023-11-22 09:33:03 +00:00
"strconv"
"strings"
2020-10-11 20:10:03 +00:00
"time"
2020-05-07 08:20:10 +00:00
"code.vikunja.io/api/pkg/log"
2020-04-19 07:27:28 +00:00
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
2020-08-01 16:54:38 +00:00
"xorm.io/xorm"
2020-04-19 07:27:28 +00:00
)
// Bucket represents a kanban bucket
type Bucket struct {
// The unique, numeric id of this bucket.
2020-12-18 16:51:22 +00:00
ID int64 ` xorm:"bigint autoincr not null unique pk" json:"id" param:"bucket" `
2020-04-19 07:27:28 +00:00
// The title of this bucket.
Title string ` xorm:"text not null" valid:"required" minLength:"1" json:"title" `
2022-11-13 16:07:01 +00:00
// The project this bucket belongs to.
2024-03-15 09:39:53 +00:00
ProjectID int64 ` xorm:"-" json:"-" param:"project" `
2024-03-15 09:32:38 +00:00
// The project view this bucket belongs to.
ProjectViewID int64 ` xorm:"bigint not null" json:"project_view_id" param:"view" `
2020-04-19 07:27:28 +00:00
// All tasks which belong to this bucket.
Tasks [ ] * Task ` xorm:"-" json:"tasks" `
2020-09-04 14:37:56 +00:00
// How many tasks can be at the same time on this board max
2021-04-15 14:55:21 +00:00
Limit int64 ` xorm:"default 0" json:"limit" minimum:"0" valid:"range(0|9223372036854775807)" `
2020-09-04 14:37:56 +00:00
2023-06-08 14:56:05 +00:00
// The number of tasks currently in this bucket
Count int64 ` xorm:"-" json:"count" `
2021-07-28 19:06:40 +00:00
// The position this bucket has when querying all buckets. See the tasks.position property on how to use this.
Position float64 ` xorm:"double null" json:"position" `
2020-04-19 07:27:28 +00:00
// A timestamp when this bucket was created. You cannot change this value.
2020-06-27 17:04:01 +00:00
Created time . Time ` xorm:"created not null" json:"created" `
2020-04-19 07:27:28 +00:00
// A timestamp when this bucket 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" `
2020-04-19 07:27:28 +00:00
// The user who initially created the bucket.
CreatedBy * user . User ` xorm:"-" json:"created_by" valid:"-" `
2020-12-18 16:51:22 +00:00
CreatedByID int64 ` xorm:"bigint not null" json:"-" `
2020-04-19 07:27:28 +00:00
2020-12-22 11:38:05 +00:00
// Including the task collection type so we can use task filters on kanban
TaskCollection ` xorm:"-" json:"-" `
2020-04-19 07:27:28 +00:00
web . Rights ` xorm:"-" json:"-" `
web . CRUDable ` xorm:"-" json:"-" `
}
// TableName returns the table name for this bucket.
func ( b * Bucket ) TableName ( ) string {
return "buckets"
}
2020-08-01 16:54:38 +00:00
func getBucketByID ( s * xorm . Session , id int64 ) ( b * Bucket , err error ) {
2020-04-19 07:27:28 +00:00
b = & Bucket { }
2020-08-01 16:54:38 +00:00
exists , err := s . Where ( "id = ?" , id ) . Get ( b )
2020-04-19 07:27:28 +00:00
if err != nil {
return
}
if ! exists {
return b , ErrBucketDoesNotExist { BucketID : id }
}
return
}
2024-03-15 10:27:31 +00:00
func getDefaultBucketID ( s * xorm . Session , view * ProjectView ) ( bucketID int64 , err error ) {
if view . DefaultBucketID != 0 {
return view . DefaultBucketID , nil
2023-09-03 13:17:17 +00:00
}
bucket := & Bucket { }
2020-08-01 16:54:38 +00:00
_ , err = s .
2024-03-15 10:27:31 +00:00
Where ( "project_view_id = ?" , view . ID ) .
2022-11-03 14:10:20 +00:00
OrderBy ( "position asc" ) .
2020-04-25 20:32:02 +00:00
Get ( bucket )
2021-03-24 20:16:35 +00:00
if err != nil {
2023-09-03 13:17:17 +00:00
return 0 , err
2021-03-24 20:16:35 +00:00
}
2023-09-03 13:17:17 +00:00
return bucket . ID , nil
2021-03-24 20:16:35 +00:00
}
2024-03-14 20:32:42 +00:00
// ReadAll returns all manual buckets for a certain project
2022-11-13 16:07:01 +00:00
// @Summary Get all kanban buckets of a project
2024-03-14 20:32:42 +00:00
// @Description Returns all kanban buckets which belong to that project. Buckets are always sorted by their `position` in ascending order. To get all buckets with their tasks, use the tasks endpoint with a kanban view.
2023-10-24 14:13:15 +00:00
// @tags project
2020-04-19 07:27:28 +00:00
// @Accept json
// @Produce json
// @Security JWTKeyAuth
2024-03-14 20:32:42 +00:00
// @Param id path int true "Project ID"
2024-03-15 09:39:53 +00:00
// @Param view path int true "Project view ID"
2024-03-14 20:32:42 +00:00
// @Success 200 {array} models.Bucket "The buckets"
2020-04-19 07:27:28 +00:00
// @Failure 500 {object} models.Message "Internal server error"
2024-03-15 09:39:53 +00:00
// @Router /projects/{id}/views/{view}/buckets [get]
2020-12-23 15:32:28 +00:00
func ( b * Bucket ) ReadAll ( s * xorm . Session , auth web . Auth , search string , page int , perPage int ) ( result interface { } , resultCount int , numberOfTotalItems int64 , err error ) {
2020-04-19 07:27:28 +00:00
2024-03-15 09:39:53 +00:00
view , err := GetProjectViewByID ( s , b . ProjectViewID , b . ProjectID )
2021-04-22 14:44:42 +00:00
if err != nil {
return nil , 0 , 0 , err
}
2024-03-15 09:39:53 +00:00
can , _ , err := view . CanRead ( s , auth )
2021-04-22 14:44:42 +00:00
if err != nil {
return nil , 0 , 0 , err
}
if ! can {
return nil , 0 , 0 , ErrGenericForbidden { }
}
2020-04-25 20:32:02 +00:00
buckets := [ ] * Bucket { }
2021-07-28 19:06:40 +00:00
err = s .
2024-03-15 09:39:53 +00:00
Where ( "project_view_id = ?" , b . ProjectViewID ) .
2021-07-28 19:06:40 +00:00
OrderBy ( "position" ) .
Find ( & buckets )
2020-04-19 07:27:28 +00:00
if err != nil {
return
}
2024-03-14 20:32:42 +00:00
userIDs := make ( [ ] int64 , 0 , len ( buckets ) )
for _ , bb := range buckets {
userIDs = append ( userIDs , bb . CreatedByID )
}
// Get all users
users , err := getUsersOrLinkSharesFromIDs ( s , userIDs )
if err != nil {
return
}
for _ , bb := range buckets {
bb . CreatedBy = users [ bb . CreatedByID ]
}
return buckets , len ( buckets ) , int64 ( len ( buckets ) ) , nil
}
func GetTasksInBucketsForView ( s * xorm . Session , view * ProjectView , opts * taskSearchOptions , auth web . Auth ) ( bucketsWithTasks [ ] * Bucket , err error ) {
// Get all buckets for this project
buckets := [ ] * Bucket { }
if view . BucketConfigurationMode == BucketConfigurationModeManual {
err = s .
Where ( "project_id = ?" , view . ProjectID ) .
OrderBy ( "position" ) .
Find ( & buckets )
if err != nil {
return
}
}
if view . BucketConfigurationMode == BucketConfigurationModeFilter {
for id , bc := range view . BucketConfiguration {
buckets = append ( buckets , & Bucket {
2024-03-15 09:32:38 +00:00
ID : int64 ( id ) ,
Title : bc . Title ,
ProjectViewID : view . ID ,
Position : float64 ( id ) ,
CreatedByID : auth . GetID ( ) ,
Created : time . Now ( ) ,
Updated : time . Now ( ) ,
2024-03-14 20:32:42 +00:00
} )
}
}
2020-04-19 07:27:28 +00:00
// Make a map from the bucket slice with their id as key so that we can use it to put the tasks in their buckets
bucketMap := make ( map [ int64 ] * Bucket , len ( buckets ) )
userIDs := make ( [ ] int64 , 0 , len ( buckets ) )
for _ , bb := range buckets {
bucketMap [ bb . ID ] = bb
userIDs = append ( userIDs , bb . CreatedByID )
}
// Get all users
2021-04-07 12:44:39 +00:00
users , err := getUsersOrLinkSharesFromIDs ( s , userIDs )
if err != nil {
return
2020-04-19 07:27:28 +00:00
}
for _ , bb := range buckets {
bb . CreatedBy = users [ bb . CreatedByID ]
}
2021-03-10 10:59:10 +00:00
tasks := [ ] * Task { }
opts . sortby = [ ] * sortParam {
{
2024-03-14 21:55:18 +00:00
projectViewID : view . ProjectID ,
orderBy : orderAscending ,
sortBy : taskPropertyPosition ,
2021-03-10 10:59:10 +00:00
} ,
}
2023-11-22 09:43:07 +00:00
for _ , filter := range opts . parsedFilters {
2021-03-10 10:59:10 +00:00
if filter . field == taskPropertyBucketID {
2023-11-22 09:33:03 +00:00
// Limiting the map to the one filter we're looking for is the easiest way to ensure we only
// get tasks in this bucket
bucketID := filter . value . ( int64 )
bucket := bucketMap [ bucketID ]
bucketMap = make ( map [ int64 ] * Bucket , 1 )
bucketMap [ bucketID ] = bucket
2021-03-10 10:59:10 +00:00
break
}
}
2023-11-22 09:33:03 +00:00
originalFilter := opts . filter
2021-03-10 10:59:10 +00:00
for id , bucket := range bucketMap {
2023-11-22 09:33:03 +00:00
if ! strings . Contains ( originalFilter , "bucket_id" ) {
2024-03-14 20:32:42 +00:00
var bucketFilter = "bucket_id = " + strconv . FormatInt ( id , 10 )
if view . BucketConfigurationMode == BucketConfigurationModeFilter {
bucketFilter = "(" + view . BucketConfiguration [ id ] . Filter + ")"
}
2023-11-22 09:33:03 +00:00
var filterString string
if originalFilter == "" {
2024-03-14 20:32:42 +00:00
filterString = bucketFilter
2023-11-22 09:33:03 +00:00
} else {
2024-03-14 20:32:42 +00:00
filterString = "(" + originalFilter + ") && " + bucketFilter
2023-11-22 09:33:03 +00:00
}
2024-03-11 15:13:42 +00:00
opts . parsedFilters , err = getTaskFiltersFromFilterString ( filterString , opts . filterTimezone )
2023-11-22 09:33:03 +00:00
if err != nil {
return
}
}
2021-03-10 10:59:10 +00:00
2024-03-15 09:39:53 +00:00
ts , _ , total , err := getRawTasksForProjects ( s , [ ] * Project { { ID : view . ProjectID } } , auth , opts )
2021-03-10 10:59:10 +00:00
if err != nil {
2024-03-14 20:32:42 +00:00
return nil , err
2021-03-10 10:59:10 +00:00
}
2023-06-08 14:56:05 +00:00
bucket . Count = total
2021-03-10 10:59:10 +00:00
tasks = append ( tasks , ts ... )
}
taskMap := make ( map [ int64 ] * Task , len ( tasks ) )
for _ , t := range tasks {
taskMap [ t . ID ] = t
2020-04-19 07:27:28 +00:00
}
2021-07-10 10:21:54 +00:00
err = addMoreInfoToTasks ( s , taskMap , auth )
2021-03-10 10:59:10 +00:00
if err != nil {
2024-03-14 20:32:42 +00:00
return nil , err
2021-03-10 10:59:10 +00:00
}
2020-12-22 11:38:05 +00:00
2020-04-19 07:27:28 +00:00
// Put all tasks in their buckets
// All tasks which are not associated to any bucket will have bucket id 0 which is the nil value for int64
// Since we created a bucked with that id at the beginning, all tasks should be in there.
for _ , task := range tasks {
2020-05-07 08:20:10 +00:00
// Check if the bucket exists in the map to prevent nil pointer panics
if _ , exists := bucketMap [ task . BucketID ] ; ! exists {
2024-03-14 20:32:42 +00:00
log . Debugf ( "Tried to put task %d into bucket %d which does not exist in project %d" , task . ID , task . BucketID , view . ProjectID )
2020-05-07 08:20:10 +00:00
continue
}
2020-04-19 07:27:28 +00:00
bucketMap [ task . BucketID ] . Tasks = append ( bucketMap [ task . BucketID ] . Tasks , task )
}
2024-03-14 20:32:42 +00:00
return buckets , nil
2020-04-19 07:27:28 +00:00
}
// Create creates a new bucket
// @Summary Create a new bucket
2022-11-13 16:07:01 +00:00
// @Description Creates a new kanban bucket on a project.
2023-10-24 14:13:15 +00:00
// @tags project
2020-04-19 07:27:28 +00:00
// @Accept json
// @Produce json
// @Security JWTKeyAuth
2022-11-13 16:07:01 +00:00
// @Param id path int true "Project Id"
2024-03-15 09:39:53 +00:00
// @Param view path int true "Project view ID"
2020-04-19 07:27:28 +00:00
// @Param bucket body models.Bucket true "The bucket object"
// @Success 200 {object} models.Bucket "The created bucket object."
2020-06-28 14:25:46 +00:00
// @Failure 400 {object} web.HTTPError "Invalid bucket object provided."
2022-11-13 16:07:01 +00:00
// @Failure 404 {object} web.HTTPError "The project does not exist."
2020-04-19 07:27:28 +00:00
// @Failure 500 {object} models.Message "Internal error"
2024-03-15 09:39:53 +00:00
// @Router /projects/{id}/views/{view}/buckets [put]
2020-12-23 15:32:28 +00:00
func ( b * Bucket ) Create ( s * xorm . Session , a web . Auth ) ( err error ) {
2021-04-07 13:02:57 +00:00
b . CreatedBy , err = GetUserOrLinkShareUser ( s , a )
2021-04-07 12:44:39 +00:00
if err != nil {
return
}
b . CreatedByID = b . CreatedBy . ID
2020-04-19 07:27:28 +00:00
2020-12-23 15:32:28 +00:00
_ , err = s . Insert ( b )
2021-07-28 19:06:40 +00:00
if err != nil {
return
}
b . Position = calculateDefaultPosition ( b . ID , b . Position )
_ , err = s . Where ( "id = ?" , b . ID ) . Update ( b )
2020-04-19 07:27:28 +00:00
return
}
// Update Updates an existing bucket
// @Summary Update an existing bucket
// @Description Updates an existing kanban bucket.
2023-10-24 14:13:15 +00:00
// @tags project
2020-04-19 07:27:28 +00:00
// @Accept json
// @Produce json
// @Security JWTKeyAuth
2022-11-13 16:07:01 +00:00
// @Param projectID path int true "Project Id"
2020-04-19 07:27:28 +00:00
// @Param bucketID path int true "Bucket Id"
2024-03-15 09:39:53 +00:00
// @Param view path int true "Project view ID"
2020-04-19 07:27:28 +00:00
// @Param bucket body models.Bucket true "The bucket object"
// @Success 200 {object} models.Bucket "The created bucket object."
2020-06-28 14:25:46 +00:00
// @Failure 400 {object} web.HTTPError "Invalid bucket object provided."
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
2020-04-19 07:27:28 +00:00
// @Failure 500 {object} models.Message "Internal error"
2024-03-15 09:39:53 +00:00
// @Router /projects/{projectID}/views/{view}/buckets/{bucketID} [post]
2023-03-24 18:17:33 +00:00
func ( b * Bucket ) Update ( s * xorm . Session , _ web . Auth ) ( err error ) {
2021-01-31 11:40:02 +00:00
_ , err = s .
Where ( "id = ?" , b . ID ) .
2021-03-24 20:16:35 +00:00
Cols (
"title" ,
"limit" ,
2021-07-28 19:06:40 +00:00
"position" ,
2021-03-24 20:16:35 +00:00
) .
2021-01-31 11:40:02 +00:00
Update ( b )
2020-04-19 07:27:28 +00:00
return
}
// Delete removes a bucket, but no tasks
// @Summary Deletes an existing bucket
2022-11-13 16:07:01 +00:00
// @Description Deletes an existing kanban bucket and dissociates all of its task. It does not delete any tasks. You cannot delete the last bucket on a project.
2023-10-24 14:13:15 +00:00
// @tags project
2020-04-19 07:27:28 +00:00
// @Accept json
// @Produce json
// @Security JWTKeyAuth
2022-11-13 16:07:01 +00:00
// @Param projectID path int true "Project Id"
2020-04-19 07:27:28 +00:00
// @Param bucketID path int true "Bucket Id"
2024-03-15 09:39:53 +00:00
// @Param view path int true "Project view ID"
2020-04-19 07:27:28 +00:00
// @Success 200 {object} models.Message "Successfully deleted."
2020-06-28 14:25:46 +00:00
// @Failure 404 {object} web.HTTPError "The bucket does not exist."
2020-04-19 07:27:28 +00:00
// @Failure 500 {object} models.Message "Internal error"
2024-03-15 09:39:53 +00:00
// @Router /projects/{projectID}/views/{view}/buckets/{bucketID} [delete]
2024-03-12 21:23:35 +00:00
func ( b * Bucket ) Delete ( s * xorm . Session , a web . Auth ) ( err error ) {
2020-08-01 16:54:38 +00:00
2020-04-25 20:32:02 +00:00
// Prevent removing the last bucket
2024-03-15 09:39:53 +00:00
total , err := s . Where ( "project_view_id = ?" , b . ProjectViewID ) . Count ( & Bucket { } )
2020-04-25 20:32:02 +00:00
if err != nil {
return
}
if total <= 1 {
return ErrCannotRemoveLastBucket {
2024-03-15 09:39:53 +00:00
BucketID : b . ID ,
ProjectViewID : b . ProjectViewID ,
2020-04-25 20:32:02 +00:00
}
}
// Get the default bucket
2024-03-15 10:27:31 +00:00
p , err := GetProjectViewByID ( s , b . ProjectViewID , b . ProjectID )
2020-04-25 20:32:02 +00:00
if err != nil {
return
}
2024-03-12 21:23:35 +00:00
var updateProject bool
if b . ID == p . DefaultBucketID {
p . DefaultBucketID = 0
updateProject = true
}
if b . ID == p . DoneBucketID {
p . DoneBucketID = 0
updateProject = true
}
if updateProject {
err = p . Update ( s , a )
if err != nil {
return
}
}
2023-09-03 13:17:17 +00:00
defaultBucketID , err := getDefaultBucketID ( s , p )
if err != nil {
return err
}
2020-04-25 20:32:02 +00:00
2020-04-19 07:27:28 +00:00
// Remove all associations of tasks to that bucket
2020-12-23 15:32:28 +00:00
_ , err = s .
Where ( "bucket_id = ?" , b . ID ) .
Cols ( "bucket_id" ) .
2023-09-03 13:17:17 +00:00
Update ( & Task { BucketID : defaultBucketID } )
2024-03-12 21:23:35 +00:00
if err != nil {
return
}
// Remove the bucket itself
_ , err = s . Where ( "id = ?" , b . ID ) . Delete ( & Bucket { } )
2020-12-23 15:32:28 +00:00
return
2020-04-19 07:27:28 +00:00
}