checkoff 0.19.2 → 0.21.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b199a642ccf75e4369caeffa5fc9cd5248164dc76f23674af38ce77dc2dfb3a
4
- data.tar.gz: 70a1db49e52e64294d44b9e1b386464b5e9a9c8cf28497fab4493abc77932954
3
+ metadata.gz: d7c237ce35c02738d3170942d13d3d47d02d0fc0ed65eb78b836b7c444aed241
4
+ data.tar.gz: 8ab5d779ab9feadeeb66d12294e25f8b2baf0de32d8a917618f3adc26c77a4ea
5
5
  SHA512:
6
- metadata.gz: '0088414917271dba3b10842d25cfe151188325582fd2fd07e7832f0ab849ccb6058c342d7b3c8fc754f54ce52f639db791562f549d509b2ecd5701b2dfd52e41'
7
- data.tar.gz: b91b8209aa5b765723a742cf86e0490935667bfa06a0b16fd5a7269dd0a743d84608da35b2a8510951db4180a50ad252d4ea39c2130a98013d6e8bf160db4f93
6
+ metadata.gz: '078c32912450408243aea106d973047fabfb4db52c6267f2dd0030894a7f1b898a89acce8115da066e2b1fa2e45560286148a7aa6ccc0632d869797cf4b008aa'
7
+ data.tar.gz: 388f84ed37bc63dc68182e4b484e18fc5ab591625840c9f6a4f60fa385340cf6e7c4386cd56ee146100d3d5e99952f8d7c77e9c12c3ae9e47fbabc6b7e9e773e
data/.circleci/config.yml CHANGED
@@ -37,9 +37,9 @@ commands:
37
37
  sed -E -e 's/checkoff \([[:digit:]]+.[[:digit:]]+.[[:digit:]]+\)/checkoff (0.1.0)/g' \
38
38
  Gemfile.lock > Gemfile.lock.deversioned
39
39
  - restore_cache:
40
- key: gems-v1-{{ checksum "Gemfile.lock.deversioned" }}
40
+ key: gems-v2-{{ checksum "Gemfile.lock.deversioned" }}
41
41
  - restore_cache:
42
- key: wheels-v1-{{ checksum "requirements_dev.txt" }}
42
+ key: wheels-v2-{{ checksum "requirements_dev.txt" }}
43
43
  - run:
44
44
  name: Initialize packages
45
45
  command: |
@@ -54,11 +54,11 @@ commands:
54
54
  exit 1
55
55
  fi
56
56
  - save_cache:
57
- key: gems-v1-{{ checksum "Gemfile.lock.deversioned" }}
57
+ key: gems-v2-{{ checksum "Gemfile.lock.deversioned" }}
58
58
  paths:
59
59
  - "vendor/bundle"
60
60
  - save_cache:
61
- key: wheels-v1-{{ checksum "requirements_dev.txt" }}
61
+ key: wheels-v2-{{ checksum "requirements_dev.txt" }}
62
62
  paths:
63
63
  - "/home/circleci/.cache/pip/wheels"
64
64
  - run:
@@ -104,13 +104,8 @@ jobs:
104
104
  steps:
105
105
  - set_up_environment
106
106
  - run_with_languages:
107
- label: Test
108
- command: |
109
- bundle exec rake citest
110
- # https://github.com/bluelabsio/records-mover/blob/master/Makefile#L25
111
- git status --porcelain coverage/.last_run.json
112
- git diff coverage/.last_run.json
113
- test -z "$(git status --porcelain coverage/.last_run.json)"
107
+ label: test
108
+ command: make citest cicoverage
114
109
  # This seemed to shave 5ish% of the build time off when added
115
110
  resource_class: large
116
111
 
data/.gitignore CHANGED
@@ -57,8 +57,4 @@ requirements_dev.txt.installed
57
57
  /pkg/
58
58
  /spec/reports/
59
59
  /tmp/
60
- /coverage/.resultset.json
61
- /coverage/.resultset.json.lock
62
- /coverage/assets/
63
- /coverage/index.html
64
- /coverage/lcov/
60
+ /coverage/
data/.overcommit.yml CHANGED
@@ -17,6 +17,15 @@
17
17
  # Uncomment the following lines to make the configuration take effect.
18
18
 
19
19
  PreCommit:
20
+ # Extend default config at https://github.com/sds/overcommit/blob/master/config/default.yml
21
+ BrokenSymlinks:
22
+ enabled: true
23
+ description: 'Check for broken symlinks'
24
+ quiet: true
25
+ exclude:
26
+ # This is a symlink to sibling checkout of vincelifedaily, used
27
+ # only in local development
28
+ - config/env.1p
20
29
  RuboCop:
