checkoff 0.97.0 → 0.98.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: 3b1e38f1b4d152c8c28b2ebb2c5a946abe7e2d95ce9650bdf708ecf177afd424
4
- data.tar.gz: 55449cde16e497f2332af45b7a8301c926e227c0732c7f0dc4cb9b837ecbf888
3
+ metadata.gz: 2725cd346d11c05b4742b3dd59d1202064a3a9036ec37c2037332a02f93165eb
4
+ data.tar.gz: b02da55d5036d05eee1c3fb58237cf42e68c28b788be66cd0f41a07647b9e6eb
5
5
  SHA512:
6
- metadata.gz: 2d8995a72761c9cba456c3c5da9e1cf57d6693018005ccfec14e34db8f9ea82c226628b4794f09fd28495c5b273a06ad1088f6bb5fe01d505d05487c8a754d66
7
- data.tar.gz: f295d9173c6e605f250b39a4572cc0148d1a720f10ca0bae3a5157504c5f36003e2f08012e75877fc0dc60f298e8d3916acd41ac03a6f7beb4e45fafd5725c28
6
+ metadata.gz: a8a4286f693c686211aa41db250b7d3825b9a846a923e53f7327ca0d94ba714f7b94ebfa8ec55964506721d4912d419318e8c2e94ef147b0ed586151a5b5d252
7
+ data.tar.gz: 1c1d1f4a88a447e27f3bc9beaf10ff6fec97f0b55ced249eb7cdfbcd2e36955bb5230fb3991339eecd19f44f6b5d9c2ef646746a6ee175b5d4db2d1964305f00
data/Gemfile.lock CHANGED
@@ -12,7 +12,7 @@ GIT
12
12
  PATH
13
13
  remote: .
14
14
  specs:
15
- checkoff (0.97.0)
15
+ checkoff (0.98.0)
16
16
  activesupport
17
17
  asana (> 0.10.0)
18
18
  cache_method
@@ -42,6 +42,8 @@
42
42
  # module Resources
43
43
  # # https://developers.asana.com/reference/gettask
44
44
  # class Task
45
+ # # @return [String]
46
+ # def resource_subtype; end
45
47
  # # @return [String,nil]
46
48
  # def due_at; end
47
49
  # # @return [String,nil]
@@ -136,6 +138,12 @@
136
138
  # # @param project_gid [String]
137
139
  # # @return [Enumerable<Asana::Resources::Section>]
138
140
  # def get_sections_for_project(project_gid:, options: {}); end
141
+ # # Returns the complete record for a single section.
142
+ # #
143
+ # # @param [String] id - The section to get.
144
+ # # @param options [Hash] - the request I/O options.
145
+ # # @return [Asana::Resources::Section]
146
+ # def find_by_id(id, options: {}); end
139
147
  # end
140
148
  # class Project
141
149
  # # Returns the compact project records for all projects in the workspace.
@@ -9,10 +9,13 @@ module Checkoff
9
9
  class FunctionEvaluator < ::Checkoff::SelectorClasses::FunctionEvaluator
10
10
  # @param selector [Array<(Symbol, Array)>,String]
11
11
  # @param tasks [Checkoff::Tasks]
12
+ # @param timelines [Checkoff::Timelines]
12
13
  def initialize(selector:,
13
- tasks:)
14
+ tasks:,
15
+ timelines:)
14
16
  @selector = selector
15
17
  @tasks = tasks
18
+ @timelines = timelines
16
19
  super()
17
20
  end
18
21
 
@@ -353,6 +353,26 @@ module Checkoff
353
353
  estimate_hours > allocated_hours
354
354
  end
355
355
  end
356
+
357
+ # :dependent_on_previous_section_last_milestone
358
+ class DependentOnPreviousSectionLastMilestoneFunctionEvaluator < FunctionEvaluator
359
+ FUNCTION_NAME = :dependent_on_previous_section_last_milestone
360
+
361
+ def matches?
362
+ fn?(selector, FUNCTION_NAME)
363
+ end
364
+
365
+ # @param task [Asana::Resources::Task]
366
+ # @param project_name [String]
367
+ # @param limit_to_portfolio_gid [String, nil] If specified,
368
+ # only projects in this portfolio will be evaluated.
369
+ #
370
+ # @return [Boolean]
371
+ def evaluate(task, limit_to_portfolio_gid: nil)
372
+ @timelines.task_dependent_on_previous_section_last_milestone?(task,
373
+ limit_to_portfolio_gid: limit_to_portfolio_gid)
374
+ end
375
+ end
356
376
  end
