plan_my_stuff 0.7.0 → 0.8.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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -1
  3. data/README.md +100 -103
  4. data/app/controllers/plan_my_stuff/application_controller.rb +22 -3
  5. data/app/controllers/plan_my_stuff/comments_controller.rb +14 -16
  6. data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +23 -13
  7. data/app/controllers/plan_my_stuff/issues/closures_controller.rb +7 -5
  8. data/app/controllers/plan_my_stuff/issues/links_controller.rb +14 -18
  9. data/app/controllers/plan_my_stuff/issues/takes_controller.rb +99 -28
  10. data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +13 -5
  11. data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +7 -5
  12. data/app/controllers/plan_my_stuff/issues_controller.rb +24 -28
  13. data/app/controllers/plan_my_stuff/labels_controller.rb +21 -5
  14. data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +13 -6
  15. data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +5 -4
  16. data/app/controllers/plan_my_stuff/project_items_controller.rb +30 -5
  17. data/app/controllers/plan_my_stuff/projects_controller.rb +16 -16
  18. data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +21 -11
  19. data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +9 -4
  20. data/app/controllers/plan_my_stuff/testing_projects_controller.rb +30 -14
  21. data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +50 -17
  22. data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +32 -49
  23. data/app/jobs/plan_my_stuff/application_job.rb +2 -3
  24. data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +15 -22
  25. data/app/views/plan_my_stuff/comments/edit.html.erb +1 -3
  26. data/app/views/plan_my_stuff/comments/partials/_form.html.erb +1 -0
  27. data/app/views/plan_my_stuff/issues/edit.html.erb +2 -4
  28. data/app/views/plan_my_stuff/issues/index.html.erb +2 -2
  29. data/app/views/plan_my_stuff/issues/new.html.erb +2 -4
  30. data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +23 -2
  31. data/app/views/plan_my_stuff/issues/partials/_form.html.erb +1 -0
  32. data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +2 -1
  33. data/app/views/plan_my_stuff/issues/partials/_links.html.erb +50 -7
  34. data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +2 -1
  35. data/app/views/plan_my_stuff/issues/show.html.erb +5 -2
  36. data/app/views/plan_my_stuff/partials/_flash.html.erb +4 -0
  37. data/app/views/plan_my_stuff/projects/edit.html.erb +1 -3
  38. data/app/views/plan_my_stuff/projects/index.html.erb +1 -1
  39. data/app/views/plan_my_stuff/projects/new.html.erb +1 -3
  40. data/app/views/plan_my_stuff/projects/partials/_form.html.erb +1 -0
  41. data/app/views/plan_my_stuff/projects/show.html.erb +13 -3
  42. data/app/views/plan_my_stuff/testing_project_items/new.html.erb +1 -3
  43. data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +1 -3
  44. data/app/views/plan_my_stuff/testing_projects/edit.html.erb +1 -3
  45. data/app/views/plan_my_stuff/testing_projects/new.html.erb +1 -3
  46. data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +4 -3
  47. data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +1 -0
  48. data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +1 -0
  49. data/app/views/plan_my_stuff/testing_projects/show.html.erb +2 -2
  50. data/config/routes.rb +2 -2
  51. data/lib/generators/plan_my_stuff/install/templates/initializer.rb +51 -2
  52. data/lib/plan_my_stuff/approval.rb +12 -4
  53. data/lib/plan_my_stuff/aws_sns_simulator.rb +12 -6
  54. data/lib/plan_my_stuff/base_metadata.rb +4 -15
  55. data/lib/plan_my_stuff/base_project.rb +68 -55
  56. data/lib/plan_my_stuff/base_project_item.rb +61 -57
  57. data/lib/plan_my_stuff/base_project_metadata.rb +1 -1
  58. data/lib/plan_my_stuff/client.rb +136 -48
  59. data/lib/plan_my_stuff/comment.rb +57 -57
  60. data/lib/plan_my_stuff/comment_metadata.rb +1 -1
  61. data/lib/plan_my_stuff/configuration.rb +95 -82
  62. data/lib/plan_my_stuff/errors.rb +10 -10
  63. data/lib/plan_my_stuff/graphql/queries.rb +1 -1
  64. data/lib/plan_my_stuff/issue.rb +467 -333
  65. data/lib/plan_my_stuff/issue_metadata.rb +10 -10
  66. data/lib/plan_my_stuff/label.rb +32 -16
  67. data/lib/plan_my_stuff/link.rb +15 -15
  68. data/lib/plan_my_stuff/markdown.rb +12 -6
  69. data/lib/plan_my_stuff/metadata_parser.rb +3 -1
  70. data/lib/plan_my_stuff/notifications.rb +1 -1
  71. data/lib/plan_my_stuff/pipeline/completed_sweep.rb +2 -2
  72. data/lib/plan_my_stuff/pipeline/issue_linker.rb +1 -1
  73. data/lib/plan_my_stuff/pipeline.rb +61 -83
  74. data/lib/plan_my_stuff/project.rb +4 -4
  75. data/lib/plan_my_stuff/project_item_metadata.rb +1 -1
  76. data/lib/plan_my_stuff/project_metadata.rb +1 -1
  77. data/lib/plan_my_stuff/reminders/closer.rb +1 -1
  78. data/lib/plan_my_stuff/reminders/fire.rb +3 -3
  79. data/lib/plan_my_stuff/reminders/sweep.rb +4 -4
  80. data/lib/plan_my_stuff/repo.rb +12 -6
  81. data/lib/plan_my_stuff/test_helpers.rb +11 -11
  82. data/lib/plan_my_stuff/testing_project.rb +12 -11
  83. data/lib/plan_my_stuff/testing_project_item.rb +11 -9
  84. data/lib/plan_my_stuff/testing_project_metadata.rb +2 -2
  85. data/lib/plan_my_stuff/version.rb +1 -1
  86. data/lib/plan_my_stuff/webhook_replayer.rb +14 -2
  87. data/lib/plan_my_stuff.rb +26 -2
  88. data/lib/tasks/plan_my_stuff.rake +33 -20
  89. metadata +3 -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,11 +20,13 @@ 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
