Class: Checkoff::Tasks

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Includes:
Logging
Defined in:
lib/checkoff/tasks.rb

Overview

Pull tasks from Asana

Constant Summary collapse

MINUTE =
60
HOUR =
MINUTE * 60
DAY =
24 * HOUR
REALLY_LONG_CACHE_TIME =
MINUTE * 30
LONG_CACHE_TIME =
MINUTE * 15
SHORT_CACHE_TIME =
MINUTE * 5

Instance Method Summary collapse

Methods included from Logging

#debug, #error, #finer, #info, #logger, #warn

Constructor Details

#initialize(config: Checkoff::Internal::ConfigLoader.load(:asana), client: Checkoff::Clients.new(config:).client, workspaces: Checkoff::Workspaces.new(config:, client:), sections: Checkoff::Sections.new(config:, client:), portfolios: Checkoff::Portfolios.new(config:, client:), custom_fields: Checkoff::CustomFields.new(config:, client:), time_class: Time, date_class: Date, asana_task: Asana::Resources::Task) ⇒ Tasks

Returns a new instance of Tasks.

Parameters:

  • config (Hash, Checkoff::Internal::EnvFallbackConfigLoader) (defaults to: Checkoff::Internal::ConfigLoader.load(:asana))
  • client (Asana::Client) (defaults to: Checkoff::Clients.new(config:).client)
  • workspaces (Checkoff::Workspaces) (defaults to: Checkoff::Workspaces.new(config:, client:))
  • sections (Checkoff::Sections) (defaults to: Checkoff::Sections.new(config:, client:))
  • portfolios (Checkoff::Portfolios) (defaults to: Checkoff::Portfolios.new(config:, client:))
  • custom_fields (Checkoff::CustomFields) (defaults to: Checkoff::CustomFields.new(config:, client:))
  • time_class (Class<Time>) (defaults to: Time)
  • date_class (Class<Date>) (defaults to: Date)
  • asana_task (Class<Asana::Resources::Task>) (defaults to: Asana::Resources::Task)


44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/checkoff/tasks.rb', line 44

def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana),
               client: Checkoff::Clients.new(config:).client,
               workspaces: Checkoff::Workspaces.new(config:,
                                                    client:),
               sections: Checkoff::Sections.new(config:,
                                                client:),
               portfolios: Checkoff::Portfolios.new(config:,
                                                    client:),
               custom_fields: Checkoff::CustomFields.new(config:,
                                                         client:),
               time_class: Time,
               date_class: Date,
               asana_task: Asana::Resources::Task)
  @config = config
  @sections = sections
  @time_class = time_class
  @date_class = date_class
  @asana_task = asana_task
  @client = client
  @portfolios = portfolios
  @custom_fields = custom_fields
  @workspaces = workspaces
  @timing = Checkoff::Timing.new(today_getter: date_class, now_getter: time_class)
end

Instance Method Details

#add_task(name, workspace_gid: @workspaces.default_workspace_gid, assignee_gid: default_assignee_gid) ⇒ Asana::Resources::Task

Add a task

Parameters:

  • name (String)
  • workspace_gid (String) (defaults to: @workspaces.default_workspace_gid)
  • assignee_gid (String) (defaults to: default_assignee_gid)

Returns:

  • (Asana::Resources::Task)


184
185
186
187
188
189
190
# File 'lib/checkoff/tasks.rb', line 184

def add_task(name,
             workspace_gid: @workspaces.default_workspace_gid,
             assignee_gid: default_assignee_gid)
  @asana_task.create(client,
                     assignee: assignee_gid,
                     workspace: workspace_gid, name:)
end

#all_dependent_tasks(task, extra_task_fields: []) ⇒ Array<Asana::Resources::Task>

Parameters:

  • task (Asana::Resources::Task)
  • extra_task_fields (Array<String>) (defaults to: [])

Returns:

  • (Array<Asana::Resources::Task>)


234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/checkoff/tasks.rb', line 234

def all_dependent_tasks(task, extra_task_fields: [])
  # @type [Array<Asana::Resources::Task>]
  dependent_tasks = T.let([], T::Array[Asana::Resources::Task])
  # See note above - same applies as does in @dependencies
  #
  # @type [Array<Hash>]
  dependents = task.instance_variable_get(:@dependents) || []
  dependents.each do |dependent_task_hash_or_obj|
    # seems like if we ever .inspect the task, it stashes the task
    # object instead of the hash.  Try to avoid this - but maybe we
    # need to convert if it does happen.
    raise 'Found dependent task object!' if dependent_task_hash_or_obj.is_a?(Asana::Resources::Task)

    dependent_task_hash = dependent_task_hash_or_obj

    dependent_task = task_by_gid(dependent_task_hash.fetch('gid'),
                                 only_uncompleted: true,
                                 extra_fields: ['dependents'] + extra_task_fields)
    debug { "#{task.name} has dependent task #{T.must(dependent_task).name}" }
    next if dependent_task.nil?

    dependent_tasks << dependent_task
    dependent_tasks += all_dependent_tasks(dependent_task,
                                           extra_task_fields:)
  end
  dependent_tasks
