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:
@ -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
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -174,6 +174,7 @@ type taskSearchOptions struct {
|
||||
parsedFilters []*taskFilter
|
||||
filterIncludeNulls bool
|
||||
filter string
|
||||
filterTimezone string
|
||||
projectIDs []int64
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user