checkoff 0.9.0 → 0.12.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.
@@ -13,10 +13,6 @@ module Checkoff
13
13
  @yaml_filename = yaml_filename
14
14
  end
15
15
 
16
- def envvar_name(key)
17
- "#{@envvar_prefix}__#{key.upcase}"
18
- end
19
-
20
16
  def [](key)
21
17
  config_value = @config[key]
22
18
  return config_value unless config_value.nil?
@@ -31,25 +27,35 @@ module Checkoff
31
27
  raise KeyError,
32
28
  "Please configure either the #{key} key in #{@yaml_filename} or set #{envvar_name(key)}"
33
29
  end
30
+
31
+ private
32
+
33
+ def envvar_name(key)
34
+ "#{@envvar_prefix}__#{key.upcase}"
35
+ end
34
36
  end
35
37
 
36
38
  # Load configuration file
37
39
  class ConfigLoader
38
- def self.yaml_filename(sym)
39
- file = "#{sym}.yml"
40
- File.expand_path("~/.#{file}")
41
- end
40
+ class << self
41
+ def load(sym)
42
+ yaml_result = load_yaml_file(sym)
43
+ EnvFallbackConfigLoader.new(yaml_result, sym, yaml_filename(sym))
44
+ end
42
45
 
43
- def self.load_yaml_file(sym)
44
- filename = yaml_filename(sym)
45
- return {} unless File.exist?(filename)
46
+ private
46
47
 
47
- YAML.load_file(filename).with_indifferent_access
48
- end
48
+ def load_yaml_file(sym)
49
+ filename = yaml_filename(sym)
50
+ return {} unless File.exist?(filename)
51
+
52
+ YAML.load_file(filename).with_indifferent_access
53
+ end
49
54
 
50
- def self.load(sym)
51
- yaml_result = load_yaml_file(sym)
52
- EnvFallbackConfigLoader.new(yaml_result, sym, yaml_filename(sym))
55
+ def yaml_filename(sym)
56
+ file = "#{sym}.yml"
57
+ File.expand_path("~/.#{file}")
58
+ end
53
59
  end
54
60
  end
55
61
  end
@@ -22,8 +22,6 @@ module Checkoff
22
22
  LONG_CACHE_TIME = MINUTE * 15
23
23
  SHORT_CACHE_TIME = MINUTE * 5
24
24
 
25
- # XXX: Move low-level functions private
26
-
27
25
  def initialize(config: Checkoff::ConfigLoader.load(:asana),
28
26
  asana_client: Asana::Client,
29
27
  workspaces: Checkoff::Workspaces.new(config: config))
@@ -32,30 +30,23 @@ module Checkoff
32
30
  @workspaces = workspaces
33
31
  end
34
32
 
33
+ # Returns Asana Ruby API Client object
35
34
  def client
36
35
  @workspaces.client
37
36
  end
38
37
 
39
- def projects
40
- client.projects
41
- end
42
- cache_method :projects, LONG_CACHE_TIME
43
-
44
- def projects_by_workspace_name(workspace_name)
45
- workspace = @workspaces.workspace_by_name(workspace_name)
46
- raise "Could not find workspace named #{workspace_name}" unless workspace
47
-
48
- projects.find_by_workspace(workspace: workspace.gid)
49
- end
50
-
51
- def my_tasks(workspace_name)
52
- workspace = @workspaces.workspace_by_name(workspace_name)
53
- result = client.user_task_lists.get_user_task_list_for_user(user_gid: 'me',
54
- workspace: workspace.gid)
55
- gid = result.gid
56
- projects.find_by_id(gid)
38
+ # Default options used in Asana API to pull taskso
39
+ def task_options
40
+ {
41
+ per_page: 100,
42
+ options: {
43
+ fields: %w[name completed_at due_at due_on tags
44
+ memberships.project.gid memberships.section.name dependencies],
45
+ },
46
+ }
57
47
  end
58
48
 
49
+ # pulls an Asana API project class given a name
59
50
  def project(workspace_name, project_name)
60
51
  if project_name.is_a?(Symbol) && project_name.to_s.start_with?('my_tasks')
61
52
  my_tasks(workspace_name)
@@ -68,20 +59,12 @@ module Checkoff
68
59
  end
69
60
  cache_method :project, LONG_CACHE_TIME
70
61
 
62
+ # find uncompleted tasks in a list
71
63
  def active_tasks(tasks)
72
64
  tasks.select { |task| task.completed_at.nil? }
73
65
  end
74
66
 
75
- def task_options
76
- {
77
- per_page: 100,
78
- options: {
79
- fields: %w[name completed_at due_at due_on assignee_status tags
80
- memberships.project.gid memberships.section.name dependencies],
81
- },
82
- }
83
- end
84
-
67
+ # pull task objects from a named project
85
68
  def tasks_from_project(project, only_uncompleted: true, extra_fields: [])
