checkoff 0.97.0 → 0.98.0

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: 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