plan_my_stuff 0.7.0 → 0.9.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -1
  3. data/CONFIGURATION.md +351 -0
  4. data/README.md +100 -103
  5. data/app/controllers/plan_my_stuff/application_controller.rb +22 -3
  6. data/app/controllers/plan_my_stuff/comments_controller.rb +14 -16
  7. data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +23 -13
  8. data/app/controllers/plan_my_stuff/issues/closures_controller.rb +7 -5
  9. data/app/controllers/plan_my_stuff/issues/links_controller.rb +14 -18
  10. data/app/controllers/plan_my_stuff/issues/takes_controller.rb +99 -28
  11. data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +13 -5
  12. data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +7 -5
  13. data/app/controllers/plan_my_stuff/issues_controller.rb +24 -28
  14. data/app/controllers/plan_my_stuff/labels_controller.rb +21 -5
  15. data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +13 -6
  16. data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +5 -4
  17. data/app/controllers/plan_my_stuff/project_items_controller.rb +30 -5
  18. data/app/controllers/plan_my_stuff/projects_controller.rb +16 -16
  19. data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +21 -11
  20. data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +9 -4
  21. data/app/controllers/plan_my_stuff/testing_projects_controller.rb +30 -14
  22. data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +50 -17
  23. data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +32 -49
  24. data/app/jobs/plan_my_stuff/application_job.rb +2 -3
  25. data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +15 -22
  26. data/app/views/plan_my_stuff/comments/edit.html.erb +1 -3
  27. data/app/views/plan_my_stuff/comments/partials/_form.html.erb +1 -0
  28. data/app/views/plan_my_stuff/issues/edit.html.erb +2 -4
  29. data/app/views/plan_my_stuff/issues/index.html.erb +2 -2
  30. data/app/views/plan_my_stuff/issues/new.html.erb +2 -4
  31. data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +23 -2
  32. data/app/views/plan_my_stuff/issues/partials/_form.html.erb +1 -0
  33. data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +2 -1
  34. data/app/views/plan_my_stuff/issues/partials/_links.html.erb +50 -7
  35. data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +2 -1
  36. data/app/views/plan_my_stuff/issues/show.html.erb +5 -2
  37. data/app/views/plan_my_stuff/partials/_flash.html.erb +3 -0
  38. data/app/views/plan_my_stuff/projects/edit.html.erb +1 -3
  39. data/app/views/plan_my_stuff/projects/index.html.erb +1 -1
  40. data/app/views/plan_my_stuff/projects/new.html.erb +1 -3
  41. data/app/views/plan_my_stuff/projects/partials/_form.html.erb +1 -0
  42. data/app/views/plan_my_stuff/projects/show.html.erb +13 -3
  43. data/app/views/plan_my_stuff/testing_project_items/new.html.erb +1 -3
  44. data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +1 -3
  45. data/app/views/plan_my_stuff/testing_projects/edit.html.erb +1 -3
  46. data/app/views/plan_my_stuff/testing_projects/new.html.erb +1 -3
  47. data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +4 -3
  48. data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +1 -0
  49. data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +1 -0
  50. data/app/views/plan_my_stuff/testing_projects/show.html.erb +2 -2
  51. data/config/routes.rb +2 -2
  52. data/lib/generators/plan_my_stuff/install/templates/initializer.rb +52 -14
  53. data/lib/plan_my_stuff/approval.rb +12 -4
  54. data/lib/plan_my_stuff/aws_sns_simulator.rb +12 -6
  55. data/lib/plan_my_stuff/base_metadata.rb +4 -15
  56. data/lib/plan_my_stuff/base_project.rb +68 -55
  57. data/lib/plan_my_stuff/base_project_item.rb +62 -57
  58. data/lib/plan_my_stuff/base_project_metadata.rb +1 -1
  59. data/lib/plan_my_stuff/client.rb +136 -48
  60. data/lib/plan_my_stuff/comment.rb +59 -57
  61. data/lib/plan_my_stuff/comment_metadata.rb +1 -1
  62. data/lib/plan_my_stuff/configuration.rb +93 -93
  63. data/lib/plan_my_stuff/errors.rb +10 -10
  64. data/lib/plan_my_stuff/graphql/queries.rb +1 -1
  65. data/lib/plan_my_stuff/issue.rb +471 -333
  66. data/lib/plan_my_stuff/issue_metadata.rb +10 -10
  67. data/lib/plan_my_stuff/label.rb +34 -18
  68. data/lib/plan_my_stuff/link.rb +15 -15
  69. data/lib/plan_my_stuff/markdown.rb +12 -6
  70. data/lib/plan_my_stuff/metadata_parser.rb +3 -1
  71. data/lib/plan_my_stuff/notifications.rb +1 -1
  72. data/lib/plan_my_stuff/pipeline/completed_sweep.rb +2 -2
  73. data/lib/plan_my_stuff/pipeline/issue_linker.rb +1 -1
  74. data/lib/plan_my_stuff/pipeline.rb +61 -83
  75. data/lib/plan_my_stuff/project.rb +4 -4
  76. data/lib/plan_my_stuff/project_item_metadata.rb +1 -1
  77. data/lib/plan_my_stuff/project_metadata.rb +1 -1
  78. data/lib/plan_my_stuff/reminders/closer.rb +1 -1
  79. data/lib/plan_my_stuff/reminders/fire.rb +3 -3
  80. data/lib/plan_my_stuff/reminders/sweep.rb +4 -4
  81. data/lib/plan_my_stuff/repo.rb +12 -6
  82. data/lib/plan_my_stuff/test_helpers.rb +11 -11
  83. data/lib/plan_my_stuff/testing_project.rb +12 -11
  84. data/lib/plan_my_stuff/testing_project_item.rb +11 -9
  85. data/lib/plan_my_stuff/testing_project_metadata.rb +2 -2
  86. data/lib/plan_my_stuff/version.rb +1 -1
  87. data/lib/plan_my_stuff/webhook_replayer.rb +14 -2
  88. data/lib/plan_my_stuff.rb +26 -2
  89. data/lib/tasks/plan_my_stuff.rake +33 -20
  90. metadata +4 -2
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanMyStuff
4
- # Wraps a GitHub comment with parsed PMS metadata.
5
- # Class methods provide the public API for CRUD operations.
4
+ # Wraps a GitHub comment with parsed PMS metadata. Class methods provide the public API for CRUD operations.
6
5
  #
