diff --git a/pkg/models/kanban.go b/pkg/models/kanban.go index c7e0214c0..2f0f112ea 100644 --- a/pkg/models/kanban.go +++ b/pkg/models/kanban.go @@ -38,7 +38,7 @@ type Bucket struct { // The project view this bucket belongs to. ProjectViewID int64 `xorm:"bigint not null" json:"project_view_id" param:"view"` // All tasks which belong to this bucket. - Tasks []*Task `xorm:"-" json:"tasks"` + Tasks []*Task `xorm:"-" json:"tasks,omitempty"` // How many tasks can be at the same time on this board max Limit int64 `xorm:"default 0" json:"limit" minimum:"0" valid:"range(0|9223372036854775807)"` @@ -271,7 +271,7 @@ func GetTasksInBucketsForView(s *xorm.Session, view *ProjectView, projects []*Pr taskMap[t.ID] = t } - err = addMoreInfoToTasks(s, taskMap, auth, view) + err = addMoreInfoToTasks(s, taskMap, auth, view, opts.expand) if err != nil { return nil, err } diff --git a/pkg/models/project.go b/pkg/models/project.go index 5a4e48211..0790ed604 100644 --- a/pkg/models/project.go +++ b/pkg/models/project.go @@ -29,8 +29,8 @@ import ( "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/utils" - "code.vikunja.io/api/pkg/web" + "xorm.io/builder" "xorm.io/xorm" ) @@ -175,6 +175,41 @@ var FavoritesPseudoProject = Project{ // @Failure 500 {object} models.Message "Internal error" // @Router /projects [get] func (p *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) { + prs, resultCount, totalItems, err := getAllRawProjects(s, a, search, page, perPage, p.IsArchived) + if err != nil { + return nil, 0, 0, err + } + + ///////////////// + // Add project details (favorite state, among other things) + err = addProjectDetails(s, prs, a) + if err != nil { + return + } + + if p.Expand == ProjectExpandableRights { + var doer *user.User + doer, err = user.GetFromAuth(a) + if err != nil { + return + } + err = addMaxRightToProjects(s, prs, doer) + if err != nil { + return + } + } else { + for _, pr := range prs { + pr.MaxRight = RightUnknown + } + } + + ////////////////////////// + // Putting it all together + + return prs, resultCount, totalItems, err +} + +func getAllRawProjects(s *xorm.Session, a web.Auth, search string, page int, perPage int, isArchived bool) (projects []*Project, resultCount int, totalItems int64, err error) { // Check if we're dealing with a share auth shareAuth, ok := a.(*LinkSharing) if ok { @@ -202,7 +237,7 @@ func (p *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, user: doer, page: page, perPage: perPage, - getArchived: p.IsArchived, + getArchived: isArchived, }) if err != nil { return nil, 0, 0, err @@ -220,27 +255,6 @@ func (p *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int, prs = append(prs, savedFiltersProject...) } - ///////////////// - // Add project details (favorite state, among other things) - err = addProjectDetails(s, prs, a) - if err != nil { - return - } - - if p.Expand == ProjectExpandableRights { - err = addMaxRightToProjects(s, prs, doer) - if err != nil { - return - } - } else { - for _, pr := range prs { - pr.MaxRight = RightUnknown - } - } - - ////////////////////////// - // Putting it all together - return prs, resultCount, totalItems, err } diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index a61084a2a..1bb419f58 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -52,7 +52,9 @@ type TaskCollection struct { // If set to `subtasks`, Vikunja will fetch only tasks which do not have subtasks and then in a // second step, will fetch all of these subtasks. This may result in more tasks than the // pagination limit being returned, but all subtasks will be present in the response. - Expand TaskCollectionExpandable `query:"expand" json:"-"` + // If set to `buckets`, the buckets of each task will be present in the response. + // You can set this multiple times with different values. + Expand []TaskCollectionExpandable `query:"expand" json:"-"` isSavedFilter bool @@ -63,15 +65,18 @@ type TaskCollection struct { type TaskCollectionExpandable string const TaskCollectionExpandSubtasks TaskCollectionExpandable = `subtasks` +const TaskCollectionExpandBuckets TaskCollectionExpandable = `buckets` // Validate validates if the TaskCollectionExpandable value is valid. func (t TaskCollectionExpandable) Validate() error { switch t { case TaskCollectionExpandSubtasks: return nil + case TaskCollectionExpandBuckets: + return nil } - return InvalidFieldErrorWithMessage([]string{"expand"}, "Expand must be one of the following values: subtasks") + return InvalidFieldErrorWithMessage([]string{"expand"}, "Expand must be one of the following values: subtasks, buckets") } func validateTaskField(fieldName string) error { @@ -341,9 +346,11 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa return nil, 0, 0, err } - err = tf.Expand.Validate() - if err != nil { - return nil, 0, 0, err + for _, expandValue := range tf.Expand { + err = expandValue.Validate() + if err != nil { + return nil, 0, 0, err + } } opts.search = search diff --git a/pkg/models/task_search.go b/pkg/models/task_search.go index 688834d1d..f289c1d99 100644 --- a/pkg/models/task_search.go +++ b/pkg/models/task_search.go @@ -319,7 +319,15 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo distinct += ", task_positions.position" } - if opts.expand == TaskCollectionExpandSubtasks { + var expandSubtasks = false + for _, expandable := range opts.expand { + if expandable == TaskCollectionExpandSubtasks { + expandSubtasks = true + break + } + } + + if expandSubtasks { cond = builder.And(cond, builder.IsNull{"task_relations.id"}) } @@ -340,7 +348,7 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo if joinTaskBuckets { query = query.Join("LEFT", "task_buckets", "task_buckets.task_id = tasks.id") } - if opts.expand == TaskCollectionExpandSubtasks { + if expandSubtasks { query = query.Join("LEFT", "task_relations", "tasks.id = task_relations.task_id and task_relations.relation_kind = 'parenttask'") } @@ -354,7 +362,7 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo } // fetch subtasks when expanding - if opts.expand == TaskCollectionExpandSubtasks { + if expandSubtasks { subtasks := []*Task{} taskIDs := []int64{} @@ -407,7 +415,7 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo if joinTaskBuckets { queryCount = queryCount.Join("LEFT", "task_buckets", "task_buckets.task_id = tasks.id") } - if opts.expand == TaskCollectionExpandSubtasks { + if expandSubtasks { queryCount = queryCount.Join("LEFT", "task_relations", "tasks.id = task_relations.task_id and task_relations.relation_kind = 'parenttask'") } totalCount, err = queryCount. diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 5817ca0f4..74427e6fe 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -119,6 +119,9 @@ type Task struct { // Can be used to move a task between buckets. In that case, the new bucket must be in the same view as the old one. BucketID int64 `xorm:"-" json:"bucket_id"` + // All buckets across all views this task is part of. Only present when fetching tasks with the `expand` parameter set to `buckets`. + Buckets []*Bucket `xorm:"-" json:"buckets,omitempty"` + // The position of the task - any task project can be sorted as usual by this parameter. // When accessing tasks via views with buckets, this is primarily used to sort them based on a range. // Positions are always saved per view. They will automatically be set if you request the tasks through a view @@ -185,7 +188,7 @@ type taskSearchOptions struct { filterTimezone string isSavedFilter bool projectIDs []int64 - expand TaskCollectionExpandable + expand []TaskCollectionExpandable } // ReadAll is a dummy function to still have that endpoint documented @@ -345,7 +348,7 @@ func getTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, opts taskMap[t.ID] = t } - err = addMoreInfoToTasks(s, taskMap, a, view) + err = addMoreInfoToTasks(s, taskMap, a, view, opts.expand) if err != nil { return nil, 0, 0, err } @@ -422,7 +425,7 @@ func GetTasksByUIDs(s *xorm.Session, uids []string, a web.Auth) (tasks []*Task, taskMap[t.ID] = t } - err = addMoreInfoToTasks(s, taskMap, a, nil) + err = addMoreInfoToTasks(s, taskMap, a, nil, nil) return } @@ -561,9 +564,59 @@ func addRelatedTasksToTasks(s *xorm.Session, taskIDs []int64, taskMap map[int64] return } +func addBucketsToTasks(s *xorm.Session, a web.Auth, taskIDs []int64, taskMap map[int64]*Task) (err error) { + if len(taskIDs) == 0 { + return nil + } + + taskBuckets := []*TaskBucket{} + err = s. + In("task_id", taskIDs). + Find(&taskBuckets) + if err != nil { + return err + } + + // We need to fetch all projects for that user to make sure they only + // get to see buckets that they have permission to see. + projectIDs := []int64{} + allProjects, _, _, err := getAllRawProjects(s, a, "", 0, -1, false) + if err != nil { + return err + } + + for _, project := range allProjects { + projectIDs = append(projectIDs, project.ID) + } + + buckets := make(map[int64]*Bucket) + err = s. + Where(builder.In("id", builder.Select("bucket_id"). + From("task_buckets"). + Where(builder.In("task_id", taskIDs)))). + And(builder.In("project_view_id", builder.Select("id"). + From("project_views"). + Where(builder.In("project_id", projectIDs)))). + Find(&buckets) + if err != nil { + return err + } + + for _, tb := range taskBuckets { + if taskMap[tb.TaskID].Buckets == nil { + taskMap[tb.TaskID].Buckets = []*Bucket{} + } + if bucket, exists := buckets[tb.BucketID]; exists { + taskMap[tb.TaskID].Buckets = append(taskMap[tb.TaskID].Buckets, bucket) + } + } + + return nil +} + // This function takes a map with pointers and returns a slice with pointers to tasks // It adds more stuff like assignees/labels/etc to a bunch of tasks -func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth, view *ProjectView) (err error) { +func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth, view *ProjectView, expand []TaskCollectionExpandable) (err error) { // No need to iterate over users and stuff if the project doesn't have tasks if len(taskMap) == 0 { @@ -632,6 +685,23 @@ func addMoreInfoToTasks(s *xorm.Session, taskMap map[int64]*Task, a web.Auth, vi } } + if expand != nil { + expanded := make(map[TaskCollectionExpandable]bool) + for _, expandable := range expand { + if expanded[expandable] { + continue + } + + if expandable == TaskCollectionExpandBuckets { + err = addBucketsToTasks(s, a, taskIDs, taskMap) + if err != nil { + return err + } + } + expanded[expandable] = true + } + } + // Add all objects to their tasks for _, task := range taskMap { @@ -1618,7 +1688,8 @@ func (t *Task) ReadOne(s *xorm.Session, a web.Auth) (err error) { taskMap := make(map[int64]*Task, 1) taskMap[t.ID] = t - err = addMoreInfoToTasks(s, taskMap, a, nil) + // TODO add expand here as well + err = addMoreInfoToTasks(s, taskMap, a, nil, nil) if err != nil { return } diff --git a/pkg/models/typesense.go b/pkg/models/typesense.go index 695d4d189..c8217c06a 100644 --- a/pkg/models/typesense.go +++ b/pkg/models/typesense.go @@ -270,7 +270,7 @@ func reindexTasksInTypesense(s *xorm.Session, tasks map[int64]*Task) (err error) return } - err = addMoreInfoToTasks(s, tasks, &user.User{ID: 1}, nil) + err = addMoreInfoToTasks(s, tasks, &user.User{ID: 1}, nil, nil) if err != nil { return fmt.Errorf("could not fetch more task info: %s", err.Error()) }