plan_my_stuff 0.3.0 → 0.5.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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +569 -38
  4. data/app/controllers/plan_my_stuff/comments_controller.rb +5 -1
  5. data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +102 -0
  6. data/app/controllers/plan_my_stuff/issues/closures_controller.rb +37 -0
  7. data/app/controllers/plan_my_stuff/issues/links_controller.rb +127 -0
  8. data/app/controllers/plan_my_stuff/issues/takes_controller.rb +88 -0
  9. data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +48 -0
  10. data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +47 -0
  11. data/app/controllers/plan_my_stuff/issues_controller.rb +22 -55
  12. data/app/controllers/plan_my_stuff/labels_controller.rb +4 -4
  13. data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +75 -0
  14. data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +40 -0
  15. data/app/controllers/plan_my_stuff/project_items_controller.rb +0 -75
  16. data/app/controllers/plan_my_stuff/projects_controller.rb +11 -1
  17. data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +54 -0
  18. data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +39 -0
  19. data/app/controllers/plan_my_stuff/testing_projects_controller.rb +93 -0
  20. data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +148 -0
  21. data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +284 -0
  22. data/app/jobs/plan_my_stuff/application_job.rb +9 -0
  23. data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +81 -0
  24. data/app/views/plan_my_stuff/comments/partials/_form.html.erb +7 -0
  25. data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +87 -0
  26. data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +2 -2
  27. data/app/views/plan_my_stuff/issues/partials/_links.html.erb +70 -0
  28. data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +2 -2
  29. data/app/views/plan_my_stuff/issues/show.html.erb +46 -3
  30. data/app/views/plan_my_stuff/projects/index.html.erb +15 -1
  31. data/app/views/plan_my_stuff/projects/show.html.erb +10 -5
  32. data/app/views/plan_my_stuff/testing_project_items/new.html.erb +12 -0
  33. data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +22 -0
  34. data/app/views/plan_my_stuff/testing_projects/edit.html.erb +7 -0
  35. data/app/views/plan_my_stuff/testing_projects/new.html.erb +7 -0
  36. data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +39 -0
  37. data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +51 -0
  38. data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +35 -0
  39. data/app/views/plan_my_stuff/testing_projects/show.html.erb +65 -0
  40. data/config/routes.rb +38 -15
  41. data/lib/generators/plan_my_stuff/install/templates/initializer.rb +172 -5
  42. data/lib/plan_my_stuff/application_record.rb +121 -0
  43. data/lib/plan_my_stuff/approval.rb +80 -0
  44. data/lib/plan_my_stuff/archive/sweep.rb +85 -0
  45. data/lib/plan_my_stuff/archive.rb +14 -0
  46. data/lib/plan_my_stuff/aws_sns_simulator.rb +110 -0
  47. data/lib/plan_my_stuff/base_project.rb +661 -0
  48. data/lib/plan_my_stuff/base_project_item.rb +562 -0
  49. data/lib/plan_my_stuff/base_project_metadata.rb +16 -0
  50. data/lib/plan_my_stuff/cache.rb +197 -0
  51. data/lib/plan_my_stuff/client.rb +7 -0
  52. data/lib/plan_my_stuff/comment.rb +171 -50
  53. data/lib/plan_my_stuff/configuration.rb +210 -10
  54. data/lib/plan_my_stuff/custom_fields.rb +31 -17
  55. data/lib/plan_my_stuff/engine.rb +0 -4
  56. data/lib/plan_my_stuff/errors.rb +49 -0
  57. data/lib/plan_my_stuff/graphql/queries.rb +392 -0
  58. data/lib/plan_my_stuff/issue.rb +1476 -175
  59. data/lib/plan_my_stuff/issue_metadata.rb +122 -0
  60. data/lib/plan_my_stuff/label.rb +82 -11
  61. data/lib/plan_my_stuff/link.rb +144 -0
  62. data/lib/plan_my_stuff/notifications.rb +142 -0
  63. data/lib/plan_my_stuff/pipeline/issue_linker.rb +62 -0
  64. data/lib/plan_my_stuff/pipeline/status.rb +44 -0
  65. data/lib/plan_my_stuff/pipeline.rb +293 -0
  66. data/lib/plan_my_stuff/project.rb +30 -693
  67. data/lib/plan_my_stuff/project_item.rb +3 -417
  68. data/lib/plan_my_stuff/project_item_metadata.rb +55 -0
  69. data/lib/plan_my_stuff/project_metadata.rb +9 -3
  70. data/lib/plan_my_stuff/reminders/closer.rb +70 -0
  71. data/lib/plan_my_stuff/reminders/fire.rb +129 -0
  72. data/lib/plan_my_stuff/reminders/sweep.rb +54 -0
  73. data/lib/plan_my_stuff/reminders.rb +16 -0
  74. data/lib/plan_my_stuff/test_helpers.rb +260 -15
  75. data/lib/plan_my_stuff/testing_project.rb +291 -0
  76. data/lib/plan_my_stuff/testing_project_item.rb +216 -0
  77. data/lib/plan_my_stuff/testing_project_metadata.rb +94 -0
  78. data/lib/plan_my_stuff/user_resolver.rb +8 -3
  79. data/lib/plan_my_stuff/version.rb +1 -1
  80. data/lib/plan_my_stuff/webhook_replayer.rb +280 -0
  81. data/lib/plan_my_stuff.rb +15 -0
  82. data/lib/tasks/plan_my_stuff.rake +179 -0
  83. metadata +77 -3
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ module Reminders
5
+ # Walks a single repo's waiting issues, dispatching each to +Closer+
6
+ # when past the inactivity ceiling or +Fire+ when a reminder is due.
7
+ # Called from +RemindersSweepJob+; no ActiveJob dependency here so
8
+ # the logic stays unit-testable and callable from a plain rake task.
9
+ class Sweep
10
+ # @param repo [Symbol, String] repo key or full name
11
+ # @param now [Time] clock reference
12
+ #
13
+ def initialize(repo:, now: Time.now.utc)
14
+ @repo = repo
15
+ @now = now.utc
16
+ end
17
+
18
+ # Runs the sweep. No-op when +config.reminders_enabled+ is false.
19
+ #
20
+ # @return [void]
21
+ #
22
+ def call
23
+ return unless PlanMyStuff.configuration.reminders_enabled
24
+
25
+ candidates.each do |issue|
26
+ if Closer.should_close?(issue, now: @now)
27
+ Closer.new(issue, now: @now).call
28
+ elsif Fire.ready?(issue, now: @now)
29
+ Fire.new(issue, now: @now).call
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # Issues carrying either waiting label, deduplicated by number.
37
+ # GitHub's labels filter is AND across multiple labels, so we
38
+ # query each label separately and merge.
39
+ #
40
+ # @return [Array<PlanMyStuff::Issue>]
41
+ #
42
+ def candidates
43
+ user_label = PlanMyStuff.configuration.waiting_on_user_label
44
+ approval_label = PlanMyStuff.configuration.waiting_on_approval_label
45
+
46
+ by_label = [user_label, approval_label].flat_map do |label|
47
+ PlanMyStuff::Issue.list(repo: @repo, labels: [label], state: :open)
48
+ end
49
+
50
+ by_label.uniq(&:number)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlanMyStuff
4
+ # Follow-up reminder engine. The +Sweep+ class walks the waiting issues
5
+ # in a repo and dispatches to +Fire+ for reminders that are due or
6
+ # +Closer+ for issues that have exceeded the inactivity threshold.
7
+ #
8
+ # Entry point for the sweep lives in +RemindersSweepJob+; this module
9
+ # holds the POROs so they can be unit-tested without ActiveJob.
10
+ module Reminders
11
+ end
12
+ end
13
+
14
+ require_relative 'reminders/closer'
15
+ require_relative 'reminders/fire'
16
+ require_relative 'reminders/sweep'
@@ -58,8 +58,8 @@ module PlanMyStuff
58
58
  custom_fields: {},