end

#as_cache_keyHash

Returns:

  • (Hash)


326
327
328
# File 'lib/checkoff/tasks.rb', line 326

def as_cache_key
  {}
end

#date_or_time_field_by_name(task, field_name) ⇒ Date, ...

Parameters:

  • task (Asana::Resources::Task)
  • field_name (Symbol, Array)

    :start - start_at or start_on (first set) :due - due_at or due_on (first set) :ready - start_at or start_on or due_at or due_on (first set) :modified - modified_at

    :custom_field, “foo”
    • ‘Date’ custom field type named ‘foo’

Returns:

  • (Date, Time, nil)


107
108
109
# File 'lib/checkoff/tasks.rb', line 107

def date_or_time_field_by_name(task, field_name)
  task_timing.date_or_time_field_by_name(task, field_name)
end

#gid_for_task(workspace_name, project_name, section_name, task_name) ⇒ String?

@sg-ignore

Parameters:

  • workspace_name (String)
  • project_name (String, Symbol)
  • section_name (String, nil, Symbol)
  • task_name (String)

Returns:

  • (String, nil)


144
145
146
147
148
149
150
151
152
# File 'lib/checkoff/tasks.rb', line 144

def gid_for_task(workspace_name, project_name, section_name, task_name)
  # @sg-ignore
  t = tasks(workspace_name,
            project_name,
            section_name:,
            only_uncompleted: false)
  task_for_gid = t.find { |task| task.name == task_name }
  task_for_gid&.gid
end

#h_to_task(task_data) ⇒ Asana::Resources::Task

Parameters:

  • task_data (Hash)

Returns:

  • (Asana::Resources::Task)


283
284
285
# File 'lib/checkoff/tasks.rb', line 283

def h_to_task(task_data)
  task_hashes.h_to_task(task_data, client:)
end

#in_period?(task, field_name, period) ⇒ Boolean

Parameters:

  • task (Asana::Resources::Task)
  • field_name (Symbol, Array)
  • period (Symbol, Array)

    :now_or_before, :this_week - See Checkoff::Timing#in_period?_

Returns:

  • (Boolean)


91
92
93
94
95
96
# File 'lib/checkoff/tasks.rb', line 91

def in_period?(task, field_name, period)
  # @type [Date,Time,nil]
  task_date_or_time = task_timing.date_or_time_field_by_name(task, field_name)

  timing.in_period?(task_date_or_time, period)
end

#in_portfolio_more_than_once?(task, portfolio_name, workspace_name: @workspaces.default_workspace.name) ⇒ Boolean

True if the task is in a project which is in the given portfolio

Parameters:

  • task (Asana::Resources::Task)
  • portfolio_name (String)
  • workspace_name (String) (defaults to: @workspaces.default_workspace.name)

Returns:

  • (Boolean)


309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/checkoff/tasks.rb', line 309

def in_portfolio_more_than_once?(task,
                                 portfolio_name,
                                 workspace_name: @workspaces.default_workspace.name)
  portfolio_projects = @portfolios.projects_in_portfolio(workspace_name, portfolio_name)
  portfolio_project_gids = portfolio_projects.map(&:gid)
  seen = T.let(false, T::Boolean)
  task.memberships.each do |membership|
    project_gid = membership.fetch('project').fetch('gid')
    next unless portfolio_project_gids.include?(project_gid)
    return true if seen

    seen = true
  end
  false
end

#in_portfolio_named?(task, portfolio_name, workspace_name: @workspaces.default_workspace.name) ⇒ Boolean

True if the task is in a project which is in the given portfolio

Parameters:

  • task (Asana::Resources::Task)
  • portfolio_name (String)
  • workspace_name (String) (defaults to: @workspaces.default_workspace.name)

Returns:

  • (Boolean)


292
293
294
295
296
297
298
299
300
301
302
# File 'lib/checkoff/tasks.rb', line 292

def in_portfolio_named?(task,
                        portfolio_name,
                        workspace_name: @workspaces.default_workspace.name)
  portfolio_projects = @portfolios.projects_in_portfolio(workspace_name, portfolio_name)
  task.memberships.any? do |membership|
    project_gid = membership.fetch('project').fetch('gid')
    portfolio_projects.any? do |portfolio_project|
      portfolio_project.gid == project_gid
    end
  end
end

#incomplete_dependencies?(task) ⇒ Boolean

True if any of the task’s dependencies are marked incomplete

Include ‘dependencies.gid’ in extra_fields of task passed in.

Parameters:

  • task (Asana::Resources::Task)

