checkoff 0.45.1 → 0.46.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 166520f53086eea60a30deae24eacea2bff98b58cc63e1bc14397e016cfce5a3
4
- data.tar.gz: 182f61588c6343c7b16100a56357909b5d1c532a2580040fdb50828dedeb4032
3
+ metadata.gz: 5a4be48acbc5f3ab9954ca8f7ec3ef64f1c629bfbc0015f158cde7a87fcc4049
4
+ data.tar.gz: 1824e5607d750cc1d9b2161dc897cd4a08fd90c27fea1c4cc7a5f19ee4e2c497
5
5
  SHA512:
6
- metadata.gz: 00461660f5b2f2927d5c0bd89b34e12a1393ce105dcddf6782054ef655c3ba55654c0d5a818e35907dd46d457beac3a18b637bb2a5379ae78d30913fdd55944d
7
- data.tar.gz: 8aa6bdc999894732e1e243cac1617b6b40b905b80562c5d8d57bbf2a988b4f1b66fc7848dbe4937dc01228c503a4e26661bc17a28c75c6effed5b10d9afd10db
6
+ metadata.gz: 3ef67e1152d4399b8b507a6b6b6555f300ac83ffda4b998a8ae031ada997deabdd60974228db9cb01101bf319a3092a36fa259391c4fbdc6940266e1d53f923d
7
+ data.tar.gz: e04b024a9f4def03f62fb7a8b1498e53ed2d08cc53cff3a5b76bc7f16b6a20402a450704a7f782372b5f3438a7ecad063f1e7047b552d50a360fa7c5d1d60dbc
data/Gemfile.lock CHANGED
@@ -12,7 +12,7 @@ GIT
12
12
  PATH
13
13
  remote: .
14
14
  specs:
15
- checkoff (0.45.1)
15
+ checkoff (0.46.1)
16
16
  activesupport
17
17
  asana (> 0.10.0)
18
18
  cache_method
@@ -83,7 +83,7 @@
83
83
  # # just because another object it is associated with (e.g. a subtask)
84
84
  # # is modified. Actions that count as modifying the task include
85
85
  # # assigning, renaming, completing, and adding stories.
86
- # # @return [Asana::Collection<Asana::Resources::Task>]
86
+ # # @return [Enumerable<Asana::Resources::Task>]
87
87
  # def find_all(assignee: nil, workspace: nil, project: nil, section: nil,
88
88
  # tag: nil, user_task_list: nil, completed_since: nil,
89
89
  # modified_since: nil, per_page: 20, options: {}); end
@@ -106,7 +106,7 @@
106
106
  # class Section
107
107
  # # @param project_gid [String]
108
108
  # # @return [Enumerable<Asana::Resources::Section>]
109
- # def get_sections_for_project(client, project_gid:, options: {}); end
109
+ # def get_sections_for_project(project_gid:, options: {}); end
110
110
  # end
111
111
  # class Project
112
112
  # # Returns the compact project records for all projects in the workspace.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'custom_field_variant'
4
+ require_relative 'results_merger'
4
5
 
5
6
  module Checkoff
6
7
  module Internal
@@ -8,44 +9,41 @@ module Checkoff
8
9
  # Convert custom field parameters from an Asana search URL into
9
10
  # API search arguments and Checkoff task selectors
10
11
  class CustomFieldParamConverter
12
+ # @param custom_field_params [Hash<String, Array<String>>]
11
13
  def initialize(custom_field_params:)
12
14
  @custom_field_params = custom_field_params
13
15
  end
14
16
 
17
+ # @return [Array(Hash<String, String>, Array<[Symbol, Array]>)]
15
18
  def convert
19
+ # @type args [Hash<String, String>]
16
20
  args = {}
21
+ # @type task_selector [Array<[Symbol, Array]>]
17
22
  task_selector = []
18
23
  by_custom_field.each do |gid, single_custom_field_params|
24
+ # @sg-ignore
19
25
  new_args, new_task_selector = convert_single_custom_field_params(gid,
20
26
  single_custom_field_params)
21
- args, task_selector = merge_args_and_task_selectors(args, new_args,
22
- task_selector, new_task_selector)
27
+
28
+ args = ResultsMerger.merge_args(args, new_args)
29
+
30
+ # @sg-ignore
31
+ task_selector = ResultsMerger.merge_task_selectors(task_selector, new_task_selector)
23
32
  end
24
33
  [args, task_selector]
25
34
  end
26
35
 
27
36
  private
28
37
 