59
59
  }.merge(metadata))
60
60
 
61
- issue.instance_variable_set(:@metadata, issue_metadata)
62
- issue.instance_variable_set(:@persisted, true)
61
+ issue.metadata = issue_metadata
62
+ issue.__send__(:persisted!)
63
63
 
64
64
  body_comment = build_comment(
65
65
  body: body,
@@ -98,8 +98,8 @@ module PlanMyStuff
98
98
  custom_fields: {},
99
99
  }.merge(metadata))
100
100
 
101
- comment.instance_variable_set(:@metadata, comment_metadata)
102
- comment.instance_variable_set(:@persisted, true)
101
+ comment.metadata = comment_metadata
102
+ comment.__send__(:persisted!)
103
103
  comment
104
104
  end
105
105
 
@@ -124,7 +124,7 @@ module PlanMyStuff
124
124
  statuses: status_options,
125
125
  fields: [{ id: 'field_status', name: 'Status', options: status_options }],
126
126
  )
127
- project.instance_variable_set(:@persisted, true)
127
+ project.__send__(:persisted!)
128
128
 
129
129
  project.items = items.map do |item_hash|
130
130
  PlanMyStuff::ProjectItem.build(
@@ -143,6 +143,54 @@ module PlanMyStuff
143
143
 
144
144
  project
145
145
  end
146
+
147
+ # Sets approvals on an in-memory +Issue+ without hitting the API.
148
+ # Accepts user objects or integer user_ids.
149
+ #
150
+ # @param issue [PlanMyStuff::Issue]
151
+ # @param approved [Array<Object, Integer>] approvers who have approved
152
+ # @param pending [Array<Object, Integer>] approvers who have not yet approved
153
+ #
154
+ # @return [Array<PlanMyStuff::Approval>] the records written to +issue.metadata.approvals+
155
+ #
156
+ def stub_approvals(issue, approved: [], pending: [])
157
+ records =
158
+ pending.map { |u| PlanMyStuff::Approval.new(user_id: extract_user_id(u), status: 'pending') } +
159
+ approved.map do |u|
160
+ PlanMyStuff::Approval.new(user_id: extract_user_id(u), status: 'approved', approved_at: Time.current)
161
+ end
162
+ issue.metadata.approvals = records
163
+ records
164
+ end
165
+
166
+ # @return [Integer]
167
+ def extract_user_id(user)
168
+ user.is_a?(Integer) ? user : PlanMyStuff::UserResolver.user_id(user)
169
+ end
170
+ end
171
+
172
+ # Captures +plan_my_stuff.*+ +ActiveSupport::Notifications+ events fired
173
+ # inside the given block. Lets consuming apps assert that their code
174
+ # triggers gem events without threading subscriptions through every spec.
175
+ #
176
+ # Usage:
177
+ #
178
+ # events = PlanMyStuff::TestHelpers::Notifications.capture do
179
+ # PlanMyStuff::Issue.create!(...)
180
+ # end
181
+ # events.first[:name] # => "plan_my_stuff.issue.created"
182
+ # events.first[:payload] # => { issue:, user:, timestamp:, ... }
183
+ #
184
+ module Notifications
185
+ module_function
186
+
187
+ # @return [Array<Hash>] events as +{ name: String, payload: Hash }+
188
+ def capture(&)
189
+ events = []
190
+ callback = -> (name, _start, _finish, _id, payload) { events << { name: name, payload: payload } }
191
+ ActiveSupport::Notifications.subscribed(callback, /^plan_my_stuff\./, &)
192
+ events
193
+ end
146
194
  end
147
195
 
148
196
  # RSpec matchers included when the module is included in a test config.
@@ -218,6 +266,41 @@ module PlanMyStuff
218
266
  expect_pms_action(:item_assigned, 'item to be assigned', **filters)
219
267
  end
220
268
 
269
+ # @param filters [Hash] attribute filters (e.g. assignee:, project_number:)
270
+ def expect_pms_pipeline_submitted(**filters)
271
+ expect_pms_action(:pipeline_submitted, 'pipeline submission', **filters)
272
+ end
273
+
274
+ # @param filters [Hash] attribute filters (e.g. item_id:)
275
+ def expect_pms_pipeline_taken(**filters)
276
+ expect_pms_action(:pipeline_started, 'pipeline item to be taken', **filters)
277
+ end
278
+
279
+ # @param filters [Hash] attribute filters (e.g. item_id:)
280
+ def expect_pms_pipeline_in_review(**filters)
281
+ expect_pms_action(:pipeline_in_review, 'pipeline item marked in review', **filters)
282
+ end
283
+
284
+ # @param filters [Hash] attribute filters (e.g. item_id:)
285
+ def expect_pms_pipeline_testing(**filters)
286
+ expect_pms_action(:pipeline_testing, 'pipeline item moved to testing', **filters)
287
+ end
288
+
289
+ # @param filters [Hash] attribute filters (e.g. item_id:)
290
+ def expect_pms_pipeline_ready_for_release(**filters)
291
+ expect_pms_action(:pipeline_ready_for_release, 'pipeline item marked ready for release', **filters)
292
+ end
293
+
294
+ # @param filters [Hash] attribute filters (e.g. project_number:, commit_sha:)
295
+ def expect_pms_pipeline_deployment_started(**filters)
296
+ expect_pms_action(:pipeline_deployment_started, 'pipeline deployment started', **filters)
297
+ end
298
+
299
+ # @param filters [Hash] attribute filters (e.g. item_id:, deployment_id:)
300
+ def expect_pms_pipeline_deployment_completed(**filters)
301
+ expect_pms_action(:pipeline_deployment_completed, 'pipeline deployment completed', **filters)
302
+ end
303
+
221
304
  private
222
305
 
223
306
  # @return [void]
@@ -265,10 +348,11 @@ module PlanMyStuff
265
348
  stub_comment_class_methods!
266
349
  stub_project_class_methods!
267
350
  stub_project_item_class_methods!
351
+ stub_pipeline_methods!
268
352
  @_test_mode = true
269
353
  end
270
354
 
271
- # Restores original class methods overwritten by test_mode!
355
+ # Restores original class / instance methods overwritten by test_mode!
272
356
  #
273
357
  # @return [void]
274
358
  #
@@ -276,8 +360,13 @@ module PlanMyStuff
276
360
  return unless @_test_mode
277
361
 
278
362
  (@_test_mode_originals || {}).each do |klass, methods|
279
- methods.each do |name, original|
280
- klass.define_singleton_method(name, original)
363
+ methods.each do |key, original|
364
+ kind, name = key
365
+ if kind == :instance
366
+ klass.define_method(name, original)
367
+ else
368
+ klass.define_singleton_method(name, original)
369
+ end
281
370
  end
282
371
  end
283
372
 
@@ -288,7 +377,7 @@ module PlanMyStuff
288
377
 
289
378
  private
290
379
 
291
- # Saves the original method so it can be restored by exit_test_mode!
380
+ # Saves the original class method so it can be restored by exit_test_mode!
292
381
  #
293
382
  # @param klass [Class]
294
383
  # @param method_name [Symbol]
@@ -297,13 +386,26 @@ module PlanMyStuff
297
386
  #
298
387
  def save_original(klass, method_name)
299
388
  @_test_mode_originals[klass] ||= {}
300
- @_test_mode_originals[klass][method_name] = klass.method(method_name)
389
+ @_test_mode_originals[klass][[:class, method_name]] = klass.method(method_name)
390
+ end
391
+
392
+ # Saves the original instance method so it can be restored by exit_test_mode!
393
+ #
394
+ # @param klass [Class]
395
+ # @param method_name [Symbol]
396
+ #
397
+ # @return [void]
398
+ #
399
+ def save_instance_original(klass, method_name)
400
+ @_test_mode_originals[klass] ||= {}
401
+ @_test_mode_originals[klass][[:instance, method_name]] = klass.instance_method(method_name)
301
402
  end
302
403
 
303
404
  # @return [void]
304
405
  def stub_issue_class_methods!
305
406
  issue_mod = PlanMyStuff::Issue
306
- %i[create! find list update! add_viewers remove_viewers].each { |m| save_original(issue_mod, m) }
407
+ %i[create! find list update!].each { |m| save_original(issue_mod, m) }
408
+ %i[add_viewers remove_viewers].each { |m| save_instance_original(issue_mod, m) }
307
409
 
308
410
  issue_mod.define_singleton_method(:create!) do |**params|
309
411
  PlanMyStuff::TestHelpers.recorded_actions << {
@@ -359,19 +461,19 @@ module PlanMyStuff
359
461
  nil
360
462
  end
361
463
 
362
- issue_mod.define_singleton_method(:add_viewers) do |**params|
464
+ issue_mod.define_method(:add_viewers) do |user_ids:, user: nil|
363
465
  PlanMyStuff::TestHelpers.recorded_actions << {
364
466
  type: :viewers_added,
365
- params: params,
467
+ params: { number: number, repo: repo&.to_s, user_ids: Array.wrap(user_ids), user: user },
366
468
  }
367
469
 
368
470
  nil
369
471
  end
370
472
 
371
- issue_mod.define_singleton_method(:remove_viewers) do |**params|
473
+ issue_mod.define_method(:remove_viewers) do |user_ids:, user: nil|
372
474
  PlanMyStuff::TestHelpers.recorded_actions << {
373
475
  type: :viewers_removed,
374
- params: params,
476
+ params: { number: number, repo: repo&.to_s, user_ids: Array.wrap(user_ids), user: user },
375
477
  }
376
478
 
377
479
  nil
@@ -505,5 +607,148 @@ module PlanMyStuff
505
607
  nil
506
608
  end
507
609
  end
610
+
611
+ # @return [void]
612
+ def stub_pipeline_methods!
613
+ pipeline_mod = PlanMyStuff::Pipeline
614
+ %i[
615
+ submit! take! mark_in_review! request_testing! mark_ready_for_release! start_deployment! complete_deployment!
616
+ ].each { |m| save_original(pipeline_mod, m) }
617
+
618
+ pipeline_mod.define_singleton_method(:submit!) do |issue, assignee:, project_number: nil|
619
+ number =
620
+ project_number || PlanMyStuff.configuration.pipeline_project_number ||
621
+ PlanMyStuff.configuration.default_project_number
622
+
623
+ PlanMyStuff::TestHelpers.recorded_actions << {
624
+ type: :pipeline_submitted,
625
+ params: {
626
+ issue_number: issue.respond_to?(:number) ? issue.number : nil,
627
+ assignee: assignee,
628
+ project_number: number,
629
+ },
630
+ }
631
+
632
+ project = PlanMyStuff::TestHelpers.build_project(number: number || 1)
633
+ PlanMyStuff::ProjectItem.build(
634
+ {
635
+ id: "PVTI_fake_#{rand(10_000)}",
636
+ title: issue.respond_to?(:title) ? issue.title : 'Untitled',
637
+ number: issue.respond_to?(:number) ? issue.number : nil,
638
+ status: PlanMyStuff::Pipeline::Status::SUBMITTED,
639
+ field_values: {},
640
+ },
641
+ project: project,
642
+ )
643
+ end
644
+
645
+ pipeline_mod.define_singleton_method(:take!) do |project_item|
646
+ PlanMyStuff::TestHelpers.recorded_actions << {
647
+ type: :pipeline_started,
648
+ params: {
649
+ item_id: project_item.respond_to?(:id) ? project_item.id : nil,
650
+ issue_number: project_item.respond_to?(:number) ? project_item.number : nil,
651
+ },
652
+ }
653
+
654
+ nil
655
+ end
656
+
657
+ pipeline_mod.define_singleton_method(:mark_in_review!) do |project_item|
658
+ PlanMyStuff::TestHelpers.recorded_actions << {
659
+ type: :pipeline_in_review,
660
+ params: {
661
+ item_id: project_item.respond_to?(:id) ? project_item.id : nil,
662
+ issue_number: project_item.respond_to?(:number) ? project_item.number : nil,
663
+ },
664
+ }
665
+
666
+ nil
667
+ end
668
+
669
+ pipeline_mod.define_singleton_method(:request_testing!) do |project_item|
670
+ PlanMyStuff::TestHelpers.recorded_actions << {
671
+ type: :pipeline_testing,
672
+ params: {
673
+ item_id: project_item.respond_to?(:id) ? project_item.id : nil,
674
+ issue_number: project_item.respond_to?(:number) ? project_item.number : nil,
675
+ },
676
+ }
677
+
678
+ nil
679
+ end
680
+
681
+ pipeline_mod.define_singleton_method(:mark_ready_for_release!) do |project_item|
682
+ PlanMyStuff::TestHelpers.recorded_actions << {
683
+ type: :pipeline_ready_for_release,
684
+ params: {
685
+ item_id: project_item.respond_to?(:id) ? project_item.id : nil,
686
+ issue_number: project_item.respond_to?(:number) ? project_item.number : nil,
687
+ },
688
+ }
689
+
690
+ nil
691
+ end
692
+
693
+ pipeline_mod.define_singleton_method(:start_deployment!) do |project_number: nil, commit_sha: nil|
694
+ number =
695
+ project_number || PlanMyStuff.configuration.pipeline_project_number ||
696
+ PlanMyStuff.configuration.default_project_number
697
+
698
+ PlanMyStuff::TestHelpers.recorded_actions << {
699
+ type: :pipeline_deployment_started,
700
+ params: {
701
+ project_number: number,
702
+ commit_sha: commit_sha,
703
+ },
704
+ }
705
+
706
+ []
707
+ end
708
+
709
+ pipeline_mod.define_singleton_method(:complete_deployment!) do |project_item, deployment_id: nil|
710
+ PlanMyStuff::TestHelpers.recorded_actions << {
711
+ type: :pipeline_deployment_completed,
712
+ params: {
713
+ item_id: project_item.respond_to?(:id) ? project_item.id : nil,
714
+ issue_number: project_item.respond_to?(:number) ? project_item.number : nil,
715
+ deployment_id: deployment_id,
716
+ },
717
+ }
718
+
719
+ nil
720
+ end
721
+ end
722
+ end
723
+ end
724
+
725
+ if defined?(RSpec::Matchers)
726
+ # Asserts that a +plan_my_stuff.<name>+ event was fired inside the block.
727
+ # Chain +.with(key: value, ...)+ to match on a payload subset.
728
+ #
729
+ # expect { Issue.create!(...) }.to(have_fired_event('plan_my_stuff.issue.created'))
730
+ # expect { Issue.create!(...) }.to(have_fired_event('plan_my_stuff.issue.created').with(user: alice))
731
+ #
732
+ RSpec::Matchers.define(:have_fired_event) do |event_name|
733
+ supports_block_expectations
734
+
735
+ chain(:with) { |expected_payload| @expected_payload = expected_payload }
736
+
737
+ match do |block|
738
+ events = PlanMyStuff::TestHelpers::Notifications.capture(&block)
739
+ matching = events.select { |e| e[:name] == event_name }
740
+ @fired_events = events
741
+ next false if matching.empty?
742
+ next true if @expected_payload.nil?
743
+
744
+ matching.any? { |e| @expected_payload.all? { |k, v| e[:payload][k] == v } }
745
+ end
746
+
747
+ failure_message do
748
+ fired_names = @fired_events.pluck(:name)
749
+ base = "expected block to fire #{event_name.inspect}"
750
+ base += " with payload including #{@expected_payload.inspect}" if @expected_payload
751
+ "#{base}, but fired: #{fired_names.empty? ? '(none)' : fired_names.inspect}"
752
+ end
508
753
  end
509
754
  end