checkoff 0.6.0 → 0.11.0

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