357
377
  end
358
378
  end
@@ -9,10 +9,13 @@ module Checkoff
9
9
  class TaskSelectorEvaluator < SelectorEvaluator
10
10
  # @param task [Asana::Resources::Task]
11
11
  # @param tasks [Checkoff::Tasks]
12
+ # @param timelines [Checkoff::Timelines]
12
13
  def initialize(task:,
13
- tasks: Checkoff::Tasks.new)
14
+ tasks: Checkoff::Tasks.new,
15
+ timelines: Checkoff::Timelines.new)
14
16
  @item = task
15
17
  @tasks = tasks
18
+ @timelines = timelines
16
19
  super()
17
20
  end
18
21
 
@@ -35,12 +38,14 @@ module Checkoff
35
38
 
36
39
  # @return [Hash]
37
40
  def initializer_kwargs
38
- { tasks: tasks }
41
+ { tasks: tasks, timelines: timelines }
39
42
  end
40
43
 
41
44
  # @return [Asana::Resources::Task]
42
45
  attr_reader :item
43
46
  # @return [Checkoff::Tasks]
44
47
  attr_reader :tasks
48
+ # @return [Checkoff::Timelines]
49
+ attr_reader :timelines
45
50
  end
46
51
  end
@@ -56,13 +56,23 @@ module Checkoff
56
56
  #
57
57
  # @return [Enumerable<Asana::Resources::Section>]
58
58
  def sections_or_raise(workspace_name, project_name, extra_fields: [])
59
- fields = %w[name] + extra_fields
60
59
  project = project_or_raise(workspace_name, project_name)
61
- client.sections.get_sections_for_project(project_gid: project.gid,
62
- options: { fields: fields })
60
+ sections_by_project_gid(project.gid, extra_fields: extra_fields)
63
61
  end
64
62
  cache_method :sections_or_raise, SHORT_CACHE_TIME
65
63
 
64
+ # Returns a list of Asana API section objects for a given project GID
65
+ # @param project_gid [String]
66
+ # @param extra_fields [Array<String>]
67
+ #
68
+ # @return [Enumerable<Asana::Resources::Section>]
69
+ def sections_by_project_gid(project_gid, extra_fields: [])
70
+ fields = %w[name] + extra_fields
71
+ client.sections.get_sections_for_project(project_gid: project_gid,
72
+ options: { fields: fields })
73
+ end
74
+ cache_method :sections_by_project_gid, SHORT_CACHE_TIME
75
+
66
76
  # Given a workspace name and project name, then provide a Hash of
67
77
  # tasks with section name -> task list of the uncompleted tasks
68
78
  # @param workspace_name [String]
@@ -85,6 +95,20 @@ module Checkoff
85
95
  end
86
96
  end
87
97
 
98
+ # @param section_gid [String]
99
+ # @param only_uncompleted [Boolean]
100
+ # @param extra_fields [Array<String>]
101
+ #
102
+ # @return [Enumerable<Asana::Resources::Task>]
103
+ def tasks_by_section_gid(section_gid,
104
+ only_uncompleted: true,
105
+ extra_fields: [])
106
+ options = projects.task_options
107
+ options[:options][:fields] += extra_fields
108
+ options[:completed_since] = '9999-12-01' if only_uncompleted
109
+ client.tasks.get_tasks(section: section_gid, **options)
110
+ end
111
+
88
112
  # XXX: Rename to section_tasks
89
113
  #
90
114
  # Pulls task objects from a specified section
@@ -147,8 +171,45 @@ module Checkoff
147
171
  name
148
172
  end
149
173
 
174
+ # @param section [Asana::Resources::Section]
175
+ #
176
+ # @return [Asana::Resources::Section, nil]
177
+ def previous_section(section)
178
+ sections = sections_by_project_gid(section.project.fetch('gid'))
179
+
180
+ # @type [Array<Asana::Resources::Section>]
181
+ sections = sections.to_a
182
+
183
+ index = sections.find_index { |s| s.gid == section.gid }
184
+ return nil if index.nil? || index.zero?
185
+
186
+ sections[index - 1]
187
+ end
188
+ cache_method :previous_section, SHORT_CACHE_TIME
189
+
190
+ # @param gid [String]
191
+ #
192
+ # @return [Asana::Resources::Section]
193
+ def section_by_gid(gid)
194
+ options = {}
195
+ Asana::Resources::Section.new(parse(client.get("/sections/#{gid}", options: options)).first,
196
+ client: client)
197
+ end
198
+
150
199
  private
