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
@@ -8,7 +8,7 @@ module PlanMyStuff
8
8
  # PATCH /projects/:project_id/items/:item_id/assignment -> update (assign)
9
9
  # DELETE /projects/:project_id/items/:item_id/assignment -> destroy (unassign)
10
10
  #
11
- class AssignmentsController < ApplicationController
11
+ class AssignmentsController < PlanMyStuff::ApplicationController
12
12
  # PATCH /projects/:project_id/items/:item_id/assignment
13
13
  def update
14
14
  item = find_project_item
@@ -16,9 +16,15 @@ module PlanMyStuff
16
16
 
17
17
  item.assign!(assignees)
18
18
 
19
- flash[:success] = "Item assigned to #{assignees.join(', ')}."
19
+ flash[:success] =
20
+ if assignees.present?
21
+ "Item assigned to #{assignees.join(', ')}."
22
+ else
23
+ 'All assignees removed.'
24
+ end
20
25
  redirect_to(plan_my_stuff.project_path(params[:project_id]))
21
- rescue ArgumentError, PMS::Error => e
26
+ rescue ArgumentError, PlanMyStuff::Error => e
27
+ pms_handle_rescue(e)
22
28
  flash[:error] = e.message
23
29
  redirect_to(plan_my_stuff.project_path(params[:project_id]))
24
30
  end
@@ -39,7 +45,8 @@ module PlanMyStuff
39
45
 
40
46
  flash[:success] = "#{params[:username]} unassigned."
41
47
  redirect_to(plan_my_stuff.project_path(params[:project_id]))
42
- rescue ArgumentError, PMS::Error => e
48
+ rescue ArgumentError, PlanMyStuff::Error => e
49
+ pms_handle_rescue(e)
43
50
  flash[:error] = e.message
44
51
  redirect_to(plan_my_stuff.project_path(params[:project_id]))
45
52
  end
@@ -51,10 +58,10 @@ module PlanMyStuff
51
58
  # @return [PlanMyStuff::ProjectItem]
52
59
  #
53
60
  def find_project_item
54
- project = PMS::Project.find(params[:project_id].to_i)
61
+ project = PlanMyStuff::Project.find(params[:project_id].to_i)
55
62
  item = project.items.find { |i| i.id == params[:item_id] }
56
63
 
57
- raise(PMS::APIError, "Item not found: #{params[:item_id]}") unless item
64
+ raise(PlanMyStuff::APIError, "Item not found: #{params[:item_id]}") unless item
58
65
 
59
66
  item
60
67
  end
@@ -7,7 +7,7 @@ module PlanMyStuff
7
7
  #
8
8
  # PATCH /projects/:project_id/items/:item_id/status -> update (moves to new status)
9
9
  #
10
- class StatusesController < ApplicationController
10
+ class StatusesController < PlanMyStuff::ApplicationController
11
11
  # PATCH /projects/:project_id/items/:item_id/status
12
12
  def update
13
13
  item = find_project_item
@@ -16,7 +16,8 @@ module PlanMyStuff
16
16
 
17
17
  flash[:success] = "Item moved to #{params[:status]}."
18
18
  redirect_to(plan_my_stuff.project_path(params[:project_id]))
19
- rescue ArgumentError, PMS::Error => e
19
+ rescue ArgumentError, PlanMyStuff::Error => e
20
+ pms_handle_rescue(e)
20
21
  flash[:error] = e.message
21
22
  redirect_to(plan_my_stuff.project_path(params[:project_id]))
22
23
  end
@@ -28,10 +29,10 @@ module PlanMyStuff
28
29
  # @return [PlanMyStuff::ProjectItem]
29
30
  #
30
31
  def find_project_item
31
- project = PMS::Project.find(params[:project_id].to_i)
32
+ project = PlanMyStuff::Project.find(params[:project_id].to_i)
32
33
  item = project.items.find { |i| i.id == params[:item_id] }
33
34
 
34
- raise(PMS::APIError, "Item not found: #{params[:item_id]}") unless item
35
+ raise(PlanMyStuff::APIError, "Item not found: #{params[:item_id]}") unless item
35
36
 
36
37
  item
37
38
  end
@@ -1,22 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanMyStuff
4
- class ProjectItemsController < ApplicationController
4
+ class ProjectItemsController < PlanMyStuff::ApplicationController
5
5
  # POST /projects/:project_id/items