7
6
  # Follows an ActiveRecord-style pattern:
8
7
  # - `Comment.new(**attrs)` creates an unpersisted instance
@@ -21,25 +20,28 @@ module PlanMyStuff
21
20
  attribute :issue
22
21
  # @return [Time, nil] GitHub's updated_at timestamp
23
22
  attribute :updated_at
23
+ # @return [Time, nil] GitHub's created_at timestamp; settable on unpersisted comments for use with +Issue.import+
24
+ attribute :created_at
24
25
  # @return [Symbol, nil] :public or :internal (locally set or from metadata)
25
26
  attribute :visibility
26
27
 
27
- # @return [Boolean, nil] transient flag used by the new-comment form to
28
- # request waiting-on-user state when a support user posts. Not persisted.
28
+ # @return [Boolean, nil] transient flag used by the new-comment form to request waiting-on-user state when a
29
+ # support user posts. Not persisted.
29
30
  attr_accessor :waiting_on_reply
30
31
 
31
32
  class << self
32
33
  # Creates a comment on a GitHub issue with PMS metadata and a visible header.
33
34
  #
35
+ # @raise [PlanMyStuff::LockedIssueError] if the parent issue is locked
36
+ #
34
37
  # @param issue [PlanMyStuff::Issue] parent issue
35
38
  # @param body [String]
36
39
  # @param user [Object, Integer] user object or user_id
37
40
  # @param visibility [Symbol] :public or :internal
38
41
  # @param custom_fields [Hash]
39
42
  # @param issue_body [Boolean] whether this comment holds the issue body
40
- # @param waiting_on_reply [Boolean] when true and the author is a
41
- # support user, marks the issue as waiting on an end-user reply.
42
- # Ignored for non-support authors.
43
+ # @param waiting_on_reply [Boolean] when true and the author is a support user, marks the issue as waiting on
44
+ # an end-user reply. Ignored for non-support authors.
43
45
  #
44
46
  # @return [PlanMyStuff::Comment]
45
47
  #
@@ -55,9 +57,9 @@ module PlanMyStuff
55
57
  )
56
58
  raise(PlanMyStuff::LockedIssueError, "Issue ##{issue.number} is locked") if issue.locked?
57
59
 