151
200
 
201
+ # https://github.com/Asana/ruby-asana/blob/master/lib/asana/resource_includes/response_helper.rb#L7
202
+ # @param response [Faraday::Response]
203
+ #
204
+ # @return [Array<Hash, Hash>]
205
+ def parse(response)
206
+ data = response.body.fetch('data') do
207
+ raise("Unexpected response body: #{response.body}")
208
+ end
209
+ extra = response.body.except('data')
210
+ [data, extra]
211
+ end
212
+
152
213
  # @return [Asana::Client]
153
214
  attr_reader :client
154
215
 
@@ -188,6 +249,7 @@ module Checkoff
188
249
  # @param project_gid [String]
189
250
  # @return [void]
190
251
  def file_task_by_section(by_section, task, project_gid)
252
+ # @sg-ignore
191
253
  # @type [Array<Hash>]
192
254
  membership = task.memberships.find { |m| m['project']['gid'] == project_gid }
193
255
  raise "Could not find task in project_gid #{project_gid}: #{task}" if membership.nil?
@@ -23,12 +23,16 @@ module Checkoff
23
23
  # @param [Hash] config
24
24
  # @param [Asana::Client] client
25
25
  # @param [Checkoff::Tasks] tasks
26
+ # @param [Checkoff::Timelines] timelines
26
27
  def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana),
27
28
  client: Checkoff::Clients.new(config: config).client,
28
29
  tasks: Checkoff::Tasks.new(config: config,
29
- client: client))
30
+ client: client),
31
+ timelines: Checkoff::Timelines.new(config: config,
32
+ client: client))
30
33
  @config = config
31
34
  @tasks = tasks
35
+ @timelines = timelines
32
36
  end
33
37
 
34
38
  # @param [Asana::Resources::Task] task
@@ -36,7 +40,7 @@ module Checkoff
36
40
  # task details. Examples: [:tag, 'foo'] [:not, [:tag, 'foo']] [:tag, 'foo']
37
41
  # @return [Boolean]
38
42
  def filter_via_task_selector(task, task_selector)
39
- evaluator = TaskSelectorEvaluator.new(task: task, tasks: tasks)
43
+ evaluator = TaskSelectorEvaluator.new(task: task, tasks: tasks, timelines: timelines)
40
44
  evaluator.evaluate(task_selector)
41
45
  end
42
46
 
@@ -45,6 +49,9 @@ module Checkoff
45
49
  # @return [Checkoff::Tasks]
46
50
  attr_reader :tasks
47
51
 
52
+ # @return [Checkoff::Timelines]
53
+ attr_reader :timelines
54
+
48
55
  # bundle exec ./task_selectors.rb
49
56
  # :nocov:
50
57
  class << self