21
30
  enabled: true
22
31
  on_warn: fail # Treat all warnings as failures
@@ -29,6 +38,10 @@ PreCommit:
29
38
  - '**/Gemfile'
30
39
  - '**/Rakefile'
31
40
  - 'bin/*'
41
+ - 'script/*'
42
+ exclude:
43
+ - db/migrate/*.rb
44
+ - db/schema.rb
32
45
  PythonFlake8:
33
46
  enabled: true
34
47
  on_warn: fail
@@ -37,6 +50,7 @@ PreCommit:
37
50
  on_warn: fail
38
51
  include:
39
52
  - '.envrc'
53
+ - '**/*.sh'
40
54
  YamlLint:
41
55
  enabled: true
42
56
  on_warn: fail
@@ -53,3 +67,10 @@ PrePush:
53
67
  include:
54
68
  - 'test/unit/**/test*.rb'
55
69
  enabled: true
70
+
71
+ #PostCheckout:
72
+ # ALL: # Special hook name that customizes all hooks of this type
73
+ # quiet: true # Change all post-checkout hooks to only display output on failure
74
+ #
75
+ # IndexTags:
76
+ # enabled: true # Generate a tags file with `ctags` each time HEAD changes
data/.rubocop.yml CHANGED
@@ -98,7 +98,7 @@ Style/TrivialAccessors:
98
98
 
99
99
  AllCops:
100
100
  NewCops: enable
101
- TargetRubyVersion: 2.6
101
+ TargetRubyVersion: 2.7
102
102
  Exclude:
103
103
  - 'bin/*'
104
104
  - 'vendor/**/*'
data/Gemfile.lock CHANGED
@@ -12,7 +12,7 @@ GIT
12
12
  PATH
13
13
  remote: .
14
14
  specs:
15
- checkoff (0.19.2)
15
+ checkoff (0.21.0)
16
16
  activesupport
17
17
  asana (> 0.10.0)
18
18
  cache_method
@@ -22,13 +22,16 @@ PATH
22
22
  GEM
23
23
  remote: https://rubygems.org/
24
24
  specs:
25
- activesupport (6.1.6.1)
25
+ activesupport (7.0.4)
26
26
  concurrent-ruby (~> 1.0, >= 1.0.2)
27
27
  i18n (>= 1.6, < 2)
28
28
  minitest (>= 5.1)
29
29
  tzinfo (~> 2.0)
30
- zeitwerk (~> 2.3)
30
+ addressable (2.8.1)
31
+ public_suffix (>= 2.0.2, < 6.0)
32
+ ansi (1.5.0)
31
33
  ast (2.4.2)
34
+ builder (3.2.4)
32
35
  bump (0.10.0)
33
36
  cache (0.4.1)
34
37
  cache_method (0.2.7)
@@ -38,8 +41,12 @@ GEM
38
41
  childprocess (4.0.0)
39
42
  coderay (1.1.3)
40
43
  concurrent-ruby (1.1.10)
41
- dalli (3.2.2)
44
+ crack (0.4.5)
45
+ rexml
46
+ dalli (3.2.3)
47
+ diff-lcs (1.5.0)
42
48
  docile (1.4.0)
49
+ fakeweb (1.3.0)
43
50
  faraday (1.5.1)
44
51
  faraday-em_http (~> 1.0)
45
52
  faraday-em_synchrony (~> 1.0)
@@ -63,17 +70,19 @@ GEM
63
70
  faraday_middleware
64
71
  multi_json
65
72
  gli (2.21.0)
73
+ hashdiff (1.0.1)
66
74
  i18n (1.12.0)
67
75
  concurrent-ruby (~> 1.0)
68
76
  imagen (0.1.8)
69
77
  parser (>= 2.5, != 2.5.1.1)
70
78
  iniparse (1.5.0)
79
+ json (2.6.2)
71
80
  jwt (2.2.3)
72
81
  kramdown (2.4.0)
73
82
  rexml
74
83
  kramdown-parser-gfm (1.1.0)
75
84
  kramdown (~> 2.0)
76
- mdl (0.11.0)
85
+ mdl (0.12.0)
77
86
  kramdown (~> 2.3)
78
87
  kramdown-parser-gfm (~> 1.1)
79
88
  mixlib-cli (~> 2.1, >= 2.1.1)
@@ -82,12 +91,17 @@ GEM
82
91
  method_source (1.0.0)
83
92
  minitest (5.16.2)
84
93
  minitest-profile (0.0.2)
