vikunja/pkg/models/task_collection.go

288 lines
9.6 KiB
Go

// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-present Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// 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
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"strings"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"xorm.io/xorm"
)
// TaskCollection is a struct used to hold filter details and not clutter the Task struct with information not related to actual tasks.
type TaskCollection struct {
ProjectID int64 `param:"project" json:"-"`
ProjectViewID int64 `param:"view" json:"-"`
// The query parameter to sort by. This is for ex. done, priority, etc.
SortBy []string `query:"sort_by" json:"sort_by"`
SortByArr []string `query:"sort_by[]" json:"-"`
// The query parameter to order the items by. This can be either asc or desc, with asc being the default.
OrderBy []string `query:"order_by" json:"order_by"`
OrderByArr []string `query:"order_by[]" json:"-"`
// The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation.
Filter string `query:"filter" json:"filter"`
// The time zone which should be used for date match (statements like "now" resolve to different actual times)
FilterTimezone string `query:"filter_timezone" json:"-"`
// If set to true, the result will also include null values
FilterIncludeNulls bool `query:"filter_include_nulls" json:"filter_include_nulls"`
isSavedFilter bool
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
}
func validateTaskField(fieldName string) error {
switch fieldName {
case
taskPropertyID,
taskPropertyTitle,
taskPropertyDescription,
taskPropertyDone,
taskPropertyDoneAt,
taskPropertyDueDate,
taskPropertyCreatedByID,
taskPropertyProjectID,
taskPropertyRepeatAfter,
taskPropertyPriority,
taskPropertyStartDate,
taskPropertyEndDate,
taskPropertyHexColor,
taskPropertyPercentDone,
taskPropertyUID,
taskPropertyCreated,
taskPropertyUpdated,
taskPropertyPosition,
taskPropertyBucketID,
taskPropertyIndex:
return nil
}
return ErrInvalidTaskField{TaskField: fieldName}
}
func getTaskFilterOptsFromCollection(tf *TaskCollection, projectView *ProjectView) (opts *taskSearchOptions, err error) {
if len(tf.SortByArr) > 0 {
tf.SortBy = append(tf.SortBy, tf.SortByArr...)
}
if len(tf.OrderByArr) > 0 {
tf.OrderBy = append(tf.OrderBy, tf.OrderByArr...)
}
var sort = make([]*sortParam, 0, len(tf.SortBy))
for i, s := range tf.SortBy {
param := &sortParam{
sortBy: s,
orderBy: orderAscending,
}
// This checks if tf.OrderBy has an entry with the same index as the current entry from tf.SortBy
// Taken from https://stackoverflow.com/a/27252199/10924593
if len(tf.OrderBy) > i {
param.orderBy = getSortOrderFromString(tf.OrderBy[i])
}
if s == taskPropertyPosition && projectView != nil && projectView.ID < 0 {
continue
}
if s == taskPropertyPosition && projectView != nil {
param.projectViewID = projectView.ID
}
// Param validation
if err := param.validate(); err != nil {
return nil, err
}
sort = append(sort, param)
}
opts = &taskSearchOptions{
sortby: sort,
filterIncludeNulls: tf.FilterIncludeNulls,
filter: tf.Filter,
filterTimezone: tf.FilterTimezone,
}
opts.parsedFilters, err = getTaskFiltersFromFilterString(tf.Filter, tf.FilterTimezone)
return opts, err
}
func getTaskOrTasksInBuckets(s *xorm.Session, a web.Auth, projects []*Project, view *ProjectView, opts *taskSearchOptions) (tasks interface{}, resultCount int, totalItems int64, err error) {
if view != nil && !strings.Contains(opts.filter, "bucket_id") {
if view.BucketConfigurationMode != BucketConfigurationModeNone {
tasksInBuckets, err := GetTasksInBucketsForView(s, view, projects, opts, a)
return tasksInBuckets, len(tasksInBuckets), int64(len(tasksInBuckets)), err
}
}
return getTasksForProjects(s, projects, a, opts, view)
}
func getRelevantProjectsFromCollection(s *xorm.Session, a web.Auth, tf *TaskCollection) (projects []*Project, err error) {
if tf.ProjectID == 0 || tf.isSavedFilter {
projects, _, _, err = getRawProjectsForUser(
s,
&projectOptions{
user: &user.User{ID: a.GetID()},
page: -1,
},
)
return projects, err
}
// Check the project exists and the user has access on it
project := &Project{ID: tf.ProjectID}
canRead, _, err := project.CanRead(s, a)
if err != nil {
return nil, err
}
if !canRead {
return nil, ErrUserDoesNotHaveAccessToProject{
ProjectID: tf.ProjectID,
UserID: a.GetID(),
}
}
return []*Project{{ID: tf.ProjectID}}, nil
}
// ReadAll gets all tasks for a collection
// @Summary Get tasks in a project
// @Description Returns all tasks for the current project.
// @tags task
// @Accept json
// @Produce json
// @Param id path int true "The project ID."
// @Param view path int true "The project view ID."
// @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."
// @Param s query string false "Search tasks by task text."
// @Param sort_by query string false "The sorting parameter. You can pass this multiple times to get the tasks ordered by multiple different parametes, along with `order_by`. Possible values to sort by are `id`, `title`, `description`, `done`, `done_at`, `due_date`, `created_by_id`, `project_id`, `repeat_after`, `priority`, `start_date`, `end_date`, `hex_color`, `percent_done`, `uid`, `created`, `updated`. Default is `id`."
// @Param order_by query string false "The ordering parameter. Possible values to order by are `asc` or `desc`. Default is `asc`."
// @Param filter query string false "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation of the feature."
// @Param filter_timezone query string false "The time zone which should be used for date match (statements like "now" resolve to different actual times)"
// @Param filter_include_nulls query string false "If set to true the result will include filtered fields whose value is set to `null`. Available values are `true` or `false`. Defaults to `false`."
// @Security JWTKeyAuth
// @Success 200 {array} models.Task "The tasks"
// @Failure 500 {object} models.Message "Internal error"
// @Router /projects/{id}/views/{view}/tasks [get]
func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, totalItems int64, err error) {
// If the project id is < -1 this means we're dealing with a saved filter - in that case we get and populate the filter
// -1 is the favorites project which works as intended
if !tf.isSavedFilter && tf.ProjectID < -1 {
sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(tf.ProjectID))
if err != nil {
return nil, 0, 0, err
}
// By prepending sort options before the saved ones from the filter, we make sure the supplied sort
// options via query take precedence over the rest.
sortby := append(tf.SortBy, tf.SortByArr...)
sortby = append(sortby, sf.Filters.SortBy...)
sortby = append(sortby, sf.Filters.SortByArr...)
orderby := append(tf.OrderBy, tf.OrderByArr...)
orderby = append(orderby, sf.Filters.OrderBy...)
orderby = append(orderby, sf.Filters.OrderByArr...)
sf.Filters.SortBy = sortby
sf.Filters.SortByArr = nil
sf.Filters.OrderBy = orderby
sf.Filters.OrderByArr = nil
if sf.Filters.FilterTimezone == "" {
u, err := user.GetUserByID(s, a.GetID())
if err != nil {
return nil, 0, 0, err
}
sf.Filters.FilterTimezone = u.Timezone
}
tc := sf.getTaskCollection()
tc.ProjectViewID = tf.ProjectViewID
tc.ProjectID = tf.ProjectID
tc.isSavedFilter = true
return tc.ReadAll(s, a, search, page, perPage)
}
var view *ProjectView
if tf.ProjectViewID != 0 {
view, err = GetProjectViewByIDAndProject(s, tf.ProjectViewID, tf.ProjectID)
if err != nil {
return nil, 0, 0, err
}
if view.Filter != "" {
if tf.Filter != "" {
tf.Filter = "(" + tf.Filter + ") && (" + view.Filter + ")"
} else {
tf.Filter = view.Filter
}
}
}
opts, err := getTaskFilterOptsFromCollection(tf, view)
if err != nil {
return nil, 0, 0, err
}
opts.search = search
opts.page = page
opts.perPage = perPage
if view != nil {
var hasOrderByPosition bool
for _, param := range opts.sortby {
if param.sortBy == taskPropertyPosition {
hasOrderByPosition = true
break
}
}
if !hasOrderByPosition {
opts.sortby = append(opts.sortby, &sortParam{
projectViewID: view.ID,
sortBy: taskPropertyPosition,
orderBy: orderAscending,
})
}
}
shareAuth, is := a.(*LinkSharing)
if is {
project, err := GetProjectSimpleByID(s, shareAuth.ProjectID)
if err != nil {
return nil, 0, 0, err
}
return getTaskOrTasksInBuckets(s, a, []*Project{project}, view, opts)
}
projects, err := getRelevantProjectsFromCollection(s, a, tf)
if err != nil {
return nil, 0, 0, err
}
return getTaskOrTasksInBuckets(s, a, projects, view, opts)
}