@@ -37,9 +38,10 @@ module PlanMyStuff
37
38
  # @param visibility [Symbol] :public or :internal
38
39
  # @param custom_fields [Hash]
39
40
  # @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.
41
+ # @param waiting_on_reply [Boolean] when true and the author is a support user, marks the issue as waiting on
42
+ # an end-user reply. Ignored for non-support authors.
43
+ #
44
+ # @raise [PlanMyStuff::LockedIssueError] if the parent issue is locked
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,32 @@ 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!(user: user, body: body)
281
283
  end
282
284
 
283
285
  self
284
286
  end
285
287
 
286
- # Updates this comment on GitHub. Raises StaleObjectError if the remote
287
- # has been modified since this instance was loaded.
288
+ # Updates this comment on GitHub. Raises StaleObjectError if the remote has been modified since this instance was
289
+ # loaded.
288
290
  #
289
291
  # @param attrs [Hash] attributes to update (body:, visibility:)
290
292
  #
291
- # @raise [PlanMyStuff::StaleObjectError] if remote updated_at differs from local
292
- #
293
293
  # @return [self]
294
294
  #
295
295
  def update!(user: nil, **attrs)
@@ -302,11 +302,12 @@ module PlanMyStuff
302
302
  meta_hash = metadata.to_h
303
303
 
304
304
  if attrs.key?(:visibility)
305
- new_visibility = attrs[:visibility].to_s
305
+ resolved_user = PlanMyStuff::UserResolver.resolve(user)
306
+ new_visibility = self.class.__send__(:resolve_visibility, attrs[:visibility], resolved_user).to_s
306
307
  meta_hash[:visibility] = new_visibility
307
308
  end
308
309
 
309
- serialized = MetadataParser.serialize(meta_hash, new_body)
310
+ serialized = PlanMyStuff::MetadataParser.serialize!(meta_hash, new_body)
310
311
  self.class.update!(id: id, repo: issue.repo, body: serialized)
311
312
 
312
313
  reload
@@ -332,8 +333,8 @@ module PlanMyStuff
332
333
  safe_read_field(github_response, :html_url)
333
334
  end
334
335
 
335
- # Serializes the comment to a JSON-safe hash, excluding the back-reference
336
- # to the parent issue to prevent recursive serialization cycles.
336
+ # Serializes the comment to a JSON-safe hash, excluding the back-reference to the parent issue to prevent
337
+ # recursive serialization cycles.
337
338
  #