94
+ minitest-reporters (1.5.0)
95
+ ansi
96
+ builder
97
+ minitest (>= 5.0)
98
+ ruby-progressbar
85
99
  mixlib-cli (2.1.8)
86
100
  mixlib-config (3.0.27)
87
101
  tomlrb
88
102
  mixlib-shellout (3.2.7)
89
103
  chef-utils
90
- mocha (1.12.0)
104
+ mocha (2.0.0.alpha.1)
91
105
  multi_json (1.15.0)
92
106
  multi_xml (0.6.0)
93
107
  multipart-post (2.1.1)
@@ -101,35 +115,50 @@ GEM
101
115
  childprocess (>= 0.6.3, < 5)
102
116
  iniparse (~> 1.4)
103
117
  rexml (~> 3.2)
104
- parallel (1.20.1)
105
- parser (3.0.2.0)
118
+ parallel (1.22.1)
119
+ parser (3.1.2.1)
106
120
  ast (~> 2.4.1)
107
121
  pry (0.14.1)
108
122
  coderay (~> 1.1)
109
123
  method_source (~> 1.0)
124
+ public_suffix (5.0.0)
110
125
  rack (2.2.4)
111
- rainbow (3.0.0)
126
+ rainbow (3.1.1)
112
127
  rake (13.0.3)
113
- regexp_parser (2.1.1)
128
+ regexp_parser (2.6.0)
114
129
  rexml (3.2.5)
115
- rubocop (1.18.4)
130
+ rspec (3.11.0)
131
+ rspec-core (~> 3.11.0)
132
+ rspec-expectations (~> 3.11.0)
133
+ rspec-mocks (~> 3.11.0)
134
+ rspec-core (3.11.0)
135
+ rspec-support (~> 3.11.0)
136
+ rspec-expectations (3.11.1)
137
+ diff-lcs (>= 1.2.0, < 2.0)
138
+ rspec-support (~> 3.11.0)
139
+ rspec-mocks (3.11.1)
140
+ diff-lcs (>= 1.2.0, < 2.0)
141
+ rspec-support (~> 3.11.0)
142
+ rspec-support (3.11.1)
143
+ rubocop (1.36.0)
144
+ json (~> 2.3)
116
145
  parallel (~> 1.10)
117
- parser (>= 3.0.0.0)
146
+ parser (>= 3.1.2.1)
118
147
  rainbow (>= 2.2.2, < 4.0)
119
148
  regexp_parser (>= 1.8, < 3.0)
120
- rexml
121
- rubocop-ast (>= 1.8.0, < 2.0)
149
+ rexml (>= 3.2.5, < 4.0)
150
+ rubocop-ast (>= 1.20.1, < 2.0)
122
151
  ruby-progressbar (~> 1.7)
123
152
  unicode-display_width (>= 1.4.0, < 3.0)
124
- rubocop-ast (1.8.0)
125
- parser (>= 3.0.1.1)
153
+ rubocop-ast (1.22.0)
154
+ parser (>= 3.1.1.0)
126
155
  rubocop-minitest (0.12.1)
127
156
  rubocop (>= 0.90, < 2.0)
128
157
  rubocop-rake (0.5.1)
129
158
  rubocop
130
159
  ruby-progressbar (1.11.0)
131
160
  ruby2_keywords (0.0.5)
132
- rugged (1.1.0)
161
+ rugged (1.5.0.1)
133
162
  simplecov (0.21.2)
134
163
  docile (~> 1.1)
135
164
  simplecov-html (~> 0.11)
@@ -140,17 +169,21 @@ GEM
140
169
  tomlrb (2.0.3)
141
170
  tzinfo (2.0.5)
142
171
  concurrent-ruby (~> 1.0)
143
- undercover (0.4.3)
172
+ undercover (0.4.5)
144
173
  imagen (>= 0.1.8)
145
174
  rainbow (>= 2.1, < 4.0)
146
- rugged (>= 0.27, < 1.2)
147
- unicode-display_width (2.0.0)
148
- zeitwerk (2.6.0)
175
+ rugged (>= 0.27, < 1.6)
176
+ unicode-display_width (2.3.0)
177
+ webmock (3.18.1)
178
+ addressable (>= 2.8.0)
179
+ crack (>= 0.3.2)
180
+ hashdiff (>= 0.4.0, < 2.0.0)
149
181
 
150
182
  PLATFORMS
151
183
  ruby
152
184
  x86_64-darwin-19
153
185
  x86_64-darwin-20
186
+ x86_64-darwin-21
154
187
  x86_64-linux
155
188
 
156
189
  DEPENDENCIES
@@ -158,18 +191,22 @@ DEPENDENCIES
158
191
  bump