86
69
  options = task_options
87
70
  options[:completed_since] = '9999-12-01' if only_uncompleted
@@ -90,5 +73,27 @@ module Checkoff
90
73
  client.tasks.find_all(**options).to_a
91
74
  end
92
75
  cache_method :tasks_from_project, SHORT_CACHE_TIME
76
+
77
+ private
78
+
79
+ def projects
80
+ client.projects
81
+ end
82
+ cache_method :projects, LONG_CACHE_TIME
83
+
84
+ def projects_by_workspace_name(workspace_name)
85
+ workspace = @workspaces.workspace_by_name(workspace_name)
86
+ raise "Could not find workspace named #{workspace_name}" unless workspace
87
+
88
+ projects.find_by_workspace(workspace: workspace.gid)
89
+ end
90
+
91
+ def my_tasks(workspace_name)
92
+ workspace = @workspaces.workspace_by_name(workspace_name)
93
+ result = client.user_task_lists.get_user_task_list_for_user(user_gid: 'me',
94
+ workspace: workspace.gid)
95
+ gid = result.gid
96
+ projects.find_by_id(gid)
97
+ end
93
98
  end
94
99
  end
@@ -4,7 +4,6 @@ require 'forwardable'
4
4
 
5
5
  module Checkoff
6
6
  # Query different sections of Asana projects
7
- # rubocop:disable Metrics/ClassLength
8
7
  class Sections
9
8
  MINUTE = 60
10
9
  LONG_CACHE_TIME = MINUTE * 15
@@ -23,68 +22,74 @@ module Checkoff
23
22
  @time = time
24
23
  end
25
24
 
26
- def_delegators :@projects, :client
27
-
28
- def file_task_by_section(by_section, task, project_gid)
29
- membership = task.memberships.find { |m| m['project']['gid'] == project_gid }
30
- raise "Could not find task in project_gid #{project_gid}: #{task}" if membership.nil?
25
+ # Returns a list of Asana API section objects for a given project
26
+ def sections_or_raise(workspace_name, project_name)
27
+ project = project_or_raise(workspace_name, project_name)
28
+ client.sections.get_sections_for_project(project_gid: project.gid)
29
+ end
31
30
 
32
- current_section = membership['section']['name']
33
- current_section = nil if current_section == '(no section)'
34
- by_section[current_section] ||= []
35
- by_section[current_section] << task
31
+ # Given a workspace name and project name, then provide a Hash of
32
+ # tasks with section name -> task list of the uncompleted tasks
33
+ def tasks_by_section(workspace_name, project_name)
34
+ project = project_or_raise(workspace_name, project_name)
35
+ tasks_by_section_for_project(project)
36
36
  end
37
+ cache_method :tasks_by_section, SHORT_CACHE_TIME
37
38
 
38
- def by_section(tasks, project_gid)
39
- by_section = {}
40
- tasks.each do |task|
41
- file_task_by_section(by_section, task, project_gid)
42
- end
43
- by_section
39
+ # XXX: Rename to section_tasks
40
+ #
41
+ # Pulls task objects from a specified section
42
+ def tasks(workspace_name, project_name, section_name,
43
+ only_uncompleted: true,
44
+ extra_fields: [])
45
+ section = section_or_raise(workspace_name, project_name, section_name)
46
+ options = projects.task_options
47
+ # asana-0.10.3 gem doesn't support per_page - not sure if API
48
+ # itself does
49
+ options.delete(:per_page)
50
+ options[:options][:fields] += extra_fields
51
+ client.tasks.get_tasks_for_section(section_gid: section.gid,
52
+ **options).to_a
44
53
  end
45
- cache_method :by_section, LONG_CACHE_TIME
54
+ cache_method :tasks, SHORT_CACHE_TIME
46
55
 
47
- def legacy_tasks_by_section_for_project(project)
48
- raw_tasks = projects.tasks_from_project(project)
49
- active_tasks = projects.active_tasks(raw_tasks)
50
- legacy_by_section(active_tasks)
56
+ # Pulls just names of tasks from a given section.
57
+ def section_task_names(workspace_name, project_name, section_name)
58
+ tasks = tasks(workspace_name, project_name, section_name)
59
+ tasks.map(&:name)
51
60
  end
61
+ cache_method :section_task_names, SHORT_CACHE_TIME
62
+
63
+ private
52
64
 
65
+ # Given a project object, pull all tasks, then provide a Hash of
66
+ # tasks with section name -> task list of the uncompleted tasks
53
67
  def tasks_by_section_for_project(project)
54
68
  raw_tasks = projects.tasks_from_project(project)
55
69
  active_tasks = projects.active_tasks(raw_tasks)
56
70
  by_section(active_tasks, project.gid)
57
71
  end
