checkoff 0.6.0 → 0.11.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +120 -12
  3. data/.envrc +2 -0
  4. data/.git-hooks/pre_commit/circle_ci.rb +21 -0
  5. data/.gitignore +51 -1
  6. data/.overcommit.yml +53 -0
  7. data/.rubocop.yml +64 -168
  8. data/.yamllint.yml +8 -0
  9. data/CODE_OF_CONDUCT.md +120 -36
  10. data/DEVELOPMENT.md +28 -0
  11. data/Gemfile.lock +139 -0
  12. data/{LICENSE.txt → LICENSE} +7 -6
  13. data/Makefile +52 -18
  14. data/README.md +15 -2
  15. data/Rakefile +2 -9
  16. data/bin/bump +29 -0
  17. data/bin/checkoff +29 -0
  18. data/bin/overcommit +29 -0
  19. data/bin/rake +29 -0
  20. data/checkoff.gemspec +14 -5
  21. data/coverage/.last_run.json +2 -1
  22. data/docs/cookiecutter_input.json +13 -0
  23. data/exe/checkoff +1 -2
  24. data/fix.sh +334 -0
  25. data/lib/checkoff/cli.rb +97 -76
  26. data/lib/checkoff/config_loader.rb +50 -4
  27. data/lib/checkoff/projects.rb +35 -31
  28. data/lib/checkoff/sections.rb +98 -50
  29. data/lib/checkoff/subtasks.rb +14 -11
  30. data/lib/checkoff/tasks.rb +18 -9
  31. data/lib/checkoff/version.rb +2 -1
  32. data/lib/checkoff/workspaces.rb +11 -6
  33. data/metrics/bigfiles_high_water_mark +1 -1
  34. data/metrics/rubocop_high_water_mark +1 -1
  35. data/{lib/tasks/ci.rake → rakelib/citest.rake} +1 -1
  36. data/rakelib/clear_metrics.rake +17 -0
  37. data/{lib/tasks → rakelib}/default.rake +0 -0
  38. data/rakelib/gem_tasks.rake +3 -0
  39. data/{lib/tasks → rakelib}/localtest.rake +1 -1
  40. data/rakelib/overcommit.rake +6 -0
  41. data/rakelib/quality.rake +4 -0
  42. data/{lib/tasks → rakelib}/test.rake +0 -0
  43. data/rakelib/undercover.rake +8 -0
  44. data/requirements_dev.txt +2 -0
  45. metadata +121 -25
  46. data/.ruby-version +0 -1
  47. data/.travis.yml +0 -22
  48. data/lib/tasks/clear_metrics.rake +0 -8
  49. data/lib/tasks/feature.rake +0 -9
  50. data/lib/tasks/quality.rake +0 -9
  51. data/lib/tasks/spec.rake +0 -9
data/lib/checkoff/cli.rb CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  require 'ostruct'
6
6
  require 'dalli'
7
+ require 'gli'
7
8
  require 'cache_method'
8
9
  require_relative 'workspaces'
9
10
  require_relative 'projects'
@@ -11,40 +12,44 @@ require_relative 'tasks'
11
12
  require_relative 'sections'
12
13
 
13
14
  module Checkoff
14
- # Provide ability for CLI to pull Asana items
15
- class CLI
16
- attr_reader :sections, :stderr
17
-
18
- def initialize(config: Checkoff::ConfigLoader.load(:asana),
19
- workspaces: Checkoff::Workspaces.new(config: config),
15
+ # CLI subcommand that shows tasks in JSON form
16
+ class ViewSubcommand
17
+ def initialize(workspace_name, project_name, section_name,
18
+ task_name,
19
+ config: Checkoff::ConfigLoader.load(:asana),
20
20
  projects: Checkoff::Projects.new(config: config),
21
21
  sections: Checkoff::Sections.new(config: config,
22
22
  projects: projects),
23
- tasks: Checkoff::Tasks.new(config: config),
24
- stderr: $stderr,
25
- kernel: Kernel)
26
- @workspaces = workspaces
27
- @projects = projects
23
+ tasks: Checkoff::Tasks.new(config: config,
24
+ sections: sections),
25
+ stderr: $stderr)
26
+ @workspace_name = workspace_name
27
+ @stderr = stderr
28
+ validate_and_assign_project_name(project_name)
29
+ @section_name = section_name
30
+ @task_name = task_name
28
31
  @sections = sections