159
192
  bundler
160
193
  checkoff!
194
+ fakeweb
161
195
  mdl
162
196
  minitest-profile
163
- mocha
197
+ minitest-reporters
198
+ mocha (~> 2.0.0.alpha.1)
164
199
  overcommit (>= 0.58.0)
165
200
  pry
166
201
  rake (~> 13.0)
167
- rubocop
202
+ rspec (>= 3.4)
203
+ rubocop (~> 1.36)
168
204
  rubocop-minitest
169
205
  rubocop-rake
170
206
  simplecov (>= 0.18.0)
171
207
  simplecov-lcov
172
208
  undercover
209
+ webmock
173
210
 
174
211
  BUNDLED WITH
175
- 2.2.25
212
+ 2.3.26
data/Makefile CHANGED
@@ -15,7 +15,17 @@ export PRINT_HELP_PYSCRIPT
15
15
  help:
16
16
  @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
17
17
 
18
- default: localtest ## run default tests and quality
18
+ default: clean-coverage test coverage clean-typecoverage typecheck typecoverage quality ## run default typechecking, tests and quality
19
+
20
+ typecheck: ## validate types in code and configuration
21
+
22
+ citypecheck: typecheck ## Run type check from CircleCI
23
+
24
+ typecoverage: typecheck ## Run type checking and then ratchet coverage in metrics/
25
+
26
+ clean-typecoverage: ## Clean out type-related coverage previous results to avoid flaky results
27
+
28
+ citypecoverage: typecoverage ## Run type checking, ratchet coverage, and then complain if ratchet needs to be committed
19
29
 
20
30
  requirements_dev.txt.installed: requirements_dev.txt
21
31
  pip install -q --disable-pip-version-check -r requirements_dev.txt
@@ -40,7 +50,7 @@ clear_metrics: ## remove or reset result artifacts created by tests and quality
40
50
  clean: clear_metrics ## remove all built artifacts
41
51
 
42
52
 
43
- typecheck: ## validate types in code and configuration
53
+ citest: test ## Run unit tests from CircleCI
44
54
 
45
55
  overcommit: ## run precommit quality checks
46
56
  bundle exec overcommit --run
@@ -56,6 +66,16 @@ localtest: ## run default local actions
56
66
  repl: ## Load up checkoff in pry
57
67
  @bundle exec rake repl
58
68
 
69
+ clean-coverage:
70
+ @bundle exec rake clear_metrics
71
+
72
+ coverage: test report-coverage ## check code coverage
73
+ @bundle exec rake undercover
74
+
75
+ report-coverage: test ## Report summary of coverage to stdout, and generate HTML, XML coverage report
76
+
77
+ cicoverage: coverage ## check code coverage
78
+
59
79
  update_from_cookiecutter: ## Bring in changes from template project used to create this repo
60
80
  bundle exec overcommit --uninstall
61
81
  IN_COOKIECUTTER_PROJECT_UPGRADER=1 cookiecutter_project_upgrader || true
data/checkoff.gemspec CHANGED
@@ -13,7 +13,7 @@ Gem::Specification.new do |spec|
13
13
  spec.summary = 'Command-line and gem client for Asana (unofficial)'
14
14
  spec.homepage = 'https://github.com/apiology/checkoff'
15
15
  spec.license = 'MIT license'
16
- spec.required_ruby_version = '>= 2.6'
16
+ spec.required_ruby_version = '>= 2.7'
17
17
 
18
18
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
19
19
  `git ls-files -z`.split("\x0").reject do |f|
@@ -32,20 +32,27 @@ Gem::Specification.new do |spec|
32
32
 
33
33
  spec.add_development_dependency 'bump'
34
34
  spec.add_development_dependency 'bundler'
35
+ spec.add_development_dependency 'fakeweb'
35
36
  spec.add_development_dependency 'mdl'
36
37
  spec.add_development_dependency 'minitest-profile'
37
- spec.add_development_dependency 'mocha'
38
+ spec.add_development_dependency 'minitest-reporters'
39
+ spec.add_development_dependency 'mocha', ['~> 2.0.0.alpha.1']
38
40
  # 0.58.0 and 0.57.0 don't seem super compatible with signatures, and
39
41
  # magit doesn't seem to want to use the bundled version at the moment,
40
42
  # so let's favor the more recent version...
41
43
  spec.add_development_dependency 'overcommit', ['>=0.58.0']
42
44
  spec.add_development_dependency 'pry'
43
45
  spec.add_development_dependency 'rake', '~> 13.0'
