api/pkg/models/task_collection_filter.go
konrad 2d4e2e452c Add task filter for lists and namespaces (#748)
Add more tests for getting namespaces

Fix namespaces not found

Fix namespaces not found

Make like the default

Update docs & fix docs

Enable searching namespaces by their ids

Enable searching lists by their ids

Enable searching labels by their ids

Enable searching by user ids

Update docs

Add namespace filter

Add task filter for lists

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/api#748
Co-Authored-By: konrad <konrad@kola-entertainments.de>
Co-Committed-By: konrad <konrad@kola-entertainments.de>
2020-12-21 23:13:15 +00:00

231 lines
6.6 KiB
Go

// 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 <https://www.gnu.org/licenses/>.
package models
import (
"fmt"
"reflect"
"strconv"
"strings"
"time"
"code.vikunja.io/api/pkg/config"
"github.com/iancoleman/strcase"
"xorm.io/xorm/schemas"
)
type taskFilterComparator string
const (
taskFilterComparatorInvalid taskFilterComparator = "invalid"
taskFilterComparatorEquals taskFilterComparator = "="
taskFilterComparatorGreater taskFilterComparator = ">"
taskFilterComparatorGreateEquals taskFilterComparator = ">="
taskFilterComparatorLess taskFilterComparator = "<"
taskFilterComparatorLessEquals taskFilterComparator = "<="
taskFilterComparatorNotEquals taskFilterComparator = "!="
taskFilterComparatorLike taskFilterComparator = "like"
taskFilterComparatorIn taskFilterComparator = "in"
)
type taskFilter struct {
field string
value interface{} // Needs to be an interface to be able to hold the field's native value
comparator taskFilterComparator
}
func getTaskFiltersByCollections(c *TaskCollection) (filters []*taskFilter, err error) {
if len(c.FilterByArr) > 0 {
c.FilterBy = append(c.FilterBy, c.FilterByArr...)
}
if len(c.FilterValueArr) > 0 {
c.FilterValue = append(c.FilterValue, c.FilterValueArr...)
}
if len(c.FilterComparatorArr) > 0 {
c.FilterComparator = append(c.FilterComparator, c.FilterComparatorArr...)
}
if c.FilterConcat != "" && c.FilterConcat != filterConcatAnd && c.FilterConcat != filterConcatOr {
return nil, ErrInvalidTaskFilterConcatinator{
Concatinator: taskFilterConcatinator(c.FilterConcat),
}
}
filters = make([]*taskFilter, 0, len(c.FilterBy))
for i, f := range c.FilterBy {
filter := &taskFilter{
field: f,
comparator: taskFilterComparatorEquals,
}
if len(c.FilterComparator) > i {
filter.comparator, err = getFilterComparatorFromString(c.FilterComparator[i])
if err != nil {
return
}
}
err = validateTaskFieldComparator(filter.comparator)
if err != nil {
return
}
// Cast the field value to its native type
if len(c.FilterValue) > i {
filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, c.FilterValue[i])
if err != nil {
return nil, ErrInvalidTaskFilterValue{
Value: filter.field,
Field: c.FilterValue[i],
}
}
}
filters = append(filters, filter)
}
return
}
func validateTaskFieldComparator(comparator taskFilterComparator) error {
switch comparator {
case
taskFilterComparatorEquals,
taskFilterComparatorGreater,
taskFilterComparatorGreateEquals,
taskFilterComparatorLess,
taskFilterComparatorLessEquals,
taskFilterComparatorNotEquals,
taskFilterComparatorLike,
taskFilterComparatorIn:
return nil
case taskFilterComparatorInvalid:
fallthrough
default:
return ErrInvalidTaskFilterComparator{Comparator: 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
case "not_equals":
return taskFilterComparatorNotEquals, nil
case "like":
return taskFilterComparatorLike, nil
case "in":
return taskFilterComparatorIn, nil
default:
return taskFilterComparatorInvalid, ErrInvalidTaskFilterComparator{Comparator: taskFilterComparator(comparator)}
}
}
func getValueForField(field reflect.StructField, rawValue string) (value interface{}, err error) {
switch field.Type.Kind() {
case reflect.Int64:
value, err = strconv.ParseInt(rawValue, 10, 64)
case reflect.Float64:
value, err = strconv.ParseFloat(rawValue, 64)
case reflect.String:
value = rawValue
case reflect.Bool:
value, err = strconv.ParseBool(rawValue)
case reflect.Struct:
if field.Type == schemas.TimeType {
value, err = time.Parse(time.RFC3339, rawValue)
value = value.(time.Time).In(config.GetTimeZone())
}
case reflect.Slice:
// If this is a slice of pointers we're dealing with some property which is a relation
// In that case we don't really care about what the actual type is, we just cast the value to an
// int64 since we need the id - yes, this assumes we only ever have int64 IDs, but this is fine.
if field.Type.Elem().Kind() == reflect.Ptr {
value, err = strconv.ParseInt(rawValue, 10, 64)
return
}
// There are probably better ways to do this - please let me know if you have one.
if field.Type.Elem().String() == "time.Time" {
value, err = time.Parse(time.RFC3339, rawValue)
value = value.(time.Time).In(config.GetTimeZone())
return
}
fallthrough
default:
panic(fmt.Errorf("unrecognized filter type %s for field %s, value %s", field.Type.String(), field.Name, value))
}
return
}
func getNativeValueForTaskField(fieldName string, comparator taskFilterComparator, value string) (nativeValue interface{}, err error) {
realFieldName := strings.ReplaceAll(strcase.ToCamel(fieldName), "Id", "ID")
if realFieldName == "Namespace" {
if comparator == taskFilterComparatorIn {
vals := strings.Split(value, ",")
valueSlice := []interface{}{}
for _, val := range vals {
v, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return nil, err
}
valueSlice = append(valueSlice, v)
}
return valueSlice, nil
}
nativeValue, err = strconv.ParseInt(value, 10, 64)
return
}
field, ok := reflect.TypeOf(&Task{}).Elem().FieldByName(realFieldName)
if !ok {
return nil, ErrInvalidTaskField{TaskField: fieldName}
}
if comparator == taskFilterComparatorIn {
vals := strings.Split(value, ",")
valueSlice := []interface{}{}
for _, val := range vals {
v, err := getValueForField(field, val)
if err != nil {
return nil, err
}
valueSlice = append(valueSlice, v)
}
return valueSlice, nil
}
return getValueForField(field, value)
}