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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +36 -33
- data/.envrc +1 -1
- data/.git-hooks/{pre-commit → pre_commit}/circle_ci.rb +0 -0
- data/.gitignore +10 -0
- data/.overcommit.yml +1 -0
- data/.rubocop.yml +1 -1
- data/CODE_OF_CONDUCT.md +127 -7
- data/DEVELOPMENT.md +28 -0
- data/Gemfile.lock +56 -45
- data/Makefile +27 -8
- data/README.md +1 -1
- data/Rakefile +2 -9
- data/bin/overcommit +29 -0
- data/checkoff.gemspec +9 -4
- data/coverage/.last_run.json +2 -1
- data/exe/checkoff +1 -2
- data/fix.sh +71 -11
- data/lib/checkoff/cli.rb +98 -85
- data/lib/checkoff/config_loader.rb +22 -16
- data/lib/checkoff/projects.rb +35 -30
- data/lib/checkoff/sections.rb +58 -129
- data/lib/checkoff/subtasks.rb +14 -11
- data/lib/checkoff/tasks.rb +21 -16
- data/lib/checkoff/version.rb +2 -1
- data/lib/checkoff/workspaces.rb +8 -4
- data/{lib/tasks/ci.rake → rakelib/citest.rake} +1 -1
- data/rakelib/clear_metrics.rake +17 -0
- data/{lib/tasks → rakelib}/default.rake +0 -0
- data/rakelib/gem_tasks.rake +3 -0
- data/rakelib/localtest.rake +4 -0
- data/rakelib/overcommit.rake +6 -0
- data/rakelib/quality.rake +4 -0
- data/{lib/tasks → rakelib}/test.rake +0 -0
- data/rakelib/undercover.rake +8 -0
- data/requirements_dev.txt +1 -0
- metadata +54 -21
- data/.ruby-version +0 -1
- data/lib/tasks/feature.rake +0 -9
- data/lib/tasks/spec.rake +0 -9
@@ -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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
44
|
-
filename = yaml_filename(sym)
|
45
|
-
return {} unless File.exist?(filename)
|
46
|
+
private
|
46
47
|
|
47
|
-
|
48
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
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
|
data/lib/checkoff/projects.rb
CHANGED
@@ -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
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
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
|
data/lib/checkoff/sections.rb
CHANGED
@@ -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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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 :
|
54
|
+
cache_method :tasks, SHORT_CACHE_TIME
|
46
55
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
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
|
-
|
71
|
-
|
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
|
-
|
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
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
100
|
-
|
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
|
168
|
-
|
169
|
-
|
170
|
-
|
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
|
-
|
178
|
-
|
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
|
data/lib/checkoff/subtasks.rb
CHANGED
@@ -11,17 +11,7 @@ module Checkoff
|
|
11
11
|
|
12
12
|
extend Forwardable
|
13
13
|
|
14
|
-
|
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
|
data/lib/checkoff/tasks.rb
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
require_relative 'sections'
|
6
6
|
|
7
7
|
module Checkoff
|
8
|
-
# Pull
|
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
|
-
|
26
|
-
|
27
|
-
|
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 =
|
36
|
-
|
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
|