From cbb7c53dd10f666a850813d2dda547de8d6df4ce Mon Sep 17 00:00:00 2001 From: kolaente Date: Tue, 7 Apr 2020 20:58:33 +0200 Subject: [PATCH] Add parsing filter query parameters into task filters Signed-off-by: kolaente --- pkg/models/error.go | 54 +++++++++++++++++++++ pkg/models/task_collection.go | 35 +++++++++++++- pkg/models/task_collection_filter.go | 72 ++++++++++++++++++++++++++++ pkg/models/task_collection_sort.go | 65 +++++++++---------------- pkg/models/tasks.go | 1 + 5 files changed, 183 insertions(+), 44 deletions(-) create mode 100644 pkg/models/task_collection_filter.go diff --git a/pkg/models/error.go b/pkg/models/error.go index 5590cd956..9ada2ba10 100644 --- a/pkg/models/error.go +++ b/pkg/models/error.go @@ -653,6 +653,60 @@ func (err ErrTaskCommentDoesNotExist) HTTPError() web.HTTPError { } } +// ErrInvalidTaskField represents an error where the provided task field is invalid +type ErrInvalidTaskField struct { + TaskField string +} + +// IsErrInvalidTaskField checks if an error is ErrInvalidTaskField. +func IsErrInvalidTaskField(err error) bool { + _, ok := err.(ErrInvalidTaskField) + return ok +} + +func (err ErrInvalidTaskField) Error() string { + return fmt.Sprintf("Task Field is invalid [TaskField: %s]", err.TaskField) +} + +// ErrCodeInvalidTaskField holds the unique world-error code of this error +const ErrCodeInvalidTaskField = 4016 + +// HTTPError holds the http error description +func (err ErrInvalidTaskField) HTTPError() web.HTTPError { + return web.HTTPError{ + HTTPCode: http.StatusBadRequest, + Code: ErrCodeInvalidTaskField, + Message: fmt.Sprintf("The task field '%s' is invalid.", err.TaskField), + } +} + +// ErrInvalidTaskFilterComparator represents an error where the provided task field is invalid +type ErrInvalidTaskFilterComparator struct { + Comparator taskFilterComparator +} + +// IsErrInvalidTaskFilterComparator checks if an error is ErrInvalidTaskFilterComparator. +func IsErrInvalidTaskFilterComparator(err error) bool { + _, ok := err.(ErrInvalidTaskFilterComparator) + return ok +} + +func (err ErrInvalidTaskFilterComparator) Error() string { + return fmt.Sprintf("Task filter comparator is invalid [Comparator: %s]", err.Comparator) +} + +// ErrCodeInvalidTaskFilterComparator holds the unique world-error code of this error +const ErrCodeInvalidTaskFilterComparator = 4017 + +// HTTPError holds the http error description +func (err ErrInvalidTaskFilterComparator) HTTPError() web.HTTPError { + return web.HTTPError{ + HTTPCode: http.StatusBadRequest, + Code: ErrCodeInvalidTaskFilterComparator, + Message: fmt.Sprintf("The task filter comparator '%s' is invalid.", err.Comparator), + } +} + // ================= // Namespace errors // ================= diff --git a/pkg/models/task_collection.go b/pkg/models/task_collection.go index b6b5f11c9..b8a8fcf60 100644 --- a/pkg/models/task_collection.go +++ b/pkg/models/task_collection.go @@ -43,6 +43,9 @@ type TaskCollection struct { // The value of the field name to filter by FilterValue []string `query:"filter_value"` FilterValueArr []string `query:"filter_value[]"` + // The comparator for field and value + FilterComparator []string `query:"filter_comparator"` + FilterComparatorArr []string `query:"filter_comparator[]"` web.CRUDable `xorm:"-" json:"-"` web.Rights `xorm:"-" json:"-"` @@ -70,7 +73,7 @@ func validateTaskField(fieldName string) error { taskPropertyUpdated: return nil } - return ErrInvalidSortParam{SortBy: sp.sortBy} + return ErrInvalidTaskField{TaskField: fieldName} } @@ -110,6 +113,10 @@ func (tf *TaskCollection) ReadAll(a web.Auth, search string, page int, perPage i tf.FilterValue = append(tf.FilterValue, tf.FilterValueArr...) } + if len(tf.FilterComparatorArr) > 0 { + tf.FilterValue = append(tf.FilterValue, tf.FilterComparatorArr...) + } + var sort = make([]*sortParam, 0, len(tf.SortBy)) for i, s := range tf.SortBy { param := &sortParam{ @@ -128,6 +135,31 @@ func (tf *TaskCollection) ReadAll(a web.Auth, search string, page int, perPage i sort = append(sort, param) } + var filters = make([]*taskFilter, 0, len(tf.FilterBy)) + for i, f := range tf.FilterBy { + filter := &taskFilter{ + field: f, + } + + if len(tf.FilterValue) > i { + filter.value = tf.FilterValue[i] + } + + if len(tf.FilterComparator) > i { + filter.comparator, err = getFilterComparatorFromString(tf.FilterComparator[i]) + if err != nil { + return nil, 0, 0, err + } + } + + err = filter.validate() + if err != nil { + return nil, 0, 0, err + } + + filters = append(filters, filter) + } + taskopts := &taskOptions{ search: search, startDate: time.Unix(tf.StartDateSortUnix, 0), @@ -135,6 +167,7 @@ func (tf *TaskCollection) ReadAll(a web.Auth, search string, page int, perPage i page: page, perPage: perPage, sortby: sort, + filters: filters, } shareAuth, is := a.(*LinkSharing) diff --git a/pkg/models/task_collection_filter.go b/pkg/models/task_collection_filter.go new file mode 100644 index 000000000..0a3caa948 --- /dev/null +++ b/pkg/models/task_collection_filter.go @@ -0,0 +1,72 @@ +// Copyright 2020 Vikunja and contriubtors. All rights reserved. +// +// This file is part of Vikunja. +// +// Vikunja is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Vikunja 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Vikunja. If not, see . + +package models + +type taskFilterComparator string + +const ( + taskFilterComparatorInvalid taskFilterComparator = "invalid" + + taskFilterComparatorEquals taskFilterComparator = "=" + taskFilterComparatorGreater taskFilterComparator = ">" + taskFilterComparatorGreateEquals taskFilterComparator = ">=" + taskFilterComparatorLess taskFilterComparator = "<" + taskFilterComparatorLessEquals taskFilterComparator = "<=" +) + +type taskFilter struct { + field string + value string + comparator taskFilterComparator +} + +func (tf *taskFilter) validate() (err error) { + err = validateTaskField(tf.field) + if err != nil { + return + } + + switch tf.comparator { + case + taskFilterComparatorEquals, + taskFilterComparatorGreater, + taskFilterComparatorGreateEquals, + taskFilterComparatorLess, + taskFilterComparatorLessEquals: + return nil + default: + return ErrInvalidTaskFilterComparator{Comparator: tf.comparator} + } +} + +func getFilterComparatorFromString(comparator string) (taskFilterComparator, error) { + switch comparator { + case "equals": + return taskFilterComparatorEquals, nil + case "greater": + return taskFilterComparatorGreater, nil + case "greater_equals": + return taskFilterComparatorGreateEquals, nil + case "less": + return taskFilterComparatorLess, nil + case "less_equals": + return taskFilterComparatorLessEquals, nil + default: + return taskFilterComparatorInvalid, ErrInvalidTaskFilterComparator{Comparator: taskFilterComparator(comparator)} + } +} diff --git a/pkg/models/task_collection_sort.go b/pkg/models/task_collection_sort.go index a668e0886..16e5fc59a 100644 --- a/pkg/models/task_collection_sort.go +++ b/pkg/models/task_collection_sort.go @@ -35,23 +35,23 @@ type ( ) const ( - taskPropertyID sortProperty = "id" - taskPropertyText sortProperty = "text" - taskPropertyDescription sortProperty = "description" - taskPropertyDone sortProperty = "done" - taskPropertyDoneAtUnix sortProperty = "done_at_unix" - taskPropertyDueDateUnix sortProperty = "due_date_unix" - taskPropertyCreatedByID sortProperty = "created_by_id" - taskPropertyListID sortProperty = "list_id" - taskPropertyRepeatAfter sortProperty = "repeat_after" - taskPropertyPriority sortProperty = "priority" - taskPropertyStartDateUnix sortProperty = "start_date_unix" - taskPropertyEndDateUnix sortProperty = "end_date_unix" - taskPropertyHexColor sortProperty = "hex_color" - taskPropertyPercentDone sortProperty = "percent_done" - taskPropertyUID sortProperty = "uid" - taskPropertyCreated sortProperty = "created" - taskPropertyUpdated sortProperty = "updated" + taskPropertyID string = "id" + taskPropertyText string = "text" + taskPropertyDescription string = "description" + taskPropertyDone string = "done" + taskPropertyDoneAtUnix string = "done_at_unix" + taskPropertyDueDateUnix string = "due_date_unix" + taskPropertyCreatedByID string = "created_by_id" + taskPropertyListID string = "list_id" + taskPropertyRepeatAfter string = "repeat_after" + taskPropertyPriority string = "priority" + taskPropertyStartDateUnix string = "start_date_unix" + taskPropertyEndDateUnix string = "end_date_unix" + taskPropertyHexColor string = "hex_color" + taskPropertyPercentDone string = "percent_done" + taskPropertyUID string = "uid" + taskPropertyCreated string = "created" + taskPropertyUpdated string = "updated" ) func (p sortProperty) String() string { @@ -82,28 +82,7 @@ func (sp *sortParam) validate() error { if sp.orderBy != orderDescending && sp.orderBy != orderAscending { return ErrInvalidSortOrder{OrderBy: sp.orderBy} } - switch sp.sortBy { - case - taskPropertyID, - taskPropertyText, - taskPropertyDescription, - taskPropertyDone, - taskPropertyDoneAtUnix, - taskPropertyDueDateUnix, - taskPropertyCreatedByID, - taskPropertyListID, - taskPropertyRepeatAfter, - taskPropertyPriority, - taskPropertyStartDateUnix, - taskPropertyEndDateUnix, - taskPropertyHexColor, - taskPropertyPercentDone, - taskPropertyUID, - taskPropertyCreated, - taskPropertyUpdated: - return nil - } - return ErrInvalidSortParam{SortBy: sp.sortBy} + return validateTaskField(string(sp.sortBy)) } type taskComparator func(lhs, rhs *Task) int64 @@ -168,7 +147,7 @@ func mustMakeComparator(fieldName string) taskComparator { // This is a map of properties that can be sorted by // and their appropriate comparator function. // The comparator function sorts in ascending mode. -var propertyComparators = map[sortProperty]taskComparator{ +var propertyComparators = map[string]taskComparator{ taskPropertyID: mustMakeComparator("ID"), taskPropertyText: mustMakeComparator("Text"), taskPropertyDescription: mustMakeComparator("Description"), @@ -208,13 +187,13 @@ func sortTasks(tasks []*Task, by []*sortParam) { // If we would not do this, we would get a different order for items with the same content every time // the slice is sorted. To circumvent this, we always order at least by ID. if len(by) == 0 || - (len(by) > 0 && by[len(by)-1].sortBy != taskPropertyID) { // Don't sort by ID last if the id parameter is already passed as the last parameter. - by = append(by, &sortParam{sortBy: taskPropertyID, orderBy: orderAscending}) + (len(by) > 0 && by[len(by)-1].sortBy != sortProperty(taskPropertyID)) { // Don't sort by ID last if the id parameter is already passed as the last parameter. + by = append(by, &sortParam{sortBy: sortProperty(taskPropertyID), orderBy: orderAscending}) } comparators := make([]taskComparator, 0, len(by)) for _, param := range by { - comparator, ok := propertyComparators[param.sortBy] + comparator, ok := propertyComparators[string(param.sortBy)] if !ok { panic("No suitable comparator for sortBy found! Param was " + param.sortBy) } diff --git a/pkg/models/tasks.go b/pkg/models/tasks.go index 54dc764f3..8b245bba1 100644 --- a/pkg/models/tasks.go +++ b/pkg/models/tasks.go @@ -116,6 +116,7 @@ type taskOptions struct { page int perPage int sortby []*sortParam + filters []*taskFilter } // ReadAll is a dummy function to still have that endpoint documented