29
32
  @tasks = tasks
30
- @kernel = kernel
31
- @stderr = stderr
32
33
  end
33
34
 
34
- def task_to_hash(task)
35
- task_out = {
36
- name: task.name,
37
- }
38
- if task.due_on
39
- task_out[:due] = task.due_on
40
- elsif task.due_at
41
- task_out[:due] = task.due_at
35
+ def run
36
+ if section_name.nil?
37
+ run_on_project(workspace_name, project_name)
38
+ elsif task_name.nil?
39
+ run_on_section(workspace_name, project_name, section_name)
40
+ else
41
+ run_on_task(workspace_name, project_name, section_name, task_name)
42
42
  end
43
- task_out
44
43
  end
45
44
 
46
- def tasks_to_hash(tasks)
47
- tasks.map { |task| task_to_hash(task) }
45
+ private
46
+
47
+ def validate_and_assign_project_name(project_name)
48
+ @project_name = if project_name.start_with? ':'
49
+ project_name[1..].to_sym
50
+ else
51
+ project_name
52
+ end
48
53
  end
49
54
 
50
55
  def run_on_project(workspace, project)
@@ -62,72 +67,88 @@ module Checkoff
62
67
  tasks_to_hash(tasks).to_json
63
68
  end
64
69
 
65
- def quickadd(workspace_name, task_name)
66
- workspace = @workspaces.workspace_by_name(workspace_name)
67
- @tasks.add_task(task_name,
68
- workspace_gid: workspace.gid)
70
+ def run_on_task(workspace, project, section, task_name)
71
+ section = nil if section == ''
72
+ task = tasks.task(workspace, project, task_name, section_name: section)
73
+ task_to_hash(task).to_json
69
74
  end
70
75
 
71
- def validate_args!(args)
72
- return unless args.length < 2 || !%w[view quickadd].include?(args[0])
73
-
74
- output_help
75
- exit(1)
76
+ def task_to_hash(task)
77
+ task_out = {
78
+ name: task.name,
79
+ }
80
+ if task.due_on
81
+ task_out[:due] = task.due_on
82
+ elsif task.due_at
83
+ task_out[:due] = task.due_at
84
+ end
85
+ task_out
76
86
  end
77
87
 
78
- def parse_view_args(subargs, args)
79
- subargs.workspace = args[1]
80
- subargs.project = args[2]
81
- subargs.section = args[3]
88
+ def tasks_to_hash(tasks)
89
+ tasks.map { |task| task_to_hash(task) }
82
90
  end
83
91
 
84
- def parse_quickadd_args(subargs, args)
85
- subargs.workspace = args[1]
86
- subargs.task_name = args[2]
87
- end
92
+ attr_reader :workspace_name, :project_name, :section_name, :task_name, :sections, :tasks, :stderr
93
+ end
88
94
 
89
- def parse_args(args)
90
- mode = args[0]
91
- subargs = OpenStruct.new
92
- case mode
93
- when 'view'
94
- parse_view_args(subargs, args)
95
- when 'quickadd'
96
- parse_quickadd_args(subargs, args)
97
- else
98
- raise
99
- end
100
- [mode, subargs]
95
+ # CLI subcommand that creates a task
96
+ class QuickaddSubcommand
97
+ def initialize(workspace_name, task_name,
98
+ config: Checkoff::ConfigLoader.load(:asana),
99
+ workspaces: Checkoff::Workspaces.new(config: config),
100
+ tasks: Checkoff::Tasks.new(config: config))
101
+ @workspace_name = workspace_name
102
+ @task_name = task_name
103
+ @workspaces = workspaces
104
+ @tasks = tasks
101
105
  end
102
106
 