38
+ # @sg-ignore
39
+ # @return [Hash<String, Hash>]
29
40
  def by_custom_field
30
41
  custom_field_params.group_by do |key, _value|
31
42
  gid_from_custom_field_key(key)
32
43
  end.transform_values(&:to_h)
33
44
  end
34
45
 
35
- def merge_args_and_task_selectors(args, new_args, task_selector, new_task_selector)
36
- final_args = args.merge(new_args)
37
-
38
- final_task_selector = if new_task_selector == []
39
- task_selector
40
- elsif task_selector == []
41
- new_task_selector
42
- else
43
- [:and, task_selector, new_task_selector]
44
- end
45
-
46
- [final_args, final_task_selector]
47
- end
48
-
46
+ # @type [Hash<String, Class<CustomFieldVariant>]
49
47
  VARIANTS = {
50
48
  'is' => CustomFieldVariant::Is,
51
49
  'no_value' => CustomFieldVariant::NoValue,
@@ -58,22 +56,33 @@ module Checkoff
58
56
  'contains_all' => CustomFieldVariant::ContainsAll,
59
57
  }.freeze
60
58
 
59
+ # @param gid [String]
60
+ # @param single_custom_field_params [Hash<String, Array<String>>]
61
+ # @sg-ignore
62
+ # @return [Array(Hash<String, String>, Array<[Symbol, Array]>)]
61
63
  def convert_single_custom_field_params(gid, single_custom_field_params)
62
64
  variant_key = "custom_field_#{gid}.variant"
63
65
  variant = single_custom_field_params.fetch(variant_key)
64
66
  remaining_params = single_custom_field_params.reject { |k, _v| k == variant_key }
65
67
  raise "Teach me how to handle #{variant_key} = #{variant}" unless variant.length == 1
66
68
 
69
+ # @sg-ignore
70
+ # @type variant_class [Class<CustomFieldVariant>]
67
71
  variant_class = VARIANTS[variant[0]]
72
+ # @type [Array(Hash<String, String>, Array<[Symbol, Array]>)]
73
+ # @sg-ignore
68
74
  return variant_class.new(gid, remaining_params).convert unless variant_class.nil?
69
75
 
70
76
  raise "Teach me how to handle #{variant_key} = #{variant}"
71
77
  end
72
78
 
79
+ # @param key [String]
80
+ # @return [String]
73
81
  def gid_from_custom_field_key(key)
74
82
  key.split('_')[2].split('.')[0]
75
83
  end
76
84
 
85
+ # @return [Hash<String, Array<String>>]
77
86
  attr_reader :custom_field_params
78
87
  end
79
88
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'custom_field_param_converter'
4
+
5
+ module Checkoff
6
+ module Internal
7
+ module SearchUrl
8
+ # Convert date parameters - ones where the param name itself
9
+ # doesn't encode any parameters'
10
+ class DateParamConverter
11
+ # @param date_url_params [Hash<String, Array<String>>] the simple params
12
+ def initialize(date_url_params:)
13
+ @date_url_params = date_url_params
14
+ end
15
+
16
+ # @return [Array(Hash<String, String>, Array<[Symbol, Array]>)] See https://developers.asana.com/docs/search-tasks-in-a-workspace
17
+ def convert
18
+ return [{}, []] if date_url_params.empty?
19
+
20
+ # example params:
21
+ # due_date.operator=through_next
22
+ # due_date.value=0
23
+ # due_date.unit=day
24
+ validate_due_date_through_next!
25
+
26
+ value = date_url_params.fetch('due_date.value').fetch(0).to_i
27
+
28
+ # @sg-ignore
29
+ # @type [Date]
30
+ before = Date.today + value
31
+
32
+ validate_unit_is_day!
33
+
34
+ # 'due_on.before' => '2023-01-01',
35
+ # 'due_on.after' => '2023-01-01',
36
+ # [{ 'due_on.before' => '2023-09-01' }, []]
37
+ [{ 'due_on.before' => before.to_s }, []]
38
+ end
39
+
40
+ private
41
+
42
+ # @return [void]
43
+ def validate_unit_is_day!
44
+ unit = date_url_params.fetch('due_date.unit').fetch(0)
45
+
46
+ raise 'Teach me how to handle other time units' unless unit == 'day'
47
+ end
48
+
49
+ # @return [void]
50
+ def validate_due_date_through_next!
51
+ due_date_operators = date_url_params.fetch('due_date.operator')
52
+
53
+ return if due_date_operators == ['through_next']
54
+
55
+ raise "Teach me how to handle date mode: #{due_date_operators}."
56
+ end
57
+
58
+ # @return [Hash<String, Array<String>>]
59
+ attr_reader :date_url_params
60
+ end
61
+ end
62
+ end
63
+ end
@@ -6,6 +6,8 @@ require 'cgi'
6
6
  require 'uri'
