checkoff 0.45.1 → 0.46.1

Sign up to get free protection for your applications and to get access to all the features.
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