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