7
7
  require_relative 'simple_param_converter'
8
8
  require_relative 'custom_field_param_converter'
9
+ require_relative 'results_merger'
10
+ require_relative 'date_param_converter'
9
11
 
10
12
  module Checkoff
11
13
  module Internal
@@ -13,36 +15,68 @@ module Checkoff
13
15
  # Parse Asana search URLs into parameters suitable to pass into
14
16
  # the /workspaces/{workspace_gid}/tasks/search endpoint
15
17
  class Parser
18
+ # @param _deps [Hash]
16
19
  def initialize(_deps = {})
17
20
  # allow dependencies to be passed in by tests
18
21
  end
19
22
 
23
+ # @param url [String]
24
+ # @return [Array(Hash<String, String>, Hash<String, String>)]
20
25
  def convert_params(url)
21
26
  url_params = CGI.parse(URI.parse(url).query)
27
+ # @type custom_field_params [Hash<String, Array<String>>]
28
+ # @type date_url_params [Hash<String, Array<String>>]
29
+ # @type simple_url_params [Hash<String, Array<String>>]
30
+ # @sg-ignore
31
+ custom_field_params, date_url_params, simple_url_params = partition_url_params(url_params)
32
+ # @type custom_field_args [Hash<String, String>]
33
+ # @type custom_field_task_selector [Hash<String, String>]
22
34
  # @sg-ignore
23
- custom_field_params, simple_url_params = partition_url_params(url_params)
35
+ custom_field_args, custom_field_task_selector = convert_custom_field_params(custom_field_params)
36
+ # @type date_args [Hash<String, String>]
37
+ # @type date_task_selector [Hash<String, String>]
24
38
  # @sg-ignore
25
- custom_field_args, task_selector = convert_custom_field_params(custom_field_params)
39
+ date_url_args, date_task_selector = convert_date_params(date_url_params)
26
40
  simple_url_args = convert_simple_params(simple_url_params)
27
- [custom_field_args.merge(simple_url_args), task_selector]
41
+ # raise 'merge these things'
42
+ [ResultsMerger.merge_args(custom_field_args, date_url_args, simple_url_args),
43
+ ResultsMerger.merge_task_selectors(date_task_selector, custom_field_task_selector)]
28
44
  end
29
45
 
30
46
  private
31
47
 
48
+ # @param date_url_params [Hash<String, Array<String>>]
49
+ # @return [Array(Hash<String, String>, Array<[Symbol, Array]>)]
50
+ def convert_date_params(date_url_params)
51
+ DateParamConverter.new(date_url_params: date_url_params).convert
52
+ end
53
+
54
+ # @param simple_url_params [Hash<String, Array<String>>]
55
+ # @return [Hash<String, String>]
32
56
  def convert_simple_params(simple_url_params)
33
57
  SimpleParamConverter.new(simple_url_params: simple_url_params).convert
34
58
  end
35
59
 
60
+ # @param custom_field_params [Hash<String, Array<String>>]
61
+ # @return [Array(Hash<String, String>, Array<[Symbol, Array]>)]
36
62
  def convert_custom_field_params(custom_field_params)
37
63
  CustomFieldParamConverter.new(custom_field_params: custom_field_params).convert
38
64
  end
39
65
 
40
66
  # @param url_params [Hash<String, String>]
41
- # @return [Array(Hash<String, String>, Hash<String, String>)]
67
+ # @return [Array(Hash<String, String>, Hash<String, String>, Hash<String, String>)]
42
68
  def partition_url_params(url_params)
43
- url_params.to_a.partition do |key, _values|
44
- key.start_with? 'custom_field_'
45
- end.map(&:to_h)
69
+ groups = url_params.to_a.group_by do |key, _values|
70
+ if key.start_with? 'custom_field_'
71
+ :custom_field
72
+ elsif key.include? '_date'
73
+ :date
74
+ else
75
+ :simple
76
+ end
77
+ end.transform_values(&:to_h)
78
+ # @sg-ignore
79
+ [groups.fetch(:custom_field, {}), groups.fetch(:date, {}), groups.fetch(:simple, {})]
46
80
  end
47
81
  end