58
72
 
59
- def legacy_file_task_by_section(current_section, by_section, task)
60
- if task.name =~ /:$/
61
- current_section = task.name
62
- by_section[current_section] = []
63
- else
64
- by_section[current_section] ||= []
65
- by_section[current_section] << task
66
- end
67
- [current_section, by_section]
68
- end
73
+ def_delegators :@projects, :client
69
74
 
70
- def legacy_by_section(tasks)
71
- current_section = nil
75
+ # Given a list of tasks, pull a Hash of tasks with section name -> task list
76
+ def by_section(tasks, project_gid)
72
77
  by_section = {}
73
78
  tasks.each do |task|
74
- current_section, by_section =
75
- legacy_file_task_by_section(current_section, by_section, task)
79
+ file_task_by_section(by_section, task, project_gid)
76
80
  end
77
81
  by_section
78
82
  end
83
+ cache_method :by_section, LONG_CACHE_TIME
79
84
 
80
- def legacy_tasks_by_section_for_project_and_assignee_status(project,
81
- assignee_status)
82
- raw_tasks = projects.tasks_from_project(project)
83
- by_assignee_status =
84
- projects.active_tasks(raw_tasks)
85
- .group_by(&:assignee_status)
86
- active_tasks = by_assignee_status[assignee_status]
87
- legacy_by_section(active_tasks)
85
+ def file_task_by_section(by_section, task, project_gid)
86
+ membership = task.memberships.find { |m| m['project']['gid'] == project_gid }
87
+ raise "Could not find task in project_gid #{project_gid}: #{task}" if membership.nil?
88
+
89
+ current_section = membership['section']['name']
90
+ current_section = nil if current_section == '(no section)'
91
+ by_section[current_section] ||= []
92
+ by_section[current_section] << task
88
93
  end
89
94
 
90
95
  def project_or_raise(workspace_name, project_name)
@@ -96,96 +101,20 @@ module Checkoff
96
101
  project
97
102
  end
98
103
 
99
- def verify_legacy_user_task_list!(workspace_name)
100
- return unless user_task_list_migrated_to_real_sections?(workspace_name)
101
-
102
- raise NotImplementedError, 'Section-based user task lists not yet supported'
103
- end
104
-
105
- ASSIGNEE_STATUS_BY_PROJECT_NAME = {
106
- my_tasks_new: 'inbox',
107
- my_tasks_today: 'today',
108
- my_tasks_upcoming: 'upcoming',
109
- }.freeze
110
-
111
- def user_task_list_by_section(workspace_name, project_name)
112
- verify_legacy_user_task_list!(workspace_name)
113
-
114
- project = projects.project(workspace_name, project_name)
115
- if project_name == :my_tasks
116
- legacy_tasks_by_section_for_project(project)
117
- else
118
- legacy_tasks_by_section_for_project_and_assignee_status(project,
119
- ASSIGNEE_STATUS_BY_PROJECT_NAME.fetch(project_name))
120
- end
121
- end
122
-
123
- # rubocop:disable Metrics/MethodLength
124
- def tasks_by_section(workspace_name, project_name)
125
- project = project_or_raise(workspace_name, project_name)
126
- case project_name
127
- when :my_tasks
128
- user_task_list_by_section(workspace_name, project_name)
129
- when :my_tasks_new
130
- user_task_list_by_section(workspace_name, project_name)
131
- when :my_tasks_today
132
- user_task_list_by_section(workspace_name, project_name)
133
- when :my_tasks_upcoming
134
- user_task_list_by_section(workspace_name, project_name)
135
- else
136
- tasks_by_section_for_project(project)
137
- end
138
- end
139
- cache_method :tasks_by_section, SHORT_CACHE_TIME
140
- # rubocop:enable Metrics/MethodLength
141
-
142
- # XXX: Rename to section_tasks
143
- def tasks(workspace_name, project_name, section_name)
144
- tasks_by_section(workspace_name, project_name)[section_name]
145
- end
146
- cache_method :tasks, SHORT_CACHE_TIME
147
-
148
- def section_task_names(workspace_name, project_name, section_name)
149
- tasks = tasks(workspace_name, project_name, section_name)
150
- if tasks.nil?
151
- by_section = tasks_by_section(workspace_name, project_name)
152
- desc = "#{workspace_name} | #{project_name} | #{section_name}"
153
- raise "Could not find task names for #{desc}. " \
154
- "Valid sections: #{by_section.keys}"
155
- end
156
- tasks.map(&:name)
157
- end
158
- cache_method :section_task_names, SHORT_CACHE_TIME
159
-
160
- def user_task_list_migrated_to_real_sections?(workspace_name)
161
- workspace = workspaces.workspace_by_name(workspace_name)
162
- result = client.user_task_lists.get_user_task_list_for_user(user_gid: 'me',
163
- workspace: workspace.gid)
164
- result.migration_status != 'not_migrated'
104
+ def section(workspace_name, project_name, section_name)
105
+ sections = sections_or_raise(workspace_name, project_name)
106
+ sections.find { |section| section.name.chomp(':') == section_name.chomp(':') }
165
107
  end