58
- resolved_user = UserResolver.resolve(user)
60
+ resolved_user = PlanMyStuff::UserResolver.resolve(user)
59
61
  visibility = resolve_visibility(visibility, resolved_user)
60
- comment_metadata = CommentMetadata.build(
62
+ comment_metadata = PlanMyStuff::CommentMetadata.build(
61
63
  user: resolved_user,
62
64
  visibility: visibility.to_s,
63
65
  custom_fields: custom_fields,
@@ -67,7 +69,7 @@ module PlanMyStuff
67
69
 
68
70
  header = build_header(resolved_user)
69
71
  full_body = "#{header}\n\n#{body}"
70
- serialized_body = MetadataParser.serialize(comment_metadata.to_h, full_body)
72
+ serialized_body = PlanMyStuff::MetadataParser.serialize!(comment_metadata.to_h, full_body)
71
73
 
72
74
  client = PlanMyStuff.client
73
75
  result = client.rest(:add_comment, issue.repo, issue.number, serialized_body)
@@ -79,11 +81,11 @@ module PlanMyStuff
79
81
  cache_writer: :write_comment,
80
82
  )
81
83
 
82
- mark_issue_responded_if_first_support_comment(issue, resolved_user) unless skip_responded
84
+ mark_issue_responded_if_first_support_comment!(issue, resolved_user) unless skip_responded
83
85
 
84
86
  comment = build(result, issue: issue)
85
87
  PlanMyStuff::Notifications.instrument('comment.created', comment, user: resolved_user)
86
- apply_waiting_state_transitions(issue, resolved_user, waiting_on_reply, comment)
88
+ apply_waiting_state_transitions!(issue, resolved_user, waiting_on_reply, comment)
87
89
  comment
88
90
  end
89
91
 
@@ -180,7 +182,7 @@ module PlanMyStuff
180
182
  def build_header(user)
181
183
  display_name =
182
184
  if user.present?
183
- UserResolver.display_name(user)
185
+ PlanMyStuff::UserResolver.display_name(user)
184
186
  else
185
187
  'Unknown'
186
188
  end
@@ -201,39 +203,37 @@ module PlanMyStuff
201
203
 
202
204
  return :public if user.blank?
203
205
 
204
- return :public unless UserResolver.support?(user)
206
+ return :public unless PlanMyStuff::UserResolver.support?(user)
205
207
 
206
208
  :internal
207
209
  end
208
210
 
209
- # Sets responded_at on the issue metadata if this is the first support
210
- # comment and the issue hasn't been responded to yet.
211
+ # Sets responded_at on the issue metadata if this is the first support comment and the issue hasn't been
212
+ # responded to yet.
211
213
  #
212
214
  # @param issue [PlanMyStuff::Issue] parent issue
213
215
  # @param user [Object, nil] resolved user object
214
216
  #
215
217
  # @return [void]
216
218
  #
217
- def mark_issue_responded_if_first_support_comment(issue, user)
219
+ def mark_issue_responded_if_first_support_comment!(issue, user)
218
220
  return if user.nil?
219
221
 
220
- return unless UserResolver.support?(user)
222
+ return unless PlanMyStuff::UserResolver.support?(user)
221
223
  return unless issue.pms_issue?
222
224
 
223
225
  return if issue.metadata.responded?
224
226
 
225
- Issue.update!(
227
+ PlanMyStuff::Issue.update!(
226
228
  number: issue.number,
227
229
  repo: issue.repo,
228
- metadata: { responded_at: Time.now.utc.iso8601 },
230
+ metadata: { responded_at: PlanMyStuff.format_time(Time.now.utc) },
229
231
  )
230
232
  end
231
233
 
232
- # Mutates issue waiting state based on the comment's author.
233
- # Support users with +waiting_on_reply: true+ enter the issue
234
- # into waiting-on-user state. Non-support users clear any
235
- # active waiting-on-user state and auto-reopen issues that
236
- # were closed by the inactivity sweep.
234
+ # Mutates issue waiting state based on the comment's author. Support users with +waiting_on_reply: true+ enter
235
+ # the issue into waiting-on-user state. Non-support users clear any active waiting-on-user state and
236
+ # auto-reopen issues that were closed by the inactivity sweep.
237
237
  #
238
238
  # No-ops on non-PMS issues or when no user is resolved.
239
239
  #
@@ -244,7 +244,7 @@ module PlanMyStuff
244
244
  #
245
245
  # @return [void]
246
246
  #
247
- def apply_waiting_state_transitions(issue, user, waiting_on_reply, comment)
247
+ def apply_waiting_state_transitions!(issue, user, waiting_on_reply, comment)
248
248
  return if user.nil?
249
249
 
250
250
  return unless issue.pms_issue?
@@ -252,7 +252,7 @@ module PlanMyStuff
252
252
  # Auto-reopen fires only for non-support replies; a support comment on a
253
253
  # +closed_by_inactivity+ issue is treated as a closure note and requires
254
254
  # the explicit Reopen button to bring the issue back.
255
- if UserResolver.support?(user)
255
+ if PlanMyStuff::UserResolver.support?(user)
256
256
  issue.enter_waiting_on_user!(user: user) if waiting_on_reply
257
257
  elsif issue.metadata.closed_by_inactivity
258
258
  issue.reopen_by_reply!(comment: comment, user: user)
@@ -264,32 +264,34 @@ module PlanMyStuff
264
264
 
265
265
  # Persists the comment. Creates if new, updates if persisted.
266
266
  #
267
- # @raise [PlanMyStuff::StaleObjectError] on update if stale
268
- #
269
267
  # @return [self]
270
268
  #
271
- def save!
269
+ def save!(user: nil)
272
270
  if new_record?
273
271
  created = self.class.create!(
274
272
  issue: issue,
275
273
  body: body,
274
+ user: user || metadata.created_by,
276
275
  visibility: visibility || :public,
276
+ custom_fields: metadata.custom_fields.to_h,
277
+ issue_body: metadata.issue_body,
278
+ waiting_on_reply: waiting_on_reply,
277
279
  )
278
280
  hydrate_from_comment(created)
279
281
  else
280
- update!(body: body)
282
+ update_attrs = { user: user, body: body }
283
+ update_attrs[:visibility] = visibility if visibility_changed?
284
+ update!(**update_attrs)
281
285
  end
282
286
 
283
287
  self
284
288
  end
285
289
 
286
- # Updates this comment on GitHub. Raises StaleObjectError if the remote
287
- # has been modified since this instance was loaded.
290
+ # Updates this comment on GitHub. Raises StaleObjectError if the remote has been modified since this instance was
291
+ # loaded.
288
292
  #
289
293
  # @param attrs [Hash] attributes to update (body:, visibility:)
290
294
  #
291
- # @raise [PlanMyStuff::StaleObjectError] if remote updated_at differs from local
292
- #
293
295
  # @return [self]
294
296
  #
295
297
  def update!(user: nil, **attrs)
@@ -302,11 +304,12 @@ module PlanMyStuff
302
304
  meta_hash = metadata.to_h
303
305
 
304
306
  if attrs.key?(:visibility)
305
- new_visibility = attrs[:visibility].to_s
307
+ resolved_user = PlanMyStuff::UserResolver.resolve(user)
308
+ new_visibility = self.class.__send__(:resolve_visibility, attrs[:visibility], resolved_user).to_s
306
309
  meta_hash[:visibility] = new_visibility
307
310
  end
308
311
 
309
- serialized = MetadataParser.serialize(meta_hash, new_body)
312
+ serialized = PlanMyStuff::MetadataParser.serialize!(meta_hash, new_body)
310
313
  self.class.update!(id: id, repo: issue.repo, body: serialized)
311
314
 
312
315
  reload
@@ -332,8 +335,8 @@ module PlanMyStuff
332
335
  safe_read_field(github_response, :html_url)
333
336
  end
334
337
 
335
- # Serializes the comment to a JSON-safe hash, excluding the back-reference
336
- # to the parent issue to prevent recursive serialization cycles.
338
+ # Serializes the comment to a JSON-safe hash, excluding the back-reference to the parent issue to prevent
339
+ # recursive serialization cycles.
337
340
  #
338
341
  # @return [Hash]
339
342
  #
@@ -347,8 +350,8 @@ module PlanMyStuff
347
350
  metadata.schema_version.present?
348
351
  end
349
352
 
350
- # Returns the comment visibility as a symbol.
351
- # Uses the locally set value if present, otherwise falls back to metadata.
353
+ # Returns the comment visibility as a symbol. Uses the locally set value if present, otherwise falls back to
354
+ # metadata.
352
355
  #
353
356
  # @return [Symbol, nil] :public or :internal
354
357
  #
@@ -356,22 +359,21 @@ module PlanMyStuff
356
359
  super || metadata.visibility&.to_sym
357
360
  end
358
361
 
359
- # Checks if the comment is visible to the given user.
360
- # Public PMS comments: visible to everyone the parent issue is visible to.
361
- # Internal PMS comments: visible only to support users.
362
- # Non-PMS comments: visible only to support users.
362
+ # Checks if the comment is visible to the given user. Public PMS comments: visible to everyone the parent issue
363
+ # is visible to. Internal PMS comments: visible only to support users. Non-PMS comments: visible only to support
364
+ # users.
363
365
  #
364
366
  # @param user [Object, Integer] user object or user_id
365
367
  #
366
368
  # @return [Boolean]
367
369
  #
368
370
  def visible_to?(user)
369
- resolved = PMS::UserResolver.resolve(user)
371
+ resolved = PlanMyStuff::UserResolver.resolve(user)
370
372
 
371
373
  if pms_comment?
372
- issue.visible_to?(resolved) && (visibility != :internal || PMS::UserResolver.support?(resolved))
374
+ issue.visible_to?(resolved) && (visibility != :internal || PlanMyStuff::UserResolver.support?(resolved))
373
375
  else
374
- PMS::UserResolver.support?(resolved)
376
+ PlanMyStuff::UserResolver.support?(resolved)
375
377
  end
376
378
  end
377
379
 
@@ -407,9 +409,8 @@ module PlanMyStuff
407
409
  "#{existing_header}\n\n#{new_body}"
408
410
  end
409
411
 
410
- # Computes the +ActiveModel::Dirty+-style changes hash from the +update!+
411
- # +attrs+ hash vs the current in-memory state. Only includes keys whose
412
- # value actually differs.
412
+ # Computes the +ActiveModel::Dirty+-style changes hash from the +update!+ +attrs+ hash vs the current in-memory
413
+ # state. Only includes keys whose value actually differs.
413
414
  #
414
415
  # @param attrs [Hash]
415
416
  #
@@ -440,10 +441,11 @@ module PlanMyStuff
440
441
  self.id = read_field(github_comment, :id)
441
442
  self.raw_body = read_field(github_comment, :body)
442
443
  self.updated_at = parse_github_time(safe_read_field(github_comment, :updated_at))
444
+ self.created_at = parse_github_time(safe_read_field(github_comment, :created_at))
443
445
  self.issue = issue
444
446
 
445
- parsed = MetadataParser.parse(raw_body)
446
- self.metadata = CommentMetadata.from_hash(parsed[:metadata])
447
+ parsed = PlanMyStuff::MetadataParser.parse(raw_body)
448
+ self.metadata = PlanMyStuff::CommentMetadata.from_hash(parsed[:metadata])
447
449
  self.body = parsed[:body]
448
450
  self.visibility = metadata.visibility&.to_sym
449
451
  persisted!
@@ -461,16 +463,16 @@ module PlanMyStuff
461
463
  self.body = other.body
462
464
  self.raw_body = other.raw_body
463
465
  self.updated_at = other.updated_at
466
+ self.created_at = other.created_at
464
467
  self.issue = other.issue
465
468
  self.metadata = other.metadata
466
469
  self.visibility = other.visibility
467
470
  persisted!
468
471
  end
469
472
 
470
- # Raises StaleObjectError if the remote comment has been modified
471
- # since this instance was loaded.
473
+ # Raises StaleObjectError if the remote comment has been modified since this instance was loaded.
472
474
  #
473
- # @raise [PlanMyStuff::StaleObjectError]
475
+ # @raise [PlanMyStuff::StaleObjectError] if comment was modified after loading
474
476
  #
475
477
  # @return [void]
476
478
  #
@@ -485,7 +487,7 @@ module PlanMyStuff
485
487
  return if remote_time.nil?
486
488
  return if local_time && remote_time.to_i == local_time.to_i
487
489
 
488
- raise(StaleObjectError.new(
490
+ raise(PlanMyStuff::StaleObjectError.new(
489
491
  "Comment ##{id} has been modified remotely",
490
492
  local_updated_at: local_time,
491
493
  remote_updated_at: remote_time,
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanMyStuff
4
- class CommentMetadata < BaseMetadata
4
+ class CommentMetadata < PlanMyStuff::BaseMetadata
5
5
  # @return [Boolean] true if this comment holds the issue's body content
6
6
  attr_accessor :issue_body
7
7