48
82
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Checkoff
4
+ module Internal
5
+ module SearchUrl
6
+ # Merge task selectors and search API arguments
7
+ class ResultsMerger
8
+ # @param args [Array<[Hash<String, String>]>]
9
+ # @return [Hash<String, String>
10
+ def self.merge_args(*args)
11
+ # first element of args
12
+ # @sg-ignore
13
+ # @type [Hash<String, String>]
14
+ f = args.fetch(0)
15
+ # rest of args
16
+ r = args.drop(0)
17
+ f.merge(*r)
18
+ end
19
+
20
+ # @param task_selectors [Array<Array<[Symbol, Array]>>]
21
+ # @return [Array<[Symbol, Array]>]
22
+ def self.merge_task_selectors(*task_selectors)
23
+ return [] if task_selectors.empty?
24
+
25
+ first_task_selector = task_selectors.fetch(0)
26
+
27
+ return merge_task_selectors(*task_selectors.drop(1)) if first_task_selector.empty?
28
+
29
+ return first_task_selector if task_selectors.length == 1
30
+
31
+ rest_task_selectors = merge_task_selectors(*task_selectors.drop(1))
32
+
33
+ return first_task_selector if rest_task_selectors.empty?
34
+
35
+ [:and, first_task_selector, rest_task_selectors]
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -12,8 +12,12 @@ module Checkoff
12
12
  LONG_CACHE_TIME = MINUTE * 15
13
13
  SHORT_CACHE_TIME = MINUTE * 5
14
14
 
15
+ # @return [Checkoff::Projects]
15
16
  attr_reader :projects
16
17
 
18
+ # @param config [Hash<Symbol, Object>]
19
+ # @param client [Asana::Client]
20
+ # @param projects [Checkoff::Projects]
17
21
  def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana),
18
22
  client: Checkoff::Clients.new(config: config).client,