Returns:

  • (Boolean)


206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/checkoff/tasks.rb', line 206

def incomplete_dependencies?(task)
  # Avoid a redundant fetch.  Unfortunately, Ruby SDK allows
  # dependencies to be fetched along with other attributes--but
  # then doesn't use it and does another HTTP GET!  At least this
  # way we can skip the extra HTTP GET in the common case when
  # there are no dependencies.
  #
  # https://github.com/Asana/ruby-asana/issues/125

  # @sg-ignore
  # @type [Enumerable<Asana::Resources::Task>, nil]
  dependencies = task.instance_variable_get(:@dependencies) || []

  dependencies.any? do |parent_task_info|
    # the real bummer though is that asana doesn't let you fetch
    # the completion status of dependencies, so we need to do this
    # regardless:
    parent_task_gid = parent_task_info.fetch('gid')

    parent_task = task_by_gid(parent_task_gid, only_uncompleted: false)
    T.must(parent_task).completed_at.nil?
  end
end

#task(workspace_name, project_name, task_name, section_name: :unspecified, only_uncompleted: true, extra_fields: []) ⇒ Asana::Resources::Task?

Pull a specific task by name

@sg-ignore

Parameters:

  • workspace_name (String)
  • project_name (String, Symbol)
  • section_name (String, nil, Symbol) (defaults to: :unspecified)
  • task_name (String)
  • only_uncompleted (Boolean) (defaults to: true)
  • extra_fields (Array<String>) (defaults to: [])

Returns:

  • (Asana::Resources::Task, nil)


121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/checkoff/tasks.rb', line 121

def task(workspace_name, project_name, task_name,
         section_name: :unspecified,
         only_uncompleted: true,
         extra_fields: [])
  thread_local = Checkoff::Internal::ThreadLocal.new
  # @type [String]
  task_gid = thread_local.with_thread_local_variable(:suppress_asana_webhook_watch_creation,
                                                     true) do
    gid_for_task(workspace_name, project_name, section_name, task_name)
  end
  return nil if task_gid.nil?

  task_by_gid(task_gid, only_uncompleted:, extra_fields:)
end

#task_by_gid(task_gid, extra_fields: [], only_uncompleted: true) ⇒ Asana::Resources::Task?

Pull a specific task by GID

Parameters:

  • task_gid (String)
  • extra_fields (Array<String>) (defaults to: [])
  • only_uncompleted (Boolean) (defaults to: true)

Returns:

  • (Asana::Resources::Task, nil)


162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/checkoff/tasks.rb', line 162

def task_by_gid(task_gid,
                extra_fields: [],
                only_uncompleted: true)
  all_options = projects.task_options(extra_fields:,
                                      only_uncompleted:)
  # @type [Hash]
  options = all_options.fetch(:options, {})
  options[:completed_since] = all_options[:completed_since] unless all_options[:completed_since].nil?
  client.tasks.find_by_id(task_gid, options:)
rescue Asana::Errors::NotFound => e
  debug e
  nil
end

#task_ready?(task, period: :now_or_before, ignore_dependencies: false) ⇒ Boolean

Indicates a task is ready for a person to work on it. This is subtly different than what is used by Asana to mark a date as red/green! A task is ready if it is not dependent on an incomplete task and one of these is true:

  • start is null and due on is today

  • start is null and due at is before now

  • start on is today

  • start at is before now

Parameters:

  • task (Asana::Resources::Task)
  • period (Symbol) (defaults to: :now_or_before)
    • :now_or_before or :this_week

  • ignore_dependencies (Boolean) (defaults to: false)

Returns:

  • (Boolean)


82
83
84
85
86
# File 'lib/checkoff/tasks.rb', line 82

def task_ready?(task, period: :now_or_before, ignore_dependencies: false)
  return false if !ignore_dependencies && incomplete_dependencies?(task)

  in_period?(task, :ready, period)
end

#task_to_h(task) ⇒ Hash

Builds on the standard API representation of an Asana task with some convenience keys:

<regular keys from API response> + unwrapped:

membership_by_section_gid: Hash{String => Hash (membership)>
membership_by_project_gid: Hash{String => Hash (membership)>
membership_by_project_name: Hash{String => Hash (membership)>

task: String (name)

Parameters:

  • task (Asana::Resources::Task)

Returns:

  • (Hash)


276
277
278
# File 'lib/checkoff/tasks.rb', line 276

def task_to_h(task)
  task_hashes.task_to_h(task)
end

#url_of_task(task) ⇒ String

Return user-accessible URL for a task

Parameters:

  • task (Asana::Resources::Task)

Returns:

  • (String)

    end-user URL to the task in question



197
198
199
# File 'lib/checkoff/tasks.rb', line 197

def url_of_task(task)
  "https://app.asana.com/0/0/#{task.gid}/f"
end