6
6
  def create
7
7
  project_number = params[:project_id].to_i
8
8
 
9
9
  if params[:draft] == '1'
10
- PMS::ProjectItem.create!(params[:title], draft: true, body: params[:body], project_number: project_number)
10
+ PlanMyStuff::ProjectItem.create!(
11
+ params[:title],
12
+ draft: true,
13
+ body: params[:body],
14
+ project_number: project_number,
15
+ )
11
16
  flash[:success] = 'Draft item created.'
12
17
  else
13
- issue = PMS::Issue.find(params[:issue_number].to_i)
14
- PMS::ProjectItem.create!(issue, project_number: project_number)
18
+ issue = PlanMyStuff::Issue.find(params[:issue_number].to_i)
19
+ PlanMyStuff::ProjectItem.create!(issue, project_number: project_number)
15
20
  flash[:success] = "Issue ##{issue.number} added to project."
16
21
  end
17
22
 
18
23
  redirect_to(plan_my_stuff.project_path(project_number))
19
- rescue ArgumentError, PMS::Error, Octokit::Error => e
24
+ rescue ArgumentError, PlanMyStuff::Error, Octokit::Error => e
25
+ pms_handle_rescue(e)
26
+ flash[:error] = e.message
27
+ redirect_to(plan_my_stuff.project_path(project_number))
28
+ end
29
+
30
+ # DELETE /projects/:project_id/items/:id
31
+ def destroy
32
+ project_number = params[:project_id].to_i
33
+ item_id = params[:id]
34
+
35
+ project = PlanMyStuff::Project.find(project_number)
36
+ item = project.items.find { |i| i.id == item_id }
37
+ raise(PlanMyStuff::APIError, "Item not found: #{item_id}") if item.nil?
38
+
39
+ item.destroy!(user: pms_current_user)
40
+
41
+ flash[:success] = 'Item removed from project.'
42
+ redirect_to(plan_my_stuff.project_path(project_number))
43
+ rescue ArgumentError, PlanMyStuff::Error, Octokit::Error => e
44
+ pms_handle_rescue(e)
20
45
  flash[:error] = e.message
21
46
  redirect_to(plan_my_stuff.project_path(project_number))
22
47
  end
@@ -1,28 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanMyStuff
4
- class ProjectsController < ApplicationController
4
+ class ProjectsController < PlanMyStuff::ApplicationController
5
5
  # GET /projects
6
6
  def index
7
- all_projects = PMS::BaseProject.list.reject(&:closed)
7
+ all_projects = PlanMyStuff::BaseProject.list.reject(&:closed)
8
8
  @filter = params[:filter].presence_in(%w[testing all]) || 'regular'
9
9
  @projects =
10
10
  case @filter
11
- when 'testing' then all_projects.select { |p| p.is_a?(PMS::TestingProject) }
12
- when 'regular' then all_projects.reject { |p| p.is_a?(PMS::TestingProject) }
11
+ when 'testing' then all_projects.select { |p| p.is_a?(PlanMyStuff::TestingProject) }
12
+ when 'regular' then all_projects.reject { |p| p.is_a?(PlanMyStuff::TestingProject) }
13
13
  else all_projects
14
14
  end
15
- @support_user = support_user?
16
15
  end
17
16
 
18
17
  # GET /projects/new
19
18
  def new
20
- @project = PMS::Project.new
19
+ @project = PlanMyStuff::Project.new
21
20
  end
22
21
 
23
22
  # POST /projects
24
23
  def create
