checkoff 0.9.0 → 0.12.0

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