166
108
 
167
- def task_due?(task)
168
- if task.due_at
169
- Time.parse(task.due_at) <= time.now
170
- elsif task.due_on
171
- Date.parse(task.due_on) <= time.today
172
- else
173
- true # set a due date if you don't want to do this now
174
- end
175
- end
109
+ def section_or_raise(workspace_name, project_name, section_name)
110
+ section = section(workspace_name, project_name, section_name)
111
+ if section.nil?
112
+ valid_sections = sections_or_raise(workspace_name, project_name).map(&:name)
176
113
 
177
- def project_task_names(workspace_name, project_name)
178
- by_section = tasks_by_section(workspace_name, project_name)
179
- by_section.flat_map do |section_name, tasks|
180
- task_names = tasks.map(&:name)
181
- if section_name.nil?
182
- task_names
183
- else
184
- [section_name, task_names]
185
- end
114
+ raise "Could not find section #{section_name} under project #{project_name} " \
115
+ "under workspace #{workspace_name}. Valid sections: #{valid_sections}"
186
116
  end
117
+ section
187
118
  end
188
- cache_method :project_task_names, SHORT_CACHE_TIME
189
119
  end
190
- # rubocop:enable Metrics/ClassLength
191
120
  end
@@ -11,17 +11,7 @@ module Checkoff
11
11
 
12
12
  extend Forwardable
13
13
 
14
- def file_task_by_section(current_section, by_section, task)
15
- if task.name =~ /:$/
16
- current_section = task.name
17
- by_section[current_section] = []
18
- else
19
- by_section[current_section] ||= []
20
- by_section[current_section] << task
21
- end
22
- [current_section, by_section]
23
- end
24
-
14
+ # pulls a Hash of subtasks broken out by section
25
15
  def by_section(tasks)
26
16
  current_section = nil
27
17
  by_section = {}
@@ -37,5 +27,18 @@ module Checkoff
37
27
  task.subtasks(projects.task_options)
38
28
  end
39
29
  cache_method :raw_subtasks, LONG_CACHE_TIME
30
+
31
+ private
32
+
33
+ def file_task_by_section(current_section, by_section, task)
34
+ if task.name =~ /:$/
35
+ current_section = task.name
36
+ by_section[current_section] = []
37
+ else
38
+ by_section[current_section] ||= []
39
+ by_section[current_section] << task
40
+ end
41
+ [current_section, by_section]
42
+ end
40
43
  end
41
44
  end
@@ -5,7 +5,7 @@
5
5
  require_relative 'sections'
6
6
 
7
7
  module Checkoff
8
- # Pull things from 'my tasks' in Asana
8
+ # Pull tasks from Asana
9
9
  class Tasks
10
10
  MINUTE = 60
11
11
  HOUR = MINUTE * 60
@@ -22,25 +22,20 @@ module Checkoff
22
22
  @asana_task = asana_task
23
23
  end
24
24
 
25
- def client
26
- @sections.client
27
- end
28
-
29
- def projects
30
- @projects ||= @sections.projects
31
- end
32
-
33
- def task(workspace_name, project_name, task_name, only_uncompleted: true)
25
+ # Pull a specific task by name
26
+ def task(workspace_name, project_name, task_name,
27
+ section_name: :unspecified,
28
+ only_uncompleted: true)
34
29
  project = projects.project(workspace_name, project_name)
35
- tasks = projects.tasks_from_project(project,
36
- only_uncompleted: only_uncompleted)
30
+ tasks = if section_name == :unspecified
31
+ projects.tasks_from_project(project,
32
+ only_uncompleted: only_uncompleted)
33
+ else
34
+ @sections.tasks(workspace_name, project_name, section_name)
35
+ end
37
36
  tasks.find { |task| task.name == task_name }
38
37
  end
39
38
 
40
- def tasks_minus_sections(tasks)
41
- @sections.by_section(tasks).values.flatten
42
- end
43
-
44
39
  def add_task(name,
45
40
  workspace_gid: default_workspace_gid,
46
41
  assignee_gid: default_assignee_gid)
@@ -49,6 +44,16 @@ module Checkoff
49
44
  workspace: workspace_gid, name: name)
50
45
  end
51
46
 
47
+ private
48
+
49
+ def client
50
+ @sections.client
51
+ end
52
+
53
+ def projects
54
+ @projects ||= @sections.projects
55
+ end
56
+
52
57
  def default_assignee_gid
53
58
  @config.fetch(:default_assignee_gid)
54
59
  end