103
- def output_help
104
- stderr.puts 'View tasks:'
105
- stderr.puts " #{$PROGRAM_NAME} view workspace project [section]"
106
- stderr.puts " #{$PROGRAM_NAME} quickadd workspace task_name"
107
- stderr.puts
108
- stderr.puts "'project' can be set to a project name, or :my_tasks, " \
109
- ":my_tasks_upcoming, :my_tasks_new, or :my_tasks_today"
107
+ def run
108
+ workspace = @workspaces.workspace_by_name(workspace_name)
109
+ @tasks.add_task(task_name,
110
+ workspace_gid: workspace.gid)
110
111
  end
111
112
 
112
- def view(workspace_name, project_name, section_name)
113
- project_name = project_name[1..-1].to_sym if project_name.start_with? ':'
114
- if section_name.nil?
115
- run_on_project(workspace_name, project_name)
116
- else
117
- run_on_section(workspace_name, project_name, section_name)
113
+ private
114
+
115
+ attr_reader :workspace_name, :task_name
116
+ end
117
+
118
+ # Provide ability for CLI to pull Asana items
119
+ class CheckoffGLIApp
120
+ extend GLI::App
121
+
122
+ program_desc 'Command-line client for Asana (unofficial)'
123
+
124
+ subcommand_option_handling :normal
125
+ arguments :strict
126
+
127
+ desc 'Add a short task to Asana'
128
+ arg 'workspace'
129
+ arg 'task_name'
130
+ command :quickadd do |c|
131
+ c.action do |_global_options, _options, args|
132
+ workspace_name = args.fetch(0)
133
+ task_name = args.fetch(1)
134
+
135
+ QuickaddSubcommand.new(workspace_name, task_name).run
118
136
  end
119
137
  end
120
138
 
121
- def run(args)
122
- validate_args!(args)
123
- command, subargs = parse_args(args)
124
- case command
125
- when 'view'
126
- view(subargs.workspace, subargs.project, subargs.section)
127
- when 'quickadd'
128
- quickadd(subargs.workspace, subargs.task_name)
129
- else
130
- raise
139
+ desc 'Output representation of Asana tasks'
140
+ arg 'workspace'
141
+ arg 'project'
142
+ arg 'section', :optional
143
+ arg 'task_name', :optional
144
+ command :view do |c|
145
+ c.action do |_global_options, _options, args|
146
+ workspace_name = args.fetch(0)
147
+ project_name = args.fetch(1)
148
+ section_name = args[2]
149
+ task_name = args[3]
150
+
151
+ puts ViewSubcommand.new(workspace_name, project_name, section_name, task_name).run
131
152
  end
132
153
  end
133
154
  end
@@ -4,12 +4,58 @@ require 'yaml'
4
4
  require 'active_support/core_ext/hash'
5
5
 
6
6
  module Checkoff
7
+ # Use the provided config from a YAML file, and fall back to env
8
+ # variable if it's not populated for a key'
9
+ class EnvFallbackConfigLoader
10
+ def initialize(config, sym, yaml_filename)
11
+ @config = config
12
+ @envvar_prefix = sym.upcase
13
+ @yaml_filename = yaml_filename
14
+ end
15
+
16
+ def [](key)
17
+ config_value = @config[key]
18
+ return config_value unless config_value.nil?
19
+
20
+ ENV[envvar_name(key)]
21
+ end
22
+
23
+ def fetch(key)
24
+ out = self[key]
25
+ return out unless out.nil?
26
+
27
+ raise KeyError,
28
+ "Please configure either the #{key} key in #{@yaml_filename} or set #{envvar_name(key)}"
29
+ end
30
+
31
+ private
32
+
33
+ def envvar_name(key)
34
+ "#{@envvar_prefix}__#{key.upcase}"
35
+ end
36
+ end
37
+
7
38
  # Load configuration file
8
39
  class ConfigLoader