25
- @project = PMS::Project.create!(
24
+ @project = PlanMyStuff::Project.create!(
26
25
  title: project_params[:title],
27
26
  readme: project_params[:readme] || '',
28
27
  description: project_params[:description],
@@ -31,32 +30,32 @@ module PlanMyStuff
31
30
 
32
31
  flash[:success] = 'Project was successfully created.'
33
32
  redirect_to(plan_my_stuff.project_path(@project.number))
34
- rescue PMS::ValidationError => e
35
- @project = PMS::Project.new(
33
+ rescue PlanMyStuff::ValidationError => e
34
+ pms_handle_rescue(e)
35
+ @project = PlanMyStuff::Project.new(
36
36
  title: project_params[:title],
37
37
  readme: project_params[:readme],
38
38
  description: project_params[:description],
39
39
  )
40
40
  flash.now[:error] = e.message
41
- render(:new, status: PMS.unprocessable_status)
41
+ render(:new, status: PlanMyStuff.unprocessable_status)
42
42
  end
43
43
 
44
44
  # GET /projects/:id
45
45
  def show
46
- @project = PMS::Project.find(params[:id].to_i)
46
+ @project = PlanMyStuff::Project.find(params[:id].to_i)
47
47
  @statuses = @project.statuses.pluck(:name)
48
48
  @items_by_status = @project.items.group_by(&:status)
49
- @support_user = support_user?
50
49
  end
51
50
 
52
51
  # GET /projects/:id/edit
53
52
  def edit
54
- @project = PMS::Project.find(params[:id].to_i)
53
+ @project = PlanMyStuff::Project.find(params[:id].to_i)
55
54
  end
56
55
 
57
56
  # PATCH/PUT /projects/:id
58
57
  def update
59
- @project = PMS::Project.find(params[:id].to_i)
58
+ @project = PlanMyStuff::Project.find(params[:id].to_i)
60
59
 
61
60
  @project.update!(
62
61
  title: project_params[:title],
@@ -66,9 +65,10 @@ module PlanMyStuff
66
65
 
67
66
  flash[:success] = 'Project was successfully updated.'
68
67
  redirect_to(plan_my_stuff.project_path(@project.number))
69
- rescue PMS::StaleObjectError
68
+ rescue PlanMyStuff::StaleObjectError => e
69
+ pms_handle_rescue(e)
70
70
  flash.now[:error] = 'Project was modified by someone else. Please review the latest changes and try again.'
71
- render(:edit, status: PMS.unprocessable_status)
71
+ render(:edit, status: PlanMyStuff.unprocessable_status)
72
72
  end
73
73
 
74
74
  private
@@ -7,7 +7,7 @@ module PlanMyStuff
7
7
  # Pass is submitted directly via button_to with result: "pass".
8
8
  # Fail renders a form (new) to capture result_notes before posting to create.
9
9
  #
10
- class ResultsController < ApplicationController
10
+ class ResultsController < PlanMyStuff::ApplicationController
11
11
  # GET /testing_projects/:testing_project_id/items/:item_id/result/new
12
12
  def new
13
13
  @project_number = params[:testing_project_id].to_i
@@ -19,21 +19,31 @@ module PlanMyStuff
19
19
  project_number = params[:testing_project_id].to_i
20
20
  item = find_project_item(project_number)
21
21
 
22
- if params[:result] == 'pass'
22
+ case params[:result]
23
+ when 'pass'
23
24
  item.mark_passed!(pms_current_user)
24
25
  flash[:success] = 'Item marked as Passed.'
25
- else
26
+ when 'fail'
26
27
  item.mark_failed!(pms_current_user, result_notes: params[:result_notes])
27
28
  flash[:success] = 'Item marked as Failed.'
29
+ else
30
+ raise(ArgumentError, "Invalid result: #{params[:result].inspect}")
28
31
  end
29
32
 
30
33
  redirect_to(plan_my_stuff.testing_project_path(project_number))
31
- rescue PMS::ValidationError => e
32
- flash.now[:error] = e.message
33
- @project_number = params[:testing_project_id].to_i
34
- @item_id = params[:item_id]
35
- render(:new, status: PMS.unprocessable_status)
36
- rescue ArgumentError, PMS::Error, Octokit::Error => e
34
+ rescue PlanMyStuff::ValidationError => e
35
+ pms_handle_rescue(e)
36
+ if params[:result] == 'fail'
37
+ flash.now[:error] = e.message
38
+ @project_number = params[:testing_project_id].to_i
39
+ @item_id = params[:item_id]
40
+ render(:new, status: PlanMyStuff.unprocessable_status)
41
+ else
42
+ flash[:error] = e.message
43
+ redirect_to(plan_my_stuff.testing_project_path(params[:testing_project_id].to_i))
44
+ end
45
+ rescue ArgumentError, PlanMyStuff::Error, Octokit::Error => e
46
+ pms_handle_rescue(e)
37
47
  flash[:error] = e.message
38
48
  redirect_to(plan_my_stuff.testing_project_path(params[:testing_project_id].to_i))
39
49
  end
@@ -43,9 +53,9 @@ module PlanMyStuff
43
53
  # @param project_number [Integer]
44
54
  # @return [PlanMyStuff::TestingProjectItem]
45
55
  def find_project_item(project_number)
46
- project = PMS::TestingProject.find(project_number)
56
+ project = PlanMyStuff::TestingProject.find(project_number)
47
57
  item = project.items.find { |i| i.id == params[:item_id] }
48
- raise(PMS::APIError, "Item not found: #{params[:item_id]}") unless item
58
+ raise(PlanMyStuff::APIError, "Item not found: #{params[:item_id]}") unless item
49
59
 
50
60
  item
51
61
  end
@@ -1,16 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanMyStuff
4
- class TestingProjectItemsController < ApplicationController
4
+ class TestingProjectItemsController < PlanMyStuff::ApplicationController
5
5
  # GET /testing_projects/:testing_project_id/items/new
6
6
  def new
7
- @project = PMS::TestingProject.find(params[:testing_project_id].to_i)
7
+ @project = PlanMyStuff::TestingProject.find(params[:testing_project_id].to_i)
8
+ rescue PlanMyStuff::Error, Octokit::Error => e
9
+ pms_handle_rescue(e)
10
+ flash[:error] = e.message
11
+ redirect_to(plan_my_stuff.testing_projects_path)
8
12
  end
9
13
 
10
14
  # POST /testing_projects/:testing_project_id/items
11
15
  def create
12
16
  project_number = params[:testing_project_id].to_i
13
- item = PMS::TestingProjectItem.create!(
17
+ item = PlanMyStuff::TestingProjectItem.create!(
14
18
  item_params[:title],
15
19
  draft: true,
16
20
  body: item_params[:body],
@@ -24,7 +28,8 @@ module PlanMyStuff
24
28
 
25
29
  flash[:success] = 'Item added.'
26
30
  redirect_to(plan_my_stuff.testing_project_path(project_number))
27
- rescue ArgumentError, PMS::Error, Octokit::Error => e
31
+ rescue ArgumentError, PlanMyStuff::Error, Octokit::Error => e
32
+ pms_handle_rescue(e)
28
33
  flash[:error] = e.message
29
34
  redirect_to(plan_my_stuff.testing_project_path(project_number))
30
35
  end
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanMyStuff
4
- class TestingProjectsController < ApplicationController
4
+ class TestingProjectsController < PlanMyStuff::ApplicationController
5
5
  # GET /testing_projects/new
6
6
  def new
7
- @project = PMS::TestingProject.new
7
+ @project = PlanMyStuff::TestingProject.new
8
8
  @project.metadata.subject_urls = [params[:subject_url]] if params[:subject_url].present?
9
9
  end
10
10
 
11
11
  # POST /testing_projects
12
12
  def create
13
- @project = PMS::TestingProject.create!(
13
+ @project = PlanMyStuff::TestingProject.create!(
14
14
  title: testing_project_params[:title],
15
15
  description: testing_project_params[:description],
16
16
  subject_urls: parse_subject_urls(testing_project_params[:subject_urls]),
@@ -21,28 +21,34 @@ module PlanMyStuff
21
21
 
22
22
  flash[:success] = 'Testing project was successfully created.'
23
23
  redirect_to(plan_my_stuff.testing_project_path(@project.number))
24
- rescue PMS::ValidationError => e
25
- @project = PMS::TestingProject.new(title: testing_project_params[:title])
24
+ rescue PlanMyStuff::ValidationError => e
25
+ pms_handle_rescue(e)
26
+ @project = PlanMyStuff::TestingProject.new(
27
+ title: testing_project_params[:title],
28
+ description: testing_project_params[:description],
29
+ )
30
+ @project.metadata.subject_urls = parse_subject_urls(testing_project_params[:subject_urls])
31
+ @project.metadata.due_date = safe_parse_due_date(testing_project_params[:due_date])
32
+ @project.metadata.deadline_miss_reason = testing_project_params[:deadline_miss_reason]
26
33
  flash.now[:error] = e.message
27
- render(:new, status: PMS.unprocessable_status)
34
+ render(:new, status: PlanMyStuff.unprocessable_status)
28
35
  end
29
36
 
30
37
  # GET /testing_projects/:id
31
38
  def show
32
- @project = PMS::TestingProject.find(params[:id].to_i)
39
+ @project = PlanMyStuff::TestingProject.find(params[:id].to_i)
33
40
  @statuses = @project.statuses.pluck(:name)
34
41
  @items_by_status = @project.items.group_by(&:status)
35
- @support_user = support_user?
36
42
  end
37
43
 
38
44
  # GET /testing_projects/:id/edit
39
45
  def edit
40
- @project = PMS::TestingProject.find(params[:id].to_i)
46
+ @project = PlanMyStuff::TestingProject.find(params[:id].to_i)
41
47
  end
42
48
 
43
49
  # PATCH/PUT /testing_projects/:id
44
50
  def update
45
- @project = PMS::TestingProject.find(params[:id].to_i)
51
+ @project = PlanMyStuff::TestingProject.find(params[:id].to_i)
46
52
 
47
53
  @project.update!(
48
54
  title: testing_project_params[:title],
@@ -56,13 +62,15 @@ module PlanMyStuff
56
62
 
57
63
  flash[:success] = 'Testing project was successfully updated.'
58
64
  redirect_to(plan_my_stuff.testing_project_path(@project.number))
59
- rescue PMS::StaleObjectError
65
+ rescue PlanMyStuff::StaleObjectError => e
66
+ pms_handle_rescue(e)
60
67
  flash.now[:error] =
61
68
  'Testing project was modified by someone else. Please review the latest changes and try again.'
62
- render(:edit, status: PMS.unprocessable_status)
63
- rescue PMS::ValidationError => e
69
+ render(:edit, status: PlanMyStuff.unprocessable_status)
70
+ rescue PlanMyStuff::ValidationError => e
71
+ pms_handle_rescue(e)
64
72
  flash.now[:error] = e.message
65
- render(:edit, status: PMS.unprocessable_status)
73
+ render(:edit, status: PlanMyStuff.unprocessable_status)
66
74
  end
67
75
 
68
76
  private
@@ -80,6 +88,14 @@ module PlanMyStuff
80
88
 
81
89
  Date.parse(raw)
82
90
  rescue ArgumentError, TypeError
91
+ raise(PlanMyStuff::ValidationError, 'Due date must be a valid date.')
92
+ end
93
+
94
+ # @param raw [String, nil]
95
+ # @return [Date, nil]
96
+ def safe_parse_due_date(raw)
97
+ parse_due_date(raw)
98
+ rescue PlanMyStuff::ValidationError
83
99
  nil
84
100
  end
85
101
 
@@ -5,7 +5,7 @@ module PlanMyStuff
5
5
  class AwsController < ActionController::API
6
6
  VALID_SNS_MESSAGE_TYPES = %w[Notification SubscriptionConfirmation UnsubscribeConfirmation].freeze
7
7
 
8
- before_action :verify_signature!
8
+ before_action :verify_signature
9
9
 
10
10
  # POST /webhooks/aws
11
11
  def create
@@ -14,6 +14,13 @@ module PlanMyStuff
14
14
 
15
15
  Rails.logger.info("[PlanMyStuff] SNS #{message_type}: #{sns_params.to_unsafe_h.inspect}")
16
16
 
17
+ if message_type == 'SubscriptionConfirmation'
18
+ confirm_subscription!
19
+ head(:ok)
20
+
21
+ return
22
+ end
23
+
17
24
  unless message_type == 'Notification'
18
25
  head(:ok)
19
26
 
@@ -42,14 +49,36 @@ module PlanMyStuff
42
49
 
43
50
  private
44
51
 
52
+ # Visits the SubscribeURL provided by SNS to confirm the subscription. Without this, SNS leaves the
53
+ # endpoint in +PendingConfirmation+ and never delivers Notifications.
54
+ #
55
+ # @return [void]
56
+ #
57
+ def confirm_subscription!
58
+ subscribe_url = sns_params[:subscribe_url].to_s
59
+ if subscribe_url.blank?
60
+ Rails.logger.warn('[PlanMyStuff] SubscriptionConfirmation missing SubscribeURL')
61
+
62
+ return
63
+ end
64
+
65
+ uri = URI.parse(subscribe_url)
66
+ http = Net::HTTP.new(uri.host, uri.port)
67
+ http.use_ssl = (uri.scheme == 'https')
68
+ response = http.request(Net::HTTP::Get.new(uri.request_uri))
69
+
70
+ Rails.logger.info("[PlanMyStuff] Confirmed SNS subscription: HTTP #{response.code}")
71
+ rescue => e
72
+ Rails.logger.error("[PlanMyStuff] SNS subscription confirmation failed: #{e.class}: #{e.message}")
73
+ end
74
+
45
75
  # Validates the SNS message signature.
46
- # Checks message type header and topic ARN first (cheap), then
47
- # delegates to the configured verifier class for cryptographic
48
- # certificate-based verification.
76
+ # Checks message type header and topic ARN first (cheap), then delegates to the configured verifier class
77
+ # for cryptographic certificate-based verification.
49
78
  #
50
79
  # @return [void]
51
80
  #
52
- def verify_signature!
81
+ def verify_signature
53
82
  message_type = request.headers['x-amz-sns-message-type'].to_s
54
83
 
55
84
  if VALID_SNS_MESSAGE_TYPES.exclude?(message_type)
@@ -82,9 +111,8 @@ module PlanMyStuff
82
111
  end
83
112
  end
84
113
 
85
- # Parses the SNS envelope. Returns empty params if the body is
86
- # not valid JSON so signature verification rejects the request
87
- # with 401 instead of bubbling a 500.
114
+ # Parses the SNS envelope. Returns empty params if the body is not valid JSON so signature verification
115
+ # rejects the request with 401 instead of bubbling a 500.
88
116
  #
89
117
  # @return [ActionController::Parameters]
90
118
  #
@@ -94,8 +122,8 @@ module PlanMyStuff
94
122
  @sns_params ||= ActionController::Parameters.new
95
123
  end
96
124
 
97
- # Parses the nested SNS message payload. Returns empty params on
98
- # malformed JSON so callers degrade gracefully.
125
+ # Parses the nested SNS message payload. Returns empty params on malformed JSON so callers degrade
126
+ # gracefully.
99
127
  #
100
128
  # @return [ActionController::Parameters]
101
129
  #
@@ -106,8 +134,7 @@ module PlanMyStuff
106
134
  ActionController::Parameters.new
107
135
  end
108
136
 
109
- # Checks whether any resource ARN in the ECS event matches the
110
- # configured service identifier suffix.
137
+ # Checks whether any resource ARN in the ECS event matches the configured service identifier suffix.
111
138
  #
112
139
  # @param service_identifier [String, nil]
113
140
  #
@@ -119,15 +146,21 @@ module PlanMyStuff
119
146
  sns_message_params.fetch(:resources, []).any? { |arn| arn.end_with?(service_identifier) }
120
147
  end
121
148
 
122
- # Finds "Release in Progress" items whose linked issue commit SHA
123
- # matches the configured production commit SHA (prefix match),
124
- # then completes deployment for each.
149
+ # Finds "Release in Progress" items whose linked issue commit SHA matches the configured production
150
+ # commit SHA (prefix match), then completes deployment for each.
125
151
  #
126
152
  # @return [void]
127
153
  #
128
154
  def handle_deployment_completed
129
155
  sha = PlanMyStuff.configuration.production_commit_sha
130
- raise(PlanMyStuff::PipelineError, 'production_commit_sha is not configured') if sha.nil?
156
+
157
+ if sha.blank?
158
+ Rails.logger.error(
159
+ '[PlanMyStuff] production_commit_sha is not configured - skipping deployment completion',
160
+ )
161
+
162
+ return
163
+ end
131
164
 
132
165
  release_in_progress_items.each do |item|
133
166
  next if item.draft?
@@ -143,7 +176,7 @@ module PlanMyStuff
143
176
  # @return [Array<PlanMyStuff::ProjectItem>]
144
177
  #
145
178
  def release_in_progress_items
146
- project_number = PlanMyStuff::Pipeline.resolve_pipeline_project_number
179
+ project_number = PlanMyStuff::Pipeline.resolve_pipeline_project_number!
147
180
  project = PlanMyStuff::Project.find(project_number)
148
181
 
149
182
  release_status = PlanMyStuff::Pipeline.resolve_status_name(