19
23
  projects: Checkoff::Projects.new(config: config,
@@ -26,13 +30,23 @@ module Checkoff
26
30
  # Given a 'My Tasks' project object, pull all tasks, then provide
27
31
  # a Hash of tasks with section name -> task list of the
28
32
  # uncompleted tasks.
29
- def tasks_by_section_for_my_tasks(project, extra_fields: [])
33
+ #
34
+ # @param project [Asana::Resources::Project]
35
+ # @param only_uncompleted [Boolean]
36
+ # @param extra_fields [Array<String>]
37
+ # @return [Hash<String, Enumerable<Asana::Resources::Task>>]
38
+ def tasks_by_section_for_my_tasks(project,
39
+ only_uncompleted: true,
40
+ extra_fields: [])
30
41
  raw_tasks = projects.tasks_from_project(project,
42
+ only_uncompleted: only_uncompleted,
31
43
  extra_fields: extra_fields + ['assignee_section.name'])
32
44
  active_tasks = projects.active_tasks(raw_tasks)
33
45
  by_my_tasks_section(active_tasks, project.gid)
34
46
  end
35
47
 
48
+ # @param name [String]
49
+ # @return [String, nil]
36
50
  def section_key(name)
37
51
  return nil if name == 'Recently assigned'
38
52
 
@@ -41,10 +55,14 @@ module Checkoff
41
55
 
42
56
  # Given a list of tasks in 'My Tasks', pull a Hash of tasks with
43
57
  # section name -> task list
58
+ #
59
+ # @param tasks [Enumerable<Asana::Resources::Task>]
60
+ # @param project_gid [String]
61
+ # @return [Hash<String, Enumerable<Asana::Resources::Task>>]
44
62
  def by_my_tasks_section(tasks, project_gid)
45
63
  by_section = {}
46
64
  sections = client.sections.get_sections_for_project(project_gid: project_gid)
47
- sections.each { |section| by_section[section_key(section.name)] = [] }
65
+ sections.each_entry { |section| by_section[section_key(section.name)] = [] }
48
66
  tasks.each do |task|
49
67
  assignee_section = task.assignee_section
50
68
  current_section = section_key(assignee_section.name)
@@ -56,6 +74,7 @@ module Checkoff
56
74
 
57
75
  private
58
76
 
77
+ # @return [Asana::Client]
59
78
  attr_reader :client
60
79
  end
61
80
  end
@@ -91,12 +91,14 @@ module Checkoff
91
91
  # @param [Boolean] only_uncompleted
92
92
  # @param [Array<String>] extra_fields
93
93
  # @return [Enumerable<Asana::Resources::Task>]
94
- def tasks_from_project(project, only_uncompleted: true, extra_fields: [])
94
+ def tasks_from_project(project,
95
+ only_uncompleted: true,
96
+ extra_fields: [])
95
97
  options = task_options
96
98
  options[:completed_since] = '9999-12-01' if only_uncompleted
97
99
  options[:project] = project.gid
98
100
  options[:options][:fields] += extra_fields
99
- client.tasks.find_all(**options).to_a
101
+ client.tasks.find_all(**options)
100
102
  end
101
103
  cache_method :tasks_from_project, SHORT_CACHE_TIME
102
104
 
@@ -65,17 +65,21 @@ module Checkoff
65
65
  # tasks with section name -> task list of the uncompleted tasks
66
66
  # @param workspace_name [String]
67
67
  # @param project_name [String, Symbol]
68
+ # @param only_uncompleted [Boolean]
68
69
  # @param extra_fields [Array<String>]
69
70
  # @return [Hash{[String, nil] => Enumerable<Asana::Resources::Task>}]
70
- def tasks_by_section(workspace_name, project_name, extra_fields: [])
71
+ def tasks_by_section(workspace_name,
72
+ project_name,
73
+ only_uncompleted: true,
74
+ extra_fields: [])
71
75
  raise ArgumentError, 'Provided nil workspace name' if workspace_name.nil?
72
76
  raise ArgumentError, 'Provided nil project name' if project_name.nil?
73
77
 
74
78
  project = project_or_raise(workspace_name, project_name)
75
79
  if project_name == :my_tasks
76
- my_tasks.tasks_by_section_for_my_tasks(project, extra_fields: extra_fields)
80
+ my_tasks.tasks_by_section_for_my_tasks(project, only_uncompleted: only_uncompleted, extra_fields: extra_fields)
77
81
  else
78
- tasks_by_section_for_project(project, extra_fields: extra_fields)
82
+ tasks_by_section_for_project(project, only_uncompleted: only_uncompleted, extra_fields: extra_fields)
79
83
  end
80
84
  end
81
85
 
@@ -140,10 +144,15 @@ module Checkoff
140
144
  # Given a project object, pull all tasks, then provide a Hash of
141
145
  # tasks with section name -> task list of the uncompleted tasks
142
146
  # @param project [Asana::Resources::Project]
147
+ # @param only_uncompleted [Boolean]
143
148
  # @param extra_fields [Array<String>]
144
149
  # @return [Hash<[String,nil], Enumerable<Asana::Resources::Task>>]
145
- def tasks_by_section_for_project(project, extra_fields: [])
146
- raw_tasks = projects.tasks_from_project(project, extra_fields: extra_fields)
150
+ def tasks_by_section_for_project(project,
151
+ only_uncompleted: true,
152
+ extra_fields: [])
153
+ raw_tasks = projects.tasks_from_project(project,
154
+ only_uncompleted: only_uncompleted,
155
+ extra_fields: extra_fields)
147
156
  active_tasks = projects.active_tasks(raw_tasks)
148
157
  by_section(active_tasks, project.gid)
149
158
  end
@@ -165,7 +174,7 @@ module Checkoff
165
174
  by_section = {}
166
175
  # @sg-ignore
167
176
  sections = client.sections.get_sections_for_project(project_gid: project_gid)
168
- sections.each { |section| by_section[section_key(section.name)] = [] }
177
+ sections.each_entry { |section| by_section[section_key(section.name)] = [] }
169
178
  tasks.each { |task| file_task_by_section(by_section, task, project_gid) }
170
179
  by_section
171
180
  end
@@ -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.45.1'
6
+ VERSION = '0.46.1'
7
7
  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.45.1
4
+ version: 0.46.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: 2023-08-29 00:00:00.000000000 Z
11
+ date: 2023-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -135,7 +135,9 @@ files:
135
135
  - lib/checkoff/internal/search_url.rb
136
136
  - lib/checkoff/internal/search_url/custom_field_param_converter.rb
137
137
  - lib/checkoff/internal/search_url/custom_field_variant.rb
138
+ - lib/checkoff/internal/search_url/date_param_converter.rb
138
139
  - lib/checkoff/internal/search_url/parser.rb
140
+ - lib/checkoff/internal/search_url/results_merger.rb
139
141
  - lib/checkoff/internal/search_url/simple_param_converter.rb
140
142
  - lib/checkoff/internal/task_selector_evaluator.rb
141
143
  - lib/checkoff/my_tasks.rb