44
- spec.add_development_dependency 'rubocop'
46
+ spec.add_development_dependency 'rspec', '>=3.4'
47
+ spec.add_development_dependency 'rubocop', ['~> 1.36']
45
48
  spec.add_development_dependency 'rubocop-minitest'
46
49
  spec.add_development_dependency 'rubocop-rake'
47
50
  # ensure version with branch coverage
48
51
  spec.add_development_dependency 'simplecov', ['>=0.18.0']
49
52
  spec.add_development_dependency 'simplecov-lcov'
50
53
  spec.add_development_dependency 'undercover'
54
+ spec.add_development_dependency 'webmock'
55
+ spec.metadata = {
56
+ 'rubygems_mfa_required' => 'true',
57
+ }
51
58
  end
data/fix.sh CHANGED
@@ -69,7 +69,18 @@ ensure_rbenv() {
69
69
 
70
70
  latest_ruby_version() {
71
71
  major_minor=${1}
72
- rbenv install --list 2>/dev/null | grep "^${major_minor}."
72
+
73
+ # Double check that this command doesn't error out under -e
74
+ rbenv install --list >/dev/null 2>&1
75
+
76
+ # not sure why, but 'rbenv install --list' below exits with error code
77
+ # 1...after providing the same output the previous line gave when it
78
+ # exited with error code 0.
79
+ #
80
+ # https://github.com/rbenv/rbenv/issues/1441
81
+ set +e
82
+ rbenv install --list 2>/dev/null | cat | grep "^${major_minor}."
83
+ set -e
73
84
  }
74
85
 
75
86
  ensure_dev_library() {
@@ -91,12 +102,20 @@ ensure_ruby_build_requirements() {
91
102
  ensure_dev_library openssl/ssl.h openssl libssl-dev
92
103
  }
93
104
 
105
+ ensure_latest_ruby_build_definitions() {
106
+ ensure_rbenv
107
+
108
+ git -C "$(rbenv root)"/plugins/ruby-build pull
109
+ }
110
+
94
111
  # You can find out which feature versions are still supported / have
95
112
  # been release here: https://www.ruby-lang.org/en/downloads/
96
113
  ensure_ruby_versions() {
114
+ ensure_latest_ruby_build_definitions
115
+
97
116
  # You can find out which feature versions are still supported / have
98
117
  # been release here: https://www.ruby-lang.org/en/downloads/
99
- ruby_versions="$(latest_ruby_version 2.6)"
118
+ ruby_versions="$(latest_ruby_version 2.7)"
100
119
 
101
120
  echo "Latest Ruby versions: ${ruby_versions}"
102
121
 
@@ -169,7 +188,7 @@ ensure_bundle() {
169
188
  #
170
189
  # This affects nokogiri, which will try to reinstall itself in
171
190
  # Docker builds where it's already installed if this is not run.
172
- for platform in x86_64-darwin-20 x86_64-linux
191
+ for platform in x86_64-darwin-21 x86_64-linux
173
192
  do
174
193
  grep "${platform:?}" Gemfile.lock >/dev/null 2>&1 || bundle lock --add-platform "${platform:?}"
175
194
  done
@@ -18,7 +18,7 @@ module Checkoff
18
18
  config_value = @config[key]
19
19
  return config_value unless config_value.nil?
20
20
 
21
- ENV[envvar_name(key)]
21
+ ENV.fetch(envvar_name(key), nil)
22
22
  end
23
23
 
24
24
  def fetch(key)
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'custom_field_variant'
4
+
5
+ module Checkoff
6
+ module Internal
7
+ module SearchUrl
8
+ # Convert custom field parameters from an Asana search URL into
9
+ # API search arguments and Checkoff task selectors
10
+ class CustomFieldParamConverter
11
+ def initialize(custom_field_params:)
12
+ @custom_field_params = custom_field_params
13
+ end
14
+
15
+ def convert
16
+ args = {}
17
+ task_selector = []
18
+ by_custom_field.each do |gid, single_custom_field_params|
19
+ new_args, new_task_selector = convert_single_custom_field_params(gid,
20
+ single_custom_field_params)
21
+ args, task_selector = merge_args_and_task_selectors(args, new_args,
22
+ task_selector, new_task_selector)
23
+ end
24
+ [args, task_selector]
25
+ end
26
+
27
+ private
28
+
29
+ def by_custom_field
30
+ custom_field_params.group_by do |key, _value|
31
+ gid_from_custom_field_key(key)
32
+ end.transform_values(&:to_h)
33
+ end
34
+
35
+ def merge_args_and_task_selectors(args, new_args, task_selector, new_task_selector)
36
+ args = args.merge(new_args)
37
+ return [args, task_selector] if new_task_selector == []
38
+
39
+ raise 'Teach me how to merge task selectors' unless task_selector == []
40
+
41
+ task_selector = new_task_selector
42
+
43
+ [args, task_selector]
44
+ end
45
+
46
+ VARIANTS = {
47
+ 'is' => CustomFieldVariant::Is,
48
+ 'no_value' => CustomFieldVariant::NoValue,
49
+ 'is_not' => CustomFieldVariant::IsNot,
50
+ 'less_than' => CustomFieldVariant::LessThan,
51
+ }.freeze
52
+
53
+ def convert_single_custom_field_params(gid, single_custom_field_params)
54
+ variant_key = "custom_field_#{gid}.variant"
55
+ variant = single_custom_field_params.fetch(variant_key)
56
+ remaining_params = single_custom_field_params.reject { |k, _v| k == variant_key }
57
+ raise "Teach me how to handle #{variant_key} = #{variant}" unless variant.length == 1
58
+
59
+ variant_class = VARIANTS[variant[0]]
60
+ return variant_class.new(gid, remaining_params).convert unless variant_class.nil?
61
+
62
+ raise "Teach me how to handle #{variant_key} = #{variant}"
63
+ end
64
+
65
+ def gid_from_custom_field_key(key)
66
+ key.split('_')[2].split('.')[0]
67
+ end
68
+
69
+ attr_reader :custom_field_params
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Checkoff
4
+ module Internal
5
+ module SearchUrl
6
+ module CustomFieldVariant
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 LessThan < 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 IsNot < 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 NoValue < 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 Is < 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
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require_relative 'simple_param_converter'
6
+ require_relative 'custom_field_param_converter'
7
+
8
+ module Checkoff
9
+ module Internal
10
+ module SearchUrl
11
+ # Parse Asana search URLs into parameters suitable to pass into
12
+ # the /workspaces/{workspace_gid}/tasks/search endpoint
13
+ class Parser
14
+ def initialize(_deps = {})
15
+ # allow dependencies to be passed in by tests
16
+ end
17
+
18
+ def convert_params(url)
19
+ url_params = CGI.parse(URI.parse(url).query)
20
+ custom_field_params, simple_url_params = partition_url_params(url_params)
21
+ custom_field_args, task_selector = convert_custom_field_params(custom_field_params)
22
+ simple_url_args = convert_simple_params(simple_url_params)
23
+ [custom_field_args.merge(simple_url_args), task_selector]
24
+ end
25
+
26
+ private
27
+
28
+ def convert_simple_params(simple_url_params)
29
+ SimpleParamConverter.new(simple_url_params: simple_url_params).convert
30
+ end
31
+
32
+ def convert_custom_field_params(custom_field_params)
33
+ CustomFieldParamConverter.new(custom_field_params: custom_field_params).convert
34
+ end
35
+
36
+ def partition_url_params(url_params)
37
+ url_params.to_a.partition do |key, _values|
38
+ key.start_with? 'custom_field_'
39
+ end.map(&:to_h)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'custom_field_param_converter'
4
+
5
+ module Checkoff
6
+ module Internal
7
+ module SearchUrl
8
+ module SimpleParam
9
+ # base class for handling different types of search url params
10
+ class SimpleParam
11
+ def initialize(values:)
12
+ @values = values
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :values
18
+ end
19
+
20
+ # Handle 'any_projects.ids' search url param
21
+ class AnyProjectsIds < SimpleParam
22
+ def convert
23
+ ['projects.any', values.join(',')]
24
+ end
25
+ end
26
+
27
+ # Handle 'completion' search url param
28
+ class Completion < SimpleParam
29
+ def convert
30
+ raise "Teach me how to handle #{key} = #{values}" if values.length != 1
31
+
32
+ value = values.fetch(0)
33
+ raise "Teach me how to handle #{key} = #{values}" if value != 'incomplete'
34
+
35
+ ['completed', false]
36
+ end
37
+ end
38
+
39
+ # Handle 'not_tags.ids' search url param
40
+ class NotTagsIds < SimpleParam
41
+ def convert
42
+ raise "Teach me how to handle #{key} = #{values}" if values.length != 1
43
+
44
+ value = values.fetch(0)
45
+ tag_ids = value.split('~')
46
+ ['tags.not', tag_ids.join(',')]
47
+ end
48
+ end
49
+ end
50
+
51
+ # Convert simple parameters - ones where the param name itself
52
+ # doesn't encode any parameters'
53
+ class SimpleParamConverter
54
+ def initialize(simple_url_params:)
55
+ @simple_url_params = simple_url_params
56
+ end
57
+
58
+ def convert
59
+ simple_url_params.to_a.to_h do |key, values|
60
+ convert_arg(key, values)
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ ARGS = {
67
+ 'any_projects.ids' => SimpleParam::AnyProjectsIds,
68
+ 'completion' => SimpleParam::Completion,
69
+ 'not_tags.ids' => SimpleParam::NotTagsIds,
70
+ }.freeze
71
+
72
+ # https://developers.asana.com/docs/search-tasks-in-a-workspace
73
+ def convert_arg(key, values)
74
+ clazz = ARGS.fetch(key)
75
+ clazz.new(values: values).convert
76
+ end
77
+
78
+ attr_reader :simple_url_params
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'search_url/parser'
@@ -110,7 +110,8 @@ module Checkoff
110
110
  raise "Could not find task in project_gid #{project_gid}: #{task}" if membership.nil?
111
111
 
112
112
  current_section = membership['section']['name']
113
- current_section = nil if ['(no section)', 'Untitled section'].include?(current_section)
113
+ inbox_section_names = ['(no section)', 'Untitled section', 'Inbox']
114
+ current_section = nil if inbox_section_names.include?(current_section)
114
115
  by_section[current_section] ||= []
115
116
  by_section[current_section] << task
116
117
  end
@@ -41,7 +41,7 @@ module Checkoff
41
41
  def raw_subtasks(task)
42
42
  task_options = projects.task_options
43
43
  task_options[:options][:fields] << 'is_rendered_as_separator'
44
- task.subtasks(task_options)
44
+ task.subtasks(**task_options)
45
45
  end
46
46
  cache_method :raw_subtasks, SHORT_CACHE_TIME
47
47
 
@@ -12,7 +12,7 @@ require_relative 'task_selectors'
12
12
  require 'asana/resource_includes/collection'
13
13
  require 'asana/resource_includes/response_helper'
14
14
 
15
- require 'checkoff/internal/search_url_parser'
15
+ require 'checkoff/internal/search_url'
16
16
 
17
17
  # https://developers.asana.com/docs/task-searches
18
18
  module Checkoff
@@ -33,7 +33,7 @@ module Checkoff
33
33
  projects: Checkoff::Projects.new(config: config),
34
34
  clients: Checkoff::Clients.new(config: config),
35
35
  client: clients.client,
36
- search_url_parser: Checkoff::Internal::SearchUrlParser.new,
36
+ search_url_parser: Checkoff::Internal::SearchUrl::Parser.new,
37
37
  asana_resources_collection_class: Asana::Resources::Collection)
38
38
  @workspaces = workspaces
39
39
  @task_selectors = task_selectors
@@ -3,5 +3,5 @@
3
3
  # Command-line and gem client for Asana (unofficial)
4
4
  module Checkoff
5
5
  # Version of library
6
- VERSION = '0.19.2'
6
+ VERSION = '0.21.0'
7
7
  end
@@ -3,15 +3,7 @@
3
3
  desc 'Ensure that any locally ratcheted coverage metrics are cleared back ' \
4
4
  'to git baseline'
5
5
  task :clear_metrics do |_t|
6
- ret =
7
- system('git checkout coverage/.last_run.json')
8
- raise unless ret
9
-
10
6
  # Without this old lines which are removed are still counted,
11
7
  # leading to inconsistent coverage percentages between runs.
12
- #
13
- # need to save coverage/.last_run.json
14
- ret =
15
- system('rm -fr coverage/assets coverage/.*.json.lock coverage/lcov/* coverage/index.html coverage/.resultset.json')
16
- raise unless ret
8
+ raise unless system('rm -fr coverage/')
17
9
  end
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.19.2
4
+ version: 0.21.0
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-24 00:00:00.000000000 Z
11
+ date: 2022-12-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: fakeweb
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: mdl
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -137,7 +151,7 @@ dependencies:
137
151
  - !ruby/object:Gem::Version
138
152
  version: '0'
139
153
  - !ruby/object:Gem::Dependency
140
- name: mocha
154
+ name: minitest-reporters
141
155
  requirement: !ruby/object:Gem::Requirement
142
156
  requirements:
143
157
  - - ">="
@@ -150,6 +164,20 @@ dependencies:
150
164
  - - ">="
151
165
  - !ruby/object:Gem::Version
152
166
  version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: mocha
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 2.0.0.alpha.1
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 2.0.0.alpha.1
153
181
  - !ruby/object:Gem::Dependency
154
182
  name: overcommit
155
183
  requirement: !ruby/object:Gem::Requirement
@@ -193,19 +221,33 @@ dependencies:
193
221
  - !ruby/object:Gem::Version
194
222
  version: '13.0'
195
223
  - !ruby/object:Gem::Dependency
196
- name: rubocop
224
+ name: rspec
197
225
  requirement: !ruby/object:Gem::Requirement
198
226
  requirements:
199
227
  - - ">="
200
228
  - !ruby/object:Gem::Version
201
- version: '0'
229
+ version: '3.4'
202
230
  type: :development
203
231
  prerelease: false
204
232
  version_requirements: !ruby/object:Gem::Requirement
205
233
  requirements:
206
234
  - - ">="
207
235
  - !ruby/object:Gem::Version
208
- version: '0'
236
+ version: '3.4'
237
+ - !ruby/object:Gem::Dependency
238
+ name: rubocop
239
+ requirement: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - "~>"
242
+ - !ruby/object:Gem::Version
243
+ version: '1.36'
244
+ type: :development
245
+ prerelease: false
246
+ version_requirements: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - "~>"
249
+ - !ruby/object:Gem::Version
250
+ version: '1.36'
209
251
  - !ruby/object:Gem::Dependency
210
252
  name: rubocop-minitest
211
253
  requirement: !ruby/object:Gem::Requirement
@@ -276,6 +318,20 @@ dependencies:
276
318
  - - ">="
277
319
  - !ruby/object:Gem::Version
278
320
  version: '0'
321
+ - !ruby/object:Gem::Dependency
322
+ name: webmock
323
+ requirement: !ruby/object:Gem::Requirement
324
+ requirements:
325
+ - - ">="
326
+ - !ruby/object:Gem::Version
327
+ version: '0'
328
+ type: :development
329
+ prerelease: false
330
+ version_requirements: !ruby/object:Gem::Requirement
331
+ requirements:
332
+ - - ">="
333
+ - !ruby/object:Gem::Version
334
+ version: '0'
279
335
  description:
280
336
  email:
281
337
  - vince@broz.cc
@@ -310,7 +366,6 @@ files:
310
366
  - bin/rake
311
367
  - bin/setup
312
368
  - checkoff.gemspec
313
- - coverage/.last_run.json
314
369
  - docs/cookiecutter_input.json
315
370
  - docs/example_project.png
316
371
  - exe/checkoff
@@ -322,7 +377,11 @@ files:
322
377
  - lib/checkoff/custom_fields.rb
323
378
  - lib/checkoff/internal/config_loader.rb
324
379
  - lib/checkoff/internal/create-class.sh
325
- - lib/checkoff/internal/search_url_parser.rb
380
+ - lib/checkoff/internal/search_url.rb
381
+ - lib/checkoff/internal/search_url/custom_field_param_converter.rb
382
+ - lib/checkoff/internal/search_url/custom_field_variant.rb
383
+ - lib/checkoff/internal/search_url/parser.rb
384
+ - lib/checkoff/internal/search_url/simple_param_converter.rb
326
385
  - lib/checkoff/internal/task_selector_evaluator.rb
327
386
  - lib/checkoff/my_tasks.rb
328
387
  - lib/checkoff/projects.rb
@@ -366,7 +425,8 @@ files:
366
425
  homepage: https://github.com/apiology/checkoff
367
426
  licenses:
368
427
  - MIT license
369
- metadata: {}
428
+ metadata:
429
+ rubygems_mfa_required: 'true'
370
430
  post_install_message:
371
431
  rdoc_options: []
372
432
  require_paths:
@@ -375,14 +435,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
375
435
  requirements:
376
436
  - - ">="
377
437
  - !ruby/object:Gem::Version
378
- version: '2.6'
438
+ version: '2.7'
379
439
  required_rubygems_version: !ruby/object:Gem::Requirement
380
440
  requirements:
381
441
  - - ">="
382
442
  - !ruby/object:Gem::Version
383
443
  version: '0'
384
444
  requirements: []
385
- rubygems_version: 3.0.3.1
445
+ rubygems_version: 3.1.6
386
446
  signing_key:
387
447
  specification_version: 4
388
448
  summary: Command-line and gem client for Asana (unofficial)
@@ -1,6 +0,0 @@
1
- {
2
- "result": {
3
- "line": 99.56,
4
- "branch": 86.11
5
- }
6
- }
@@ -1,228 +0,0 @@
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
- when 'not_tags.ids'
217
- raise "Teach me how to handle #{key} = #{values}" if values.length != 1
218
-
219
- value = values.fetch(0)
220
- tag_ids = value.split('~')
221
- ['tags.not', tag_ids.join(',')]
222
- else
223
- raise "Teach me how to handle #{key} = #{values}"
224
- end
225
- end
226
- end
227
- end
228
- end