338
339
  # @return [Hash]
339
340
  #
@@ -347,8 +348,8 @@ module PlanMyStuff
347
348
  metadata.schema_version.present?
348
349
  end
349
350
 
350
- # Returns the comment visibility as a symbol.
351
- # Uses the locally set value if present, otherwise falls back to metadata.
351
+ # Returns the comment visibility as a symbol. Uses the locally set value if present, otherwise falls back to
352
+ # metadata.
352
353
  #
353
354
  # @return [Symbol, nil] :public or :internal
354
355
  #
@@ -356,22 +357,21 @@ module PlanMyStuff
356
357
  super || metadata.visibility&.to_sym
357
358
  end
358
359
 
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.
360
+ # Checks if the comment is visible to the given user. Public PMS comments: visible to everyone the parent issue
361
+ # is visible to. Internal PMS comments: visible only to support users. Non-PMS comments: visible only to support
362
+ # users.
363
363
  #
364
364
  # @param user [Object, Integer] user object or user_id
365
365
  #
366
366
  # @return [Boolean]
367
367
  #
368
368
  def visible_to?(user)
369
- resolved = PMS::UserResolver.resolve(user)
369
+ resolved = PlanMyStuff::UserResolver.resolve(user)
370
370
 
371
371
  if pms_comment?
372
- issue.visible_to?(resolved) && (visibility != :internal || PMS::UserResolver.support?(resolved))
372
+ issue.visible_to?(resolved) && (visibility != :internal || PlanMyStuff::UserResolver.support?(resolved))
373
373
  else
374
- PMS::UserResolver.support?(resolved)
374
+ PlanMyStuff::UserResolver.support?(resolved)
375
375
  end
376
376
  end
377
377
 
@@ -407,9 +407,8 @@ module PlanMyStuff
407
407
  "#{existing_header}\n\n#{new_body}"
408
408
  end
409
409
 
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.
410
+ # Computes the +ActiveModel::Dirty+-style changes hash from the +update!+ +attrs+ hash vs the current in-memory
411
+ # state. Only includes keys whose value actually differs.
413
412
  #
414
413
  # @param attrs [Hash]
415
414
  #
@@ -440,10 +439,11 @@ module PlanMyStuff
440
439
  self.id = read_field(github_comment, :id)
441
440
  self.raw_body = read_field(github_comment, :body)
442
441
  self.updated_at = parse_github_time(safe_read_field(github_comment, :updated_at))
442
+ self.created_at = parse_github_time(safe_read_field(github_comment, :created_at))
443
443
  self.issue = issue
444
444
 
445
- parsed = MetadataParser.parse(raw_body)
446
- self.metadata = CommentMetadata.from_hash(parsed[:metadata])
445
+ parsed = PlanMyStuff::MetadataParser.parse(raw_body)
446
+ self.metadata = PlanMyStuff::CommentMetadata.from_hash(parsed[:metadata])
447
447
  self.body = parsed[:body]
448
448
  self.visibility = metadata.visibility&.to_sym
449
449
  persisted!
@@ -461,16 +461,16 @@ module PlanMyStuff
461
461
  self.body = other.body
462
462
  self.raw_body = other.raw_body
463
463
  self.updated_at = other.updated_at
464
+ self.created_at = other.created_at
464
465
  self.issue = other.issue
465
466
  self.metadata = other.metadata
466
467
  self.visibility = other.visibility
467
468
  persisted!
468
469
  end
469
470
 
470
- # Raises StaleObjectError if the remote comment has been modified
471
- # since this instance was loaded.
471
+ # Raises StaleObjectError if the remote comment has been modified since this instance was loaded.
472
472
  #
473
- # @raise [PlanMyStuff::StaleObjectError]
473
+ # @raise [PlanMyStuff::StaleObjectError] if comment was modified after loading
474
474
  #
475
475
  # @return [void]
476
476
  #
@@ -485,7 +485,7 @@ module PlanMyStuff
485
485
  return if remote_time.nil?
486
486
  return if local_time && remote_time.to_i == local_time.to_i
487
487
 
488
- raise(StaleObjectError.new(
488
+ raise(PlanMyStuff::StaleObjectError.new(
489
489
  "Comment ##{id} has been modified remotely",
490
490
  local_updated_at: local_time,
491
491
  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