checkoff 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.circleci/config.yml +1 -0
- data/Gemfile.lock +3 -3
- data/coverage/.last_run.json +2 -2
- data/lib/checkoff/internal/search_url_parser.rb +220 -0
- data/lib/checkoff/internal/task_selector_evaluator.rb +173 -0
- data/lib/checkoff/task_searches.rb +89 -0
- data/lib/checkoff/task_selectors.rb +9 -41
- data/lib/checkoff/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b6a571f1d0bef00f4e030c22ea9a1cfae923c5253f8949deeaa24d537709c723
|
4
|
+
data.tar.gz: 1545211bc1d508f12fd26769d41201818ea280714c96c685ca31148950ba56f9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '08f83449b8628676c61c2afc08a9723b0639572e42c646e2fd81dc8571f446ac5a5e46ccbc58ce3ea2dddc5538ac7736081f5efde01bf46fc3b2111872f7fa93'
|
7
|
+
data.tar.gz: ddefd4b5d02bb05a610e0ead08c3126c76f9373ceb6a339ac61777a997de753b25491447df52c4127b96bf4fb8dac01f9e57183d82da3c61f121c629f0e1a757
|
data/.circleci/config.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -12,7 +12,7 @@ GIT
|
|
12
12
|
PATH
|
13
13
|
remote: .
|
14
14
|
specs:
|
15
|
-
checkoff (0.
|
15
|
+
checkoff (0.19.1)
|
16
16
|
activesupport
|
17
17
|
asana (> 0.10.0)
|
18
18
|
cache_method
|
@@ -63,7 +63,7 @@ GEM
|
|
63
63
|
faraday_middleware
|
64
64
|
multi_json
|
65
65
|
gli (2.21.0)
|
66
|
-
i18n (1.
|
66
|
+
i18n (1.12.0)
|
67
67
|
concurrent-ruby (~> 1.0)
|
68
68
|
imagen (0.1.8)
|
69
69
|
parser (>= 2.5, != 2.5.1.1)
|
@@ -138,7 +138,7 @@ GEM
|
|
138
138
|
simplecov-lcov (0.8.0)
|
139
139
|
simplecov_json_formatter (0.1.3)
|
140
140
|
tomlrb (2.0.3)
|
141
|
-
tzinfo (2.0.
|
141
|
+
tzinfo (2.0.5)
|
142
142
|
concurrent-ruby (~> 1.0)
|
143
143
|
undercover (0.4.3)
|
144
144
|
imagen (>= 0.1.8)
|
data/coverage/.last_run.json
CHANGED
@@ -0,0 +1,220 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module Checkoff
|
6
|
+
module Internal
|
7
|
+
# base class for handling different custom_field_#{gid}.variant params
|
8
|
+
class CustomFieldVariant
|
9
|
+
def initialize(gid, remaining_params)
|
10
|
+
@gid = gid
|
11
|
+
@remaining_params = remaining_params
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
attr_reader :gid, :remaining_params
|
17
|
+
end
|
18
|
+
|
19
|
+
# custom_field_#{gid}.variant = 'less_than'
|
20
|
+
class LessThanCustomFieldVariant < CustomFieldVariant
|
21
|
+
def convert
|
22
|
+
max_param = "custom_field_#{gid}.max"
|
23
|
+
case remaining_params.keys
|
24
|
+
when [max_param]
|
25
|
+
convert_single_custom_field_less_than_params_max_param(max_param)
|
26
|
+
else
|
27
|
+
raise "Teach me how to handle #{remaining_params}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def convert_single_custom_field_less_than_params_max_param(max_param)
|
34
|
+
max_values = remaining_params.fetch(max_param)
|
35
|
+
unless max_values.length == 1
|
36
|
+
raise "Teach me how to handle these remaining keys for #{max_param}: #{remaining_params}"
|
37
|
+
end
|
38
|
+
|
39
|
+
max_value = max_values[0]
|
40
|
+
empty_task_selector = []
|
41
|
+
[{ "custom_fields.#{gid}.less_than" => max_value }, empty_task_selector]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# custom_field_#{gid}.variant = 'is_not'
|
46
|
+
class IsNotCustomFieldVariant < CustomFieldVariant
|
47
|
+
def convert
|
48
|
+
case remaining_params.keys
|
49
|
+
when ["custom_field_#{gid}.selected_options"]
|
50
|
+
convert_single_custom_field_is_not_params_selected_options
|
51
|
+
else
|
52
|
+
raise "Teach me how to handle #{remaining_params}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def convert_single_custom_field_is_not_params_selected_options
|
59
|
+
selected_options = remaining_params.fetch("custom_field_#{gid}.selected_options")
|
60
|
+
raise "Teach me how to handle #{remaining_params}" unless selected_options.length == 1
|
61
|
+
|
62
|
+
[{ "custom_fields.#{gid}.is_set" => 'true' },
|
63
|
+
['not',
|
64
|
+
['custom_field_gid_value_contains_any_gid',
|
65
|
+
gid,
|
66
|
+
selected_options.fetch(0).split('~')]]]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# custom_field_#{gid}.variant = 'no_value'
|
71
|
+
class NoValueCustomFieldVariant < CustomFieldVariant
|
72
|
+
def convert
|
73
|
+
unless remaining_params.length.zero?
|
74
|
+
raise "Teach me how to handle these remaining keys for #{variant_key}: #{remaining_params}"
|
75
|
+
end
|
76
|
+
|
77
|
+
empty_task_selector = []
|
78
|
+
[{ "custom_fields.#{gid}.is_set" => 'false' }, empty_task_selector]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# custom_field_#{gid}.variant = 'is'
|
83
|
+
class IsCustomFieldVariant < CustomFieldVariant
|
84
|
+
def convert
|
85
|
+
unless remaining_params.length == 1
|
86
|
+
raise "Teach me how to handle these remaining keys for #{variant_key}: #{remaining_params}"
|
87
|
+
end
|
88
|
+
|
89
|
+
key, values = remaining_params.to_a[0]
|
90
|
+
convert_custom_field_is_arg(key, values)
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def convert_custom_field_is_arg(key, values)
|
96
|
+
empty_task_selector = []
|
97
|
+
|
98
|
+
if key.end_with? '.selected_options'
|
99
|
+
raise "Too many values found for #{key}: #{values}" if values.length != 1
|
100
|
+
|
101
|
+
return [{ "custom_fields.#{gid}.value" => values[0] },
|
102
|
+
empty_task_selector]
|
103
|
+
end
|
104
|
+
|
105
|
+
raise "Teach me how to handle #{key} = #{values}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Convert custom field parameters from an Asana search URL into
|
110
|
+
# API search arguments and Checkoff task selectors
|
111
|
+
class CustomFieldParamConverter
|
112
|
+
def initialize(custom_field_params:)
|
113
|
+
@custom_field_params = custom_field_params
|
114
|
+
end
|
115
|
+
|
116
|
+
def convert
|
117
|
+
args = {}
|
118
|
+
task_selector = []
|
119
|
+
by_custom_field.each do |gid, single_custom_field_params|
|
120
|
+
new_args, new_task_selector = convert_single_custom_field_params(gid,
|
121
|
+
single_custom_field_params)
|
122
|
+
args, task_selector = merge_args_and_task_selectors(args, new_args,
|
123
|
+
task_selector, new_task_selector)
|
124
|
+
end
|
125
|
+
[args, task_selector]
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def by_custom_field
|
131
|
+
custom_field_params.group_by do |key, _value|
|
132
|
+
gid_from_custom_field_key(key)
|
133
|
+
end.transform_values(&:to_h)
|
134
|
+
end
|
135
|
+
|
136
|
+
def merge_args_and_task_selectors(args, new_args, task_selector, new_task_selector)
|
137
|
+
args = args.merge(new_args)
|
138
|
+
return [args, task_selector] if new_task_selector == []
|
139
|
+
|
140
|
+
raise 'Teach me how to merge task selectors' unless task_selector == []
|
141
|
+
|
142
|
+
task_selector = new_task_selector
|
143
|
+
|
144
|
+
[args, task_selector]
|
145
|
+
end
|
146
|
+
|
147
|
+
VARIANTS = {
|
148
|
+
'is' => IsCustomFieldVariant,
|
149
|
+
'no_value' => NoValueCustomFieldVariant,
|
150
|
+
'is_not' => IsNotCustomFieldVariant,
|
151
|
+
'less_than' => LessThanCustomFieldVariant,
|
152
|
+
}.freeze
|
153
|
+
|
154
|
+
def convert_single_custom_field_params(gid, single_custom_field_params)
|
155
|
+
variant_key = "custom_field_#{gid}.variant"
|
156
|
+
variant = single_custom_field_params.fetch(variant_key)
|
157
|
+
remaining_params = single_custom_field_params.reject { |k, _v| k == variant_key }
|
158
|
+
raise "Teach me how to handle #{variant_key} = #{variant}" unless variant.length == 1
|
159
|
+
|
160
|
+
variant_class = VARIANTS[variant[0]]
|
161
|
+
return variant_class.new(gid, remaining_params).convert unless variant_class.nil?
|
162
|
+
|
163
|
+
raise "Teach me how to handle #{variant_key} = #{variant}"
|
164
|
+
end
|
165
|
+
|
166
|
+
def gid_from_custom_field_key(key)
|
167
|
+
key.split('_')[2].split('.')[0]
|
168
|
+
end
|
169
|
+
|
170
|
+
attr_reader :custom_field_params
|
171
|
+
end
|
172
|
+
|
173
|
+
# Parse Asana search URLs into parameters suitable to pass into
|
174
|
+
# the /workspaces/{workspace_gid}/tasks/search endpoint
|
175
|
+
class SearchUrlParser
|
176
|
+
def initialize(_deps = {}); end
|
177
|
+
|
178
|
+
def convert_params(url)
|
179
|
+
url_params = CGI.parse(URI.parse(url).query)
|
180
|
+
custom_field_params, regular_url_params = partition_url_params(url_params)
|
181
|
+
custom_field_args, task_selector = convert_custom_field_params(custom_field_params)
|
182
|
+
regular_url_args = convert_regular_params(regular_url_params)
|
183
|
+
[custom_field_args.merge(regular_url_args), task_selector]
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
def convert_regular_params(regular_url_params)
|
189
|
+
regular_url_params.to_a.map do |key, values|
|
190
|
+
convert_arg(key, values)
|
191
|
+
end.to_h
|
192
|
+
end
|
193
|
+
|
194
|
+
def convert_custom_field_params(custom_field_params)
|
195
|
+
CustomFieldParamConverter.new(custom_field_params: custom_field_params).convert
|
196
|
+
end
|
197
|
+
|
198
|
+
def partition_url_params(url_params)
|
199
|
+
url_params.to_a.partition do |key, _values|
|
200
|
+
key.start_with? 'custom_field_'
|
201
|
+
end.map(&:to_h)
|
202
|
+
end
|
203
|
+
|
204
|
+
# https://developers.asana.com/docs/search-tasks-in-a-workspace
|
205
|
+
def convert_arg(key, values)
|
206
|
+
case key
|
207
|
+
when 'any_projects.ids'
|
208
|
+
['projects.any', values.join(',')]
|
209
|
+
when 'completion'
|
210
|
+
raise "Teach me how to handle #{key} = #{values}" if values.length != 1
|
211
|
+
|
212
|
+
value = values.fetch(0)
|
213
|
+
raise "Teach me how to handle #{key} = #{values}" if value != 'incomplete'
|
214
|
+
|
215
|
+
['completed', false]
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Checkoff
|
4
|
+
# Base class to evaluate a task selector function given fully evaluated arguments
|
5
|
+
class FunctionEvaluator
|
6
|
+
def initialize(task_selector:)
|
7
|
+
@task_selector = task_selector
|
8
|
+
end
|
9
|
+
|
10
|
+
def evaluate_arg?(_index)
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def fn?(object, fn_name)
|
17
|
+
object.is_a?(Array) && !object.empty? && [fn_name, fn_name.to_s].include?(object[0])
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :task_selector
|
21
|
+
end
|
22
|
+
|
23
|
+
# :not function
|
24
|
+
class NotFunctionEvaluator < FunctionEvaluator
|
25
|
+
def matches?
|
26
|
+
fn?(task_selector, :not)
|
27
|
+
end
|
28
|
+
|
29
|
+
def evaluate(_task, subvalue)
|
30
|
+
!subvalue
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# :nil? function
|
35
|
+
class NilPFunctionEvaluator < FunctionEvaluator
|
36
|
+
def matches?
|
37
|
+
fn?(task_selector, :nil?)
|
38
|
+
end
|
39
|
+
|
40
|
+
def evaluate(_task, subvalue)
|
41
|
+
subvalue.nil?
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# :tag function
|
46
|
+
class TagPFunctionEvaluator < FunctionEvaluator
|
47
|
+
def matches?
|
48
|
+
fn?(task_selector, :tag)
|
49
|
+
end
|
50
|
+
|
51
|
+
def evaluate_arg?(_index)
|
52
|
+
false
|
53
|
+
end
|
54
|
+
|
55
|
+
def evaluate(task, tag_name)
|
56
|
+
task.tags.map(&:name).include? tag_name
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# :custom_field_value function
|
61
|
+
class CustomFieldValueFunctionEvaluator < FunctionEvaluator
|
62
|
+
def matches?
|
63
|
+
fn?(task_selector, :custom_field_value)
|
64
|
+
end
|
65
|
+
|
66
|
+
def evaluate_arg?(_index)
|
67
|
+
false
|
68
|
+
end
|
69
|
+
|
70
|
+
def evaluate(task, custom_field_name)
|
71
|
+
custom_fields = task.custom_fields
|
72
|
+
if custom_fields.nil?
|
73
|
+
raise "custom fields not found on task - did you add 'custom_field' in your extra_fields argument?"
|
74
|
+
end
|
75
|
+
|
76
|
+
custom_field = custom_fields.find { |field| field.fetch('name') == custom_field_name }
|
77
|
+
return nil if custom_field.nil?
|
78
|
+
|
79
|
+
custom_field['display_value']
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# :custom_field_gid_value_contains_any_gid function
|
84
|
+
class CustomFieldGidValueContainsAnyGidFunctionEvaluator < FunctionEvaluator
|
85
|
+
def matches?
|
86
|
+
fn?(task_selector, :custom_field_gid_value_contains_any_gid)
|
87
|
+
end
|
88
|
+
|
89
|
+
def evaluate_arg?(_index)
|
90
|
+
false
|
91
|
+
end
|
92
|
+
|
93
|
+
def evaluate(task, custom_field_gid, custom_field_values_gids)
|
94
|
+
actual_custom_field_values_gids = pull_custom_field_values_gids(task, custom_field_gid)
|
95
|
+
|
96
|
+
(custom_field_values_gids - actual_custom_field_values_gids).empty?
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def pull_custom_field_or_raise(task, custom_field_gid)
|
102
|
+
custom_fields = task.custom_fields
|
103
|
+
if custom_fields.nil?
|
104
|
+
raise "Could not find custom_fields under task (was 'custom_fields' included in 'extra_fields'?)"
|
105
|
+
end
|
106
|
+
|
107
|
+
matched_custom_field = custom_fields.find { |data| data.fetch('gid') == custom_field_gid }
|
108
|
+
raise "Could not find custom field with gid #{custom_field_gid}" if matched_custom_field.nil?
|
109
|
+
|
110
|
+
matched_custom_field
|
111
|
+
end
|
112
|
+
|
113
|
+
def pull_custom_field_values_gids(task, custom_field_gid)
|
114
|
+
matched_custom_field = pull_custom_field_or_raise(task, custom_field_gid)
|
115
|
+
|
116
|
+
enum_value = matched_custom_field.fetch('enum_value')
|
117
|
+
actual_custom_field_values_gids = []
|
118
|
+
unless enum_value.nil?
|
119
|
+
if enum_value.fetch('enabled') == false
|
120
|
+
raise "Unexpected enabled value on custom field: #{matched_custom_field}"
|
121
|
+
end
|
122
|
+
|
123
|
+
actual_custom_field_values_gids = [enum_value.fetch('gid')]
|
124
|
+
end
|
125
|
+
actual_custom_field_values_gids
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Evaluator task selectors against a task
|
130
|
+
class TaskSelectorEvaluator
|
131
|
+
def initialize(task:)
|
132
|
+
@task = task
|
133
|
+
end
|
134
|
+
|
135
|
+
FUNCTION_EVALUTORS = [
|
136
|
+
NotFunctionEvaluator,
|
137
|
+
NilPFunctionEvaluator,
|
138
|
+
TagPFunctionEvaluator,
|
139
|
+
CustomFieldValueFunctionEvaluator,
|
140
|
+
CustomFieldGidValueContainsAnyGidFunctionEvaluator,
|
141
|
+
].freeze
|
142
|
+
|
143
|
+
def evaluate(task_selector)
|
144
|
+
return true if task_selector == []
|
145
|
+
|
146
|
+
FUNCTION_EVALUTORS.each do |evaluator_class|
|
147
|
+
evaluator = evaluator_class.new(task_selector: task_selector)
|
148
|
+
|
149
|
+
next unless evaluator.matches?
|
150
|
+
|
151
|
+
return try_this_evaluator(task_selector, evaluator)
|
152
|
+
end
|
153
|
+
|
154
|
+
raise "Syntax issue trying to handle #{task_selector}"
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
def try_this_evaluator(task_selector, evaluator)
|
160
|
+
evaluated_args = task_selector[1..].map.with_index do |item, index|
|
161
|
+
if evaluator.evaluate_arg?(index)
|
162
|
+
evaluate(item)
|
163
|
+
else
|
164
|
+
item
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
evaluator.evaluate(task, *evaluated_args)
|
169
|
+
end
|
170
|
+
|
171
|
+
attr_reader :task, :task_selector
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require 'forwardable'
|
6
|
+
require 'cache_method'
|
7
|
+
require_relative 'internal/config_loader'
|
8
|
+
require_relative 'workspaces'
|
9
|
+
require_relative 'projects'
|
10
|
+
require_relative 'clients'
|
11
|
+
require_relative 'task_selectors'
|
12
|
+
require 'asana/resource_includes/collection'
|
13
|
+
require 'asana/resource_includes/response_helper'
|
14
|
+
|
15
|
+
require 'checkoff/internal/search_url_parser'
|
16
|
+
|
17
|
+
# https://developers.asana.com/docs/task-searches
|
18
|
+
module Checkoff
|
19
|
+
# Run task searches against the Asana API
|
20
|
+
class TaskSearches
|
21
|
+
MINUTE = 60
|
22
|
+
HOUR = MINUTE * 60
|
23
|
+
DAY = 24 * HOUR
|
24
|
+
REALLY_LONG_CACHE_TIME = HOUR * 1
|
25
|
+
LONG_CACHE_TIME = MINUTE * 15
|
26
|
+
SHORT_CACHE_TIME = MINUTE
|
27
|
+
|
28
|
+
include Asana::Resources::ResponseHelper
|
29
|
+
|
30
|
+
def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana),
|
31
|
+
workspaces: Checkoff::Workspaces.new(config: config),
|
32
|
+
task_selectors: Checkoff::TaskSelectors.new(config: config),
|
33
|
+
projects: Checkoff::Projects.new(config: config),
|
34
|
+
clients: Checkoff::Clients.new(config: config),
|
35
|
+
client: clients.client,
|
36
|
+
search_url_parser: Checkoff::Internal::SearchUrlParser.new,
|
37
|
+
asana_resources_collection_class: Asana::Resources::Collection)
|
38
|
+
@workspaces = workspaces
|
39
|
+
@task_selectors = task_selectors
|
40
|
+
@projects = projects
|
41
|
+
@client = client
|
42
|
+
@search_url_parser = search_url_parser
|
43
|
+
@asana_resources_collection_class = asana_resources_collection_class
|
44
|
+
end
|
45
|
+
|
46
|
+
def task_search(workspace_name, url, extra_fields: [])
|
47
|
+
workspace = workspaces.workspace_or_raise(workspace_name)
|
48
|
+
api_params, task_selector = @search_url_parser.convert_params(url)
|
49
|
+
path = "/workspaces/#{workspace.gid}/tasks/search"
|
50
|
+
options = calculate_api_options(extra_fields)
|
51
|
+
tasks = @asana_resources_collection_class.new(parse(client.get(path,
|
52
|
+
params: api_params,
|
53
|
+
options: options)),
|
54
|
+
type: Asana::Resources::Task,
|
55
|
+
client: client)
|
56
|
+
tasks.select { |task| task_selectors.filter_via_task_selector(task, task_selector) }
|
57
|
+
end
|
58
|
+
cache_method :task_search, LONG_CACHE_TIME
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def calculate_api_options(extra_fields)
|
63
|
+
options = projects.task_options[:options]
|
64
|
+
options[:fields] += ['custom_fields']
|
65
|
+
options[:fields] += extra_fields
|
66
|
+
options
|
67
|
+
end
|
68
|
+
|
69
|
+
# bundle exec ./task_searches.rb
|
70
|
+
# :nocov:
|
71
|
+
class << self
|
72
|
+
def run
|
73
|
+
workspace_name = ARGV[0] || raise('Please pass workspace name as first argument')
|
74
|
+
url = ARGV[1] || raise('Please pass task search URL as second argument')
|
75
|
+
task_searches = Checkoff::TaskSearches.new
|
76
|
+
task_search = task_searches.task_search(workspace_name, url)
|
77
|
+
puts "Results: #{task_search}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
# :nocov:
|
81
|
+
|
82
|
+
attr_reader :task_selectors, :projects, :workspaces, :client
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# :nocov:
|
87
|
+
abs_program_name = File.expand_path($PROGRAM_NAME)
|
88
|
+
Checkoff::TaskSearches.run if abs_program_name == __FILE__
|
89
|
+
# :nocov:
|
@@ -6,8 +6,7 @@ require 'forwardable'
|
|
6
6
|
require 'cache_method'
|
7
7
|
require 'json'
|
8
8
|
require_relative 'internal/config_loader'
|
9
|
-
require_relative '
|
10
|
-
require_relative 'clients'
|
9
|
+
require_relative 'internal/task_selector_evaluator'
|
11
10
|
|
12
11
|
# https://developers.asana.com/docs/task-selectors
|
13
12
|
|
@@ -21,49 +20,17 @@ module Checkoff
|
|
21
20
|
LONG_CACHE_TIME = MINUTE * 15
|
22
21
|
SHORT_CACHE_TIME = MINUTE
|
23
22
|
|
24
|
-
def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana)
|
25
|
-
|
26
|
-
clients: Checkoff::Clients.new(config: config),
|
27
|
-
client: clients.client)
|
28
|
-
@workspaces = workspaces
|
29
|
-
@client = client
|
23
|
+
def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana))
|
24
|
+
@config = config
|
30
25
|
end
|
31
26
|
|
32
27
|
# @param [Hash<Symbol, Object>] task_selector Filter based on
|
33
28
|
# description. Examples: {tag: 'foo'} {:not {tag: 'foo'} (:tag 'foo')
|
34
29
|
def filter_via_task_selector(task, task_selector)
|
35
|
-
|
36
|
-
|
37
|
-
return !filter_via_task_selector(task, task_selector.fetch(1)) if fn?(task_selector, :not)
|
38
|
-
|
39
|
-
return filter_via_task_selector(task, task_selector.fetch(1)).nil? if fn?(task_selector, :nil?)
|
40
|
-
|
41
|
-
return contains_tag?(task, task_selector.fetch(1)) if fn?(task_selector, :tag)
|
42
|
-
|
43
|
-
return custom_field_value(task, task_selector.fetch(1)) if fn?(task_selector, :custom_field_value)
|
44
|
-
|
45
|
-
raise "Syntax issue trying to handle #{task_selector}"
|
46
|
-
end
|
47
|
-
|
48
|
-
private
|
49
|
-
|
50
|
-
def contains_tag?(task, tag_name)
|
51
|
-
task.tags.map(&:name).include? tag_name
|
52
|
-
end
|
53
|
-
|
54
|
-
def custom_field_value(task, custom_field_name)
|
55
|
-
custom_field = task.custom_fields.find { |field| field.fetch('name') == custom_field_name }
|
56
|
-
return nil if custom_field.nil?
|
57
|
-
|
58
|
-
custom_field['display_value']
|
30
|
+
evaluator = TaskSelectorEvaluator.new(task: task)
|
31
|
+
evaluator.evaluate(task_selector)
|
59
32
|
end
|
60
33
|
|
61
|
-
def fn?(object, fn_name)
|
62
|
-
object.is_a?(Array) && !object.empty? && [fn_name, fn_name.to_s].include?(object[0])
|
63
|
-
end
|
64
|
-
|
65
|
-
attr_reader :workspaces, :client
|
66
|
-
|
67
34
|
# bundle exec ./task_selectors.rb
|
68
35
|
# :nocov:
|
69
36
|
class << self
|
@@ -84,12 +51,13 @@ module Checkoff
|
|
84
51
|
require 'checkoff/projects'
|
85
52
|
|
86
53
|
task_selectors = Checkoff::TaskSelectors.new
|
87
|
-
extra_fields = []
|
54
|
+
extra_fields = ['custom_fields']
|
88
55
|
projects = Checkoff::Projects.new
|
89
|
-
project = projects.
|
56
|
+
project = projects.project_or_raise(workspace_name, project_name)
|
90
57
|
raw_tasks = projects.tasks_from_project(project, extra_fields: extra_fields)
|
91
58
|
tasks = raw_tasks.filter { |task| task_selectors.filter_via_task_selector(task, task_selector) }
|
92
|
-
|
59
|
+
# avoid n+1 queries generating the full task formatting
|
60
|
+
puts JSON.pretty_generate(tasks.map(&:to_h))
|
93
61
|
end
|
94
62
|
end
|
95
63
|
# :nocov:
|
data/lib/checkoff/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: checkoff
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.19.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vince Broz
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-07-
|
11
|
+
date: 2022-07-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -322,11 +322,14 @@ files:
|
|
322
322
|
- lib/checkoff/custom_fields.rb
|
323
323
|
- lib/checkoff/internal/config_loader.rb
|
324
324
|
- lib/checkoff/internal/create-class.sh
|
325
|
+
- lib/checkoff/internal/search_url_parser.rb
|
326
|
+
- lib/checkoff/internal/task_selector_evaluator.rb
|
325
327
|
- lib/checkoff/my_tasks.rb
|
326
328
|
- lib/checkoff/projects.rb
|
327
329
|
- lib/checkoff/sections.rb
|
328
330
|
- lib/checkoff/subtasks.rb
|
329
331
|
- lib/checkoff/tags.rb
|
332
|
+
- lib/checkoff/task_searches.rb
|
330
333
|
- lib/checkoff/task_selectors.rb
|
331
334
|
- lib/checkoff/tasks.rb
|
332
335
|
- lib/checkoff/version.rb
|