1
0

feat(filters): pass timezone down when filtering with relative date math

Resolves https://community.vikunja.io/t/my-vikunja-instance-creates-tasks-with-due-date-time-of-9am-for-tasks-with-the-word-today-word-in-it/2105/8
This commit is contained in:
kolaente
2024-03-11 16:13:42 +01:00
parent 6fc3d1e98f
commit a66e26678e
10 changed files with 65 additions and 19 deletions

View File

@ -109,6 +109,7 @@ func getDefaultBucketID(s *xorm.Session, project *Project) (bucketID int64, err
// @Param per_page query int false "The maximum number of tasks per bucket per page. This parameter is limited by the configured maximum of items per page."
// @Param s query string false "Search tasks by task text."
// @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`."
// @Success 200 {array} models.Bucket "The buckets with their tasks"
// @Failure 500 {object} models.Message "Internal server error"
@ -197,7 +198,7 @@ func (b *Bucket) ReadAll(s *xorm.Session, auth web.Auth, search string, page int
} else {
filterString = "(" + originalFilter + ") && bucket_id = " + strconv.FormatInt(id, 10)
}
opts.parsedFilters, err = getTaskFiltersFromFilterString(filterString)
opts.parsedFilters, err = getTaskFiltersFromFilterString(filterString, opts.filterTimezone)
if err != nil {
return
}

View File

@ -33,7 +33,10 @@ type TaskCollection struct {
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 of the feature.
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:"filter_timezone"`
// If set to true, the result will also include null values
FilterIncludeNulls bool `query:"filter_include_nulls" json:"filter_include_nulls"`
@ -103,9 +106,10 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskSearchOption
sortby: sort,
filterIncludeNulls: tf.FilterIncludeNulls,
filter: tf.Filter,
filterTimezone: tf.FilterTimezone,
}
opts.parsedFilters, err = getTaskFiltersFromFilterString(tf.Filter)
opts.parsedFilters, err = getTaskFiltersFromFilterString(tf.Filter, tf.FilterTimezone)
return opts, err
}
@ -122,6 +126,7 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection) (opts *taskSearchOption
// @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"

View File

@ -92,7 +92,7 @@ func parseTimeFromUserInput(timeString string) (value time.Time, err error) {
return value.In(config.GetTimeZone()), err
}
func parseFilterFromExpression(f fexpr.ExprGroup) (filter *taskFilter, err error) {
func parseFilterFromExpression(f fexpr.ExprGroup, loc *time.Location) (filter *taskFilter, err error) {
filter = &taskFilter{
join: filterConcatAnd,
}
@ -112,7 +112,7 @@ func parseFilterFromExpression(f fexpr.ExprGroup) (filter *taskFilter, err error
case []fexpr.ExprGroup:
values := make([]*taskFilter, 0, len(v))
for _, expression := range v {
subfilter, err := parseFilterFromExpression(expression)
subfilter, err := parseFilterFromExpression(expression, loc)
if err != nil {
return nil, err
}
@ -132,7 +132,7 @@ func parseFilterFromExpression(f fexpr.ExprGroup) (filter *taskFilter, err error
if filter.field == "project" {
filter.field = "project_id"
}
reflectValue, filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, value)
reflectValue, filter.value, err = getNativeValueForTaskField(filter.field, filter.comparator, value, loc)
if err != nil {
return nil, ErrInvalidTaskFilterValue{
Value: filter.field,
@ -146,7 +146,7 @@ func parseFilterFromExpression(f fexpr.ExprGroup) (filter *taskFilter, err error
return filter, nil
}
func getTaskFiltersFromFilterString(filter string) (filters []*taskFilter, err error) {
func getTaskFiltersFromFilterString(filter string, filterTimezone string) (filters []*taskFilter, err error) {
if filter == "" {
return
@ -174,9 +174,17 @@ func getTaskFiltersFromFilterString(filter string) (filters []*taskFilter, err e
}
}
var loc *time.Location
if filterTimezone != "" {
loc, err = time.LoadLocation(filterTimezone)
if err != nil {
return
}
}
filters = make([]*taskFilter, 0, len(parsedFilter))
for _, f := range parsedFilter {
parsedFilter, err := parseFilterFromExpression(f)
parsedFilter, err := parseFilterFromExpression(f, loc)
if err != nil {
return nil, err
}
@ -230,7 +238,12 @@ func getFilterComparatorFromOp(op fexpr.SignOp) (taskFilterComparator, error) {
}
}
func getValueForField(field reflect.StructField, rawValue string) (value interface{}, err error) {
func getValueForField(field reflect.StructField, rawValue string, loc *time.Location) (value interface{}, err error) {
if loc == nil {
loc = config.GetTimeZone()
}
switch field.Type.Kind() {
case reflect.Int64:
value, err = strconv.ParseInt(rawValue, 10, 64)
@ -245,7 +258,7 @@ func getValueForField(field reflect.StructField, rawValue string) (value interfa
var t datemath.Expression
t, err = datemath.Parse(rawValue)
if err == nil {
value = t.Time(datemath.WithLocation(config.GetTimeZone()))
value = t.Time(datemath.WithLocation(config.GetTimeZone())).In(loc)
} else {
value, err = parseTimeFromUserInput(rawValue)
}
@ -273,7 +286,7 @@ func getValueForField(field reflect.StructField, rawValue string) (value interfa
return
}
func getNativeValueForTaskField(fieldName string, comparator taskFilterComparator, value string) (reflectField *reflect.StructField, nativeValue interface{}, err error) {
func getNativeValueForTaskField(fieldName string, comparator taskFilterComparator, value string, loc *time.Location) (reflectField *reflect.StructField, nativeValue interface{}, err error) {
realFieldName := strings.ReplaceAll(strcase.ToCamel(fieldName), "Id", "ID")
@ -299,7 +312,7 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato
vals := strings.Split(value, ",")
valueSlice := []interface{}{}
for _, val := range vals {
v, err := getValueForField(field, val)
v, err := getValueForField(field, val, loc)
if err != nil {
return nil, nil, err
}
@ -308,6 +321,6 @@ func getNativeValueForTaskField(fieldName string, comparator taskFilterComparato
return nil, valueSlice, nil
}
val, err := getValueForField(field, value)
val, err := getValueForField(field, value, loc)
return &field, val, err
}

View File

@ -174,6 +174,7 @@ type taskSearchOptions struct {
parsedFilters []*taskFilter
filterIncludeNulls bool
filter string
filterTimezone string
projectIDs []int64
}