@@ -0,0 +1,121 @@
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 'clients'
10
+
11
+ # https://developers.asana.com/reference/timelines
12
+
13
+ module Checkoff
14
+ # Manages timelines of dependent tasks with dates and milestones
15
+ class Timelines
16
+ # @!parse
17
+ # extend CacheMethod::ClassMethods
18
+
19
+ MINUTE = 60
20
+ HOUR = MINUTE * 60
21
+ DAY = 24 * HOUR
22
+ REALLY_LONG_CACHE_TIME = HOUR * 1
23
+ LONG_CACHE_TIME = MINUTE * 15
24
+ SHORT_CACHE_TIME = MINUTE
25
+
26
+ # @param config [Hash]
27
+ # @param workspaces [Checkoff::Workspaces]
28
+ # @param sections [Checkoff::Sections]
29
+ # @param tasks [Checkoff::Tasks]
30
+ # @param clients [Checkoff::Clients]
31
+ # @param client [Asana::Client]
32
+ def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana),
33
+ workspaces: Checkoff::Workspaces.new(config: config),
34
+ sections: Checkoff::Sections.new(config: config),
35
+ tasks: Checkoff::Tasks.new(config: config),
36
+ clients: Checkoff::Clients.new(config: config),
37
+ client: clients.client)
38
+ @workspaces = workspaces
39
+ @sections = sections
40
+ @tasks = tasks
41
+ @client = client
42
+ end
43
+
44
+ # @param task [Asana::Resources::Task]
45
+ # @param limit_to_portfolio_gid [String, nil]
46
+ # @param project_name [String]
47
+ def task_dependent_on_previous_section_last_milestone?(task, limit_to_portfolio_gid: nil)
48
+ task_data = @tasks.task_to_h(task)
49
+ # @sg-ignore
50
+ # @type [Array<Hash{String => Hash{String => String}}>]
51
+ memberships_data = task_data.fetch('memberships')
52
+ memberships_data.all? do |membership_data|
53
+ # @type [Hash{String => String}]
54
+ section_data = membership_data.fetch('section')
55
+ section_gid = section_data.fetch('gid')
56
+ section = @sections.section_by_gid(section_gid)
57
+ task_data_dependent_on_previous_section_last_milestone?(task_data, section)
58
+ end
59
+ end
60
+
61
+ # @param section_gid [String]
62
+ #
63
+ # @return [Asana::Resources::Task,nil]
64
+ def last_milestone_in_section(section_gid)
65
+ # @type [Array<Asana::Resources::Task>]
66
+ task_list = @sections.tasks_by_section_gid(section_gid).to_a
67
+ last_task = task_list.last
68
+ last_task&.resource_subtype == 'milestone' ? last_task : nil
69
+ end
70
+
71
+ private
72
+
73
+ # @param task_data [Hash]
74
+ # @param section [Asana::Resources::Section]
75
+ #
76
+ # @return [Boolean]
77
+ def task_data_dependent_on_previous_section_last_milestone?(task_data, section)
78
+ # @sg-ignore
79
+ # @type [Array<Hash{String => String}>]
80
+ dependencies = task_data.fetch('dependencies')
81
+ return false if dependencies.empty?
82
+
83
+ previous_section = @sections.previous_section(section)
84
+ return false if previous_section.nil?
85
+
86
+ previous_section_last_milestone = last_milestone_in_section(previous_section.gid)
87
+ return false if previous_section_last_milestone.nil?
88
+
89
+ dependencies.any? { |dependency| dependency.fetch('gid') == previous_section_last_milestone.gid }
90
+ end
91
+
92
+ # @return [Checkoff::Workspaces]
93
+ attr_reader :workspaces
94
+
95
+ # @return [Asana::Client]
96
+ attr_reader :client
97
+
98
+ # bundle exec ./timelines.rb
99
+ # :nocov:
100
+ class << self
101
+ # @return [void]
102
+ def run
103
+ # @sg-ignore
104
+ # @type [String]
105
+ # workspace_name = ARGV[0] || raise('Please pass workspace name as first argument')
106
+ # @sg-ignore
107
+ # @type [String]
108
+ # timeline_name = ARGV[1] || raise('Please pass timeline name as second argument')
109
+ # timelines = Checkoff::Timelines.new
110
+ # timeline = timelines.timeline_or_raise(workspace_name, timeline_name)
111
+ # puts "Results: #{timeline}"
112
+ end
113
+ end
114
+ # :nocov:
115
+ end
116
+ end
117
+
118
+ # :nocov:
119
+ abs_program_name = File.expand_path($PROGRAM_NAME)
120
+ Checkoff::Timelines.run if abs_program_name == File.expand_path(__FILE__)
121
+ # :nocov:
@@ -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.97.0'
6
+ VERSION = '0.98.0'
7
7
  end
data/lib/checkoff.rb CHANGED
@@ -8,6 +8,7 @@ require 'checkoff/projects'
8
8
  require 'checkoff/sections'
9
9
  require 'checkoff/subtasks'
10
10
  require 'checkoff/timing'
11
+ require 'checkoff/timelines'
11
12
  require 'checkoff/tasks'
12
13
  require 'checkoff/custom_fields'
13
14
  require 'checkoff/tags'
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.97.0
4
+ version: 0.98.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: 2023-10-25 00:00:00.000000000 Z
11
+ date: 2023-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -166,6 +166,7 @@ files:
166
166
  - lib/checkoff/task_searches.rb
167
167
  - lib/checkoff/task_selectors.rb
168
168
  - lib/checkoff/tasks.rb
169
+ - lib/checkoff/timelines.rb
169
170
  - lib/checkoff/timing.rb
170
171
  - lib/checkoff/version.rb
171
172
  - lib/checkoff/workspaces.rb