9
- def self.load(sym)
10
- file = "#{sym}.yml"
11
- YAML.load_file(File.expand_path("~/.#{file}"))
12
- .with_indifferent_access
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
45
+
46
+ private
47
+
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
54
+
55
+ def yaml_filename(sym)
56
+ file = "#{sym}.yml"
57
+ File.expand_path("~/.#{file}")
58
+ end
13
59
  end
14
60
  end
15
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,12 @@ 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
- my_tasks = @config[:my_tasks]
53
- gid = @config[:my_tasks][workspace_name] unless my_tasks.nil?
54
- raise "Please define [:my_tasks][#{workspace_name}] in config file" if my_tasks.nil? || gid.nil?
55
-
56
- projects.find_by_id(gid)
57
- end
58
-
38
+ # pulls an Asana API project class given a name
59
39
  def project(workspace_name, project_name)
60
40
  if project_name.is_a?(Symbol) && project_name.to_s.start_with?('my_tasks')
61
41
  my_tasks(workspace_name)
@@ -68,10 +48,43 @@ module Checkoff
68
48
  end
69
49
  cache_method :project, LONG_CACHE_TIME
70
50
 
51
+ # find uncompleted tasks in a list
71
52
  def active_tasks(tasks)
72
53
  tasks.select { |task| task.completed_at.nil? }
73
54
  end
74
55
 
56
+ # pull task objects from a named project
57
+ def tasks_from_project(project, only_uncompleted: true, extra_fields: [])
58
+ options = task_options
59
+ options[:completed_since] = '9999-12-01' if only_uncompleted
60
+ options[:project] = project.gid
61
+ options[:options][:fields] += extra_fields
62
+ client.tasks.find_all(**options).to_a
63
+ end
64
+ cache_method :tasks_from_project, SHORT_CACHE_TIME
65
+
66
+ private
67
+
68
+ def projects
69
+ client.projects
70
+ end
71
+ cache_method :projects, LONG_CACHE_TIME
72
+
73
+ def projects_by_workspace_name(workspace_name)
74
+ workspace = @workspaces.workspace_by_name(workspace_name)
75
+ raise "Could not find workspace named #{workspace_name}" unless workspace
76
+
77
+ projects.find_by_workspace(workspace: workspace.gid)
78
+ end
79
+
80
+ def my_tasks(workspace_name)
81
+ workspace = @workspaces.workspace_by_name(workspace_name)
82
+ result = client.user_task_lists.get_user_task_list_for_user(user_gid: 'me',
83
+ workspace: workspace.gid)
84
+ gid = result.gid
85
+ projects.find_by_id(gid)
86
+ end
87
+
75
88
  def task_options
76
89
  {
77
90
  per_page: 100,
@@ -81,14 +94,5 @@ module Checkoff
81
94
  },
82
95
  }
83
96
  end
84
-
85
- def tasks_from_project(project, only_uncompleted: true, extra_fields: [])
86
- options = task_options
87
- options[:completed_since] = '9999-12-01' if only_uncompleted
88
- options[:project] = project.gid
89
- options[:options][:fields] += extra_fields
90
- client.tasks.find_all(**options).to_a
91
- end
92
- cache_method :tasks_from_project, SHORT_CACHE_TIME
93
97
  end
94
98
  end
@@ -4,6 +4,7 @@ require 'forwardable'
4
4
 
5
5
  module Checkoff
6
6
  # Query different sections of Asana projects
7
+ # rubocop:disable Metrics/ClassLength
7
8
  class Sections
8
9
  MINUTE = 60
9
10
  LONG_CACHE_TIME = MINUTE * 15
@@ -11,15 +12,82 @@ module Checkoff
11
12
 
12
13
  extend Forwardable
13
14
 
14
- attr_reader :projects, :time
15
+ attr_reader :projects, :workspaces, :time
15
16
 
16
17
  def initialize(config: Checkoff::ConfigLoader.load(:asana),
17
18
  projects: Checkoff::Projects.new(config: config),
19
+ workspaces: Checkoff::Workspaces.new(config: config),
18
20
  time: Time)
19
21
  @projects = projects
22
+ @workspaces = workspaces
20
23
  @time = time
21
24
  end
22
25
 
26
+ # Given a list of tasks, pull a Hash of tasks with section name -> task list
27
+ def by_section(tasks, project_gid)
28
+ by_section = {}
29
+ tasks.each do |task|
30
+ file_task_by_section(by_section, task, project_gid)
31
+ end
32
+ by_section
33
+ end
34
+ cache_method :by_section, LONG_CACHE_TIME
35
+
36
+ # Given a project object, pull all tasks, then provide a Hash of
37
+ # tasks with section name -> task list of the uncompleted tasks
38
+ def tasks_by_section_for_project(project)
39
+ raw_tasks = projects.tasks_from_project(project)
40
+ active_tasks = projects.active_tasks(raw_tasks)
41
+ by_section(active_tasks, project.gid)
42
+ end
43
+
44
+ # Given a workspace name and project name, then provide a Hash of
45
+ # tasks with section name -> task list of the uncompleted tasks
46
+ def tasks_by_section(workspace_name, project_name)
47
+ project = project_or_raise(workspace_name, project_name)
48
+ case project_name
49
+ when :my_tasks, :my_tasks_new, :my_tasks_today, :my_tasks_upcoming
50
+ user_task_list_by_section(workspace_name, project_name)
51
+ else
52
+ tasks_by_section_for_project(project)
53
+ end
54
+ end
55
+ cache_method :tasks_by_section, SHORT_CACHE_TIME
56
+
57
+ # XXX: Rename to section_tasks
58
+ #
59
+ # Pulls task objects from a specified section
60
+ def tasks(workspace_name, project_name, section_name)
61
+ tasks_by_section(workspace_name, project_name).fetch(section_name, [])
62
+ end
63
+ cache_method :tasks, SHORT_CACHE_TIME
64
+
65
+ # Pulls just names of tasks from a given section.
66
+ def section_task_names(workspace_name, project_name, section_name)
67
+ tasks = tasks(workspace_name, project_name, section_name)
68
+ if tasks.nil?
69
+ by_section = tasks_by_section(workspace_name, project_name)
70
+ desc = "#{workspace_name} | #{project_name} | #{section_name}"
71
+ raise "Could not find task names for #{desc}. " \
72
+ "Valid sections: #{by_section.keys}"
73
+ end
74
+ tasks.map(&:name)
75
+ end
76
+ cache_method :section_task_names, SHORT_CACHE_TIME
77
+
78
+ # returns if a task's due field is at or before the current date/time
79
+ def task_due?(task)
80
+ if task.due_at
81
+ Time.parse(task.due_at) <= time.now
82
+ elsif task.due_on
83
+ Date.parse(task.due_on) <= time.today
84
+ else
85
+ true # set a due date if you don't want to do this now
86
+ end
87
+ end
88
+
89
+ private
90
+
23
91
  def_delegators :@projects, :client
24
92
 
25
93
  def file_task_by_section(by_section, task, project_gid)
@@ -32,19 +100,10 @@ module Checkoff
32
100
  by_section[current_section] << task
33
101
  end
34
102
 
35
- def by_section(tasks, project_gid)
36
- by_section = {}
37
- tasks.each do |task|
38
- file_task_by_section(by_section, task, project_gid)
39
- end
40
- by_section
41
- end
42
- cache_method :by_section, LONG_CACHE_TIME
43
-
44
- def tasks_by_section_for_project(project)
103
+ def legacy_tasks_by_section_for_project(project)
45
104
  raw_tasks = projects.tasks_from_project(project)
46
105
  active_tasks = projects.active_tasks(raw_tasks)
47
- by_section(active_tasks, project.gid)
106
+ legacy_by_section(active_tasks)
48
107
  end
49
108
 
50
109
  def legacy_file_task_by_section(current_section, by_section, task)
@@ -68,12 +127,12 @@ module Checkoff
68
127
  by_section
69
128
  end
70
129
 
71
- def tasks_by_section_for_project_and_assignee_status(project,
72
- assignee_status)
130
+ def legacy_tasks_by_section_for_project_and_assignee_status(project,
131
+ assignee_status)
73
132
  raw_tasks = projects.tasks_from_project(project)
74
133
  by_assignee_status =
75
134
  projects.active_tasks(raw_tasks)
76
- .group_by(&:assignee_status)
135
+ .group_by(&:assignee_status)
77
136
  active_tasks = by_assignee_status[assignee_status]
78
137
  legacy_by_section(active_tasks)
79
138
  end
@@ -87,49 +146,37 @@ module Checkoff
87
146
  project
88
147
  end
89
148
 
90
- def tasks_by_section(workspace_name, project_name)
91
- project = project_or_raise(workspace_name, project_name)
92
- case project_name
93
- when :my_tasks_new
94
- tasks_by_section_for_project_and_assignee_status(project, 'inbox')
95
- when :my_tasks_today
96
- tasks_by_section_for_project_and_assignee_status(project, 'today')
97
- when :my_tasks_upcoming
98
- tasks_by_section_for_project_and_assignee_status(project, 'upcoming')
99
- else
100
- tasks_by_section_for_project(project)
101
- end
102
- end
103
- cache_method :tasks_by_section, SHORT_CACHE_TIME
149
+ def verify_legacy_user_task_list!(workspace_name)
150
+ return unless user_task_list_migrated_to_real_sections?(workspace_name)
104
151
 
105
- # XXX: Rename to section_tasks
106
- def tasks(workspace_name, project_name, section_name)
107
- tasks_by_section(workspace_name, project_name)[section_name]
152
+ raise NotImplementedError, 'Section-based user task lists not yet supported'
108
153
  end
109
- cache_method :tasks, SHORT_CACHE_TIME
110
154
 
111
- def section_task_names(workspace_name, project_name, section_name)
112
- tasks = tasks(workspace_name, project_name, section_name)
113
- if tasks.nil?
114
- by_section = tasks_by_section(workspace_name, project_name)
115
- desc = "#{workspace_name} | #{project_name} | #{section_name}"
116
- raise "Could not find task names for #{desc}. " \
117
- "Valid sections: #{by_section.keys}"
118
- end
119
- tasks.map(&:name)
120
- end
121
- cache_method :section_task_names, SHORT_CACHE_TIME
155
+ ASSIGNEE_STATUS_BY_PROJECT_NAME = {
156
+ my_tasks_new: 'inbox',
157
+ my_tasks_today: 'today',
158
+ my_tasks_upcoming: 'upcoming',
159
+ }.freeze
122
160
 
123
- def task_due?(task)
124
- if task.due_at
125
- Time.parse(task.due_at) <= time.now
126
- elsif task.due_on
127
- Date.parse(task.due_on) <= time.today
161
+ def user_task_list_by_section(workspace_name, project_name)
162
+ verify_legacy_user_task_list!(workspace_name)
163
+
164
+ project = projects.project(workspace_name, project_name)
165
+ if project_name == :my_tasks
166
+ legacy_tasks_by_section_for_project(project)
128
167
  else
129
- true # set a due date if you don't want to do this now
168
+ legacy_tasks_by_section_for_project_and_assignee_status(project,
169
+ ASSIGNEE_STATUS_BY_PROJECT_NAME.fetch(project_name))
130
170
  end
131
171
  end
132
172
 
173
+ def user_task_list_migrated_to_real_sections?(workspace_name)
174
+ workspace = workspaces.workspace_by_name(workspace_name)
175
+ result = client.user_task_lists.get_user_task_list_for_user(user_gid: 'me',
176
+ workspace: workspace.gid)
177
+ result.migration_status != 'not_migrated'
178
+ end
179
+
133
180
  def project_task_names(workspace_name, project_name)
134
181
  by_section = tasks_by_section(workspace_name, project_name)
135
182
  by_section.flat_map do |section_name, tasks|
@@ -143,4 +190,5 @@ module Checkoff
143
190
  end
144
191
  cache_method :project_task_names, SHORT_CACHE_TIME
145
192
  end
193
+ # rubocop:enable Metrics/ClassLength
146
194
  end