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
@@ -2,14 +2,13 @@
2
2
 
3
3
  module PlanMyStuff
4
4
  module Issues
5
- # CRUD for manager approvals on an issue. Backs the approvals panel
6
- # on the issue show view.
5
+ # CRUD for manager approvals on an issue. Backs the approvals panel on the issue show view.
7
6
  #
8
7
  # POST /issues/:issue_id/approvals -> create (adds approver(s))
9
8
  # PATCH /issues/:issue_id/approvals/:id -> update (approves or revokes)
10
9
  # DELETE /issues/:issue_id/approvals/:id -> destroy (removes an approver)
11
10
  #
12
- class ApprovalsController < ApplicationController
11
+ class ApprovalsController < PlanMyStuff::ApplicationController
13
12
  # POST /issues/:issue_id/approvals
14
13
  def create
15
14
  unless support_user?
@@ -24,12 +23,13 @@ module PlanMyStuff
24
23
  return
25
24
  end
26
25
 
27
- issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
26
+ issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
28
27
  issue.request_approvals!(user_ids: user_ids, user: pms_current_user)
29
28
 
30
29
  flash[:success] = 'Approvers were successfully added.'
31
30
  redirect_to(show_path)
32
- rescue PMS::AuthorizationError, PMS::ValidationError => e
31
+ rescue PlanMyStuff::AuthorizationError, PlanMyStuff::ValidationError => e
32
+ pms_handle_rescue(e)
33
33
  flash[:error] = e.message
34
34
  redirect_to(show_path)
35
35
  end
@@ -39,32 +39,41 @@ module PlanMyStuff
39
39
  status = approval_params[:status].to_s
40
40
  target_id = params[:id].to_i
41
41
 
42
- if %w[approved pending].exclude?(status)
42
+ if PlanMyStuff::Approval::STATUSES.exclude?(status)
43
43
  head(:bad_request)
44
44
  return
45
45
  end
46
46
 
47
- issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
48
- caller_id = pms_current_user.present? ? PMS::UserResolver.user_id(pms_current_user) : nil
47
+ issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
48
+ caller_id = pms_current_user.present? ? PlanMyStuff::UserResolver.user_id(pms_current_user) : nil
49
49
 
50
- if status == 'approved'
50
+ case status
51
+ when 'approved'
51
52
  unless caller_id == target_id
52
53
  redirect_to_unauthorized(show_path)
53
54
  return
54
55
  end
55
56
  issue.approve!(user: pms_current_user)
56
57
  flash[:success] = 'Approval recorded.'
58
+ when 'rejected'
59
+ unless caller_id == target_id
60
+ redirect_to_unauthorized(show_path)
61
+ return
62
+ end
63
+ issue.reject!(user: pms_current_user)
64
+ flash[:success] = 'Rejection recorded.'
57
65
  else
58
66
  if caller_id != target_id && !support_user?
59
67
  redirect_to_unauthorized(show_path)
60
68
  return
61
69
  end
62
70
  issue.revoke_approval!(user: pms_current_user, target_user_id: target_id)
63
- flash[:success] = 'Approval revoked.'
71
+ flash[:success] = 'Response revoked.'
64
72
  end
65
73
 
66
74
  redirect_to(show_path)
67
- rescue PMS::AuthorizationError, PMS::ValidationError => e
75
+ rescue PlanMyStuff::AuthorizationError, PlanMyStuff::ValidationError => e
76
+ pms_handle_rescue(e)
68
77
  flash[:error] = e.message
69
78
  redirect_to(show_path)
70
79
  end
@@ -76,12 +85,13 @@ module PlanMyStuff
76
85
  return
77
86
  end
78
87
 
79
- issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
88
+ issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
80
89
  issue.remove_approvers!(user_ids: [params[:id].to_i], user: pms_current_user)
81
90
 
82
91
  flash[:success] = 'Approver was successfully removed.'
83
92
  redirect_to(show_path)
84
- rescue PMS::AuthorizationError, PMS::ValidationError => e
93
+ rescue PlanMyStuff::AuthorizationError, PlanMyStuff::ValidationError => e
94
+ pms_handle_rescue(e)
85
95
  flash[:error] = e.message
86
96
  redirect_to(show_path)
87
97
  end
@@ -8,27 +8,29 @@ module PlanMyStuff
8
8
  # POST /issues/:issue_id/closure -> create (closes)
9
9
  # DELETE /issues/:issue_id/closure -> destroy (reopens)
10
10
  #
11
- class ClosuresController < ApplicationController
11
+ class ClosuresController < PlanMyStuff::ApplicationController
12
12
  # POST /issues/:issue_id/closure
13
13
  def create
14
- issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
14
+ issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
15
15
  issue.update!(state: :closed)
16
16
 
17
17
  flash[:success] = 'Issue was successfully closed.'
18
18
  redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
19
- rescue PMS::Error, ArgumentError => e
19
+ rescue PlanMyStuff::Error, ArgumentError => e
20
+ pms_handle_rescue(e)
20
21
  flash[:error] = e.message
21
22
  redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
22
23
  end
23
24
 
24
25
  # DELETE /issues/:issue_id/closure
25
26
  def destroy
26
- issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
27
+ issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
27
28
  issue.update!(state: :open)
28
29
 
29
30
  flash[:success] = 'Issue was successfully reopened.'
30
31
  redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
31
- rescue PMS::Error, ArgumentError => e
32
+ rescue PlanMyStuff::Error, ArgumentError => e
33
+ pms_handle_rescue(e)
32
34
  flash[:error] = e.message
33
35
  redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
34
36
  end
@@ -2,14 +2,13 @@
2
2
 
3
3
  module PlanMyStuff
4
4
  module Issues
5
- # CRUD for ticket relationships: +:related+ (metadata-backed) and
6
- # +:blocked_by+ / +:parent+ / +:sub_ticket+ / +:duplicate_of+
7
- # (native GitHub APIs). Backs the links panel on the issue show view.
5
+ # CRUD for ticket relationships: +:related+ (metadata-backed) and +:blocked_by+ / +:parent+ / +:sub_ticket+ /
6
+ # +:duplicate_of+ (native GitHub APIs). Backs the links panel on the issue show view.
8
7
  #
9
8
  # POST /issues/:issue_id/links -> create (adds a link)
10
9
  # DELETE /issues/:issue_id/links/:id -> destroy (removes a link)
11
10
  #
12
- class LinksController < ApplicationController
11
+ class LinksController < PlanMyStuff::ApplicationController
13
12
  NATIVE_DISPATCH = {
14
13
  'related' => { add: :add_related!, remove: :remove_related! },
15
14
  'blocked_by' => { add: :add_blocker!, remove: :remove_blocker! },
@@ -26,12 +25,13 @@ module PlanMyStuff
26
25
  return
27
26
  end
28
27
 
29
- issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
28
+ issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
30
29
  link = add_link(issue, type)
31
30
 
32
31
  flash[:success] = "Linked #{link}"
33
32
  redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
34
- rescue PMS::ValidationError, ActiveModel::ValidationError, ArgumentError => e
33
+ rescue PlanMyStuff::ValidationError, ActiveModel::ValidationError, ArgumentError => e
34
+ pms_handle_rescue(e)
35
35
  flash[:error] = e.message
36
36
  redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
37
37
  end
@@ -44,12 +44,13 @@ module PlanMyStuff
44
44
  return
45
45
  end
46
46
 
47
- issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
47
+ issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
48
48
  remove_link(issue, type, repo: repo, number: number)
49
49
 
50
50
  flash[:success] = "Unlinked #{repo}##{number}"
51
51
  redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
52
- rescue PMS::ValidationError, ActiveModel::ValidationError, ArgumentError => e
52
+ rescue PlanMyStuff::ValidationError, ActiveModel::ValidationError, ArgumentError => e
53
+ pms_handle_rescue(e)
53
54
  flash[:error] = e.message
54
55
  redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
55
56
  end
@@ -61,21 +62,18 @@ module PlanMyStuff
61
62
  params.require(:link).permit(:type, :issue_number, :repo)
62
63
  end
63
64
 
64
- # Only +:related+ may be mutated by non-support users. +destroy+
65
- # is not defined for +:duplicate_of+ (reopen via GitHub).
65
+ # All link mutations require a support user. +destroy+ is not defined for +:duplicate_of+ (reopen via GitHub).
66
66
  #
67
67
  # @return [Boolean]
68
68
  #
69
69
  def dispatch_allowed?(type, action)
70
70
  entry = NATIVE_DISPATCH[type]
71
71
  return false if entry.nil? || entry[action].nil?
72
- # FIXME: should non-support users be able to alter related links?
73
- return true if type == 'related'
74
72
 
75
73
  support_user?
76
74
  end
77
75
 
78
- # @return [PMS::Link]
76
+ # @return [PlanMyStuff::Link]
79
77
  def add_link(issue, type)
80
78
  method = NATIVE_DISPATCH.dig(type, :add)
81
79
  target = {
@@ -105,11 +103,9 @@ module PlanMyStuff
105
103
  end
106
104
  end
107
105
 
108
- # Parses +"{type}:{owner/repo}:{number}"+ out of +params[:id]+.
109
- # The route helper URL-encodes outgoing segments, but Rack's
110
- # default path decoder intentionally leaves +%2F+ escaped in
111
- # path params (anti-path-traversal), so the repo portion can
112
- # still contain +%2F+. Decode before splitting.
106
+ # Parses +"{type}:{owner/repo}:{number}"+ out of +params[:id]+. The route helper URL-encodes outgoing segments,
107
+ # but Rack's default path decoder intentionally leaves +%2F+ escaped in path params (anti-path-traversal), so
108
+ # the repo portion can still contain +%2F+. Decode before splitting.
113
109
  #
114
110
  # @param id [String]
115
111
  #
@@ -2,21 +2,23 @@
2
2
 
3
3
  module PlanMyStuff
4
4
  module Issues
5
- # Moves an issue's pipeline ProjectItem to "Started" via
6
- # +PlanMyStuff::Pipeline.take!+. Backs the "Take" button on the
7
- # mounted issue show view (T-RC-017). The primary UI path for a dev
8
- # picking up work on an issue they've been assigned to.
5
+ # Moves an issue's pipeline ProjectItem to "Started" via +PlanMyStuff::Pipeline.take!+. Backs the "Take" button on
6
+ # the mounted issue show view (T-RC-017). The primary UI path for a dev picking up work on an issue they've been
7
+ # assigned to.
9
8
  #
10
- class TakesController < ApplicationController
11
- before_action :require_support_user!
9
+ class TakesController < PlanMyStuff::ApplicationController
10
+ before_action :require_support_user
12
11
 
13
12
  # POST /issues/:issue_id/take
14
13
  def create
15
14
  issue_number = params[:issue_id].to_i
16
15
  repo = params[:repo]
17
16
 
17
+ issue = PlanMyStuff::Issue.find(issue_number, repo: repo)
18
+ guard_already_taken!(issue)
19
+
18
20
  project_item = PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue_number)
19
- project_item ||= add_to_pipeline(issue_number, repo)
21
+ project_item ||= add_to_pipeline(issue)
20
22
 
21
23
  PlanMyStuff::Pipeline.take!(project_item)
22
24
  assign_current_user(project_item)
@@ -24,18 +26,49 @@ module PlanMyStuff
24
26
 
25
27
  redirect_to(plan_my_stuff.issue_path(issue_number, repo: repo))
26
28
  rescue ArgumentError, PlanMyStuff::Error => e
29
+ pms_handle_rescue(e)
30
+ flash[:error] = e.message
31
+ redirect_to(plan_my_stuff.issue_path(issue_number, repo: repo))
32
+ end
33
+
34
+ # DELETE /issues/:issue_id/take
35
+ def destroy
36
+ issue_number = params[:issue_id].to_i
37
+ repo = params[:repo]
38
+
39
+ issue = PlanMyStuff::Issue.find(issue_number, repo: repo)
40
+ login = current_user_login
41
+ guard_release!(issue, login)
42
+
43
+ remaining = issue.assignees - [login]
44
+ project_item = PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue_number)
45
+
46
+ if project_item.present?
47
+ if remaining.empty?
48
+ issue.update!(assignees: [])
49
+ project_item.destroy!
50
+ else
51
+ project_item.assign!(remaining)
52
+ end
53
+ else
54
+ issue.update!(assignees: remaining)
55
+ end
56
+
57
+ flash[:success] = "Issue ##{issue_number} released."
58
+ redirect_to(plan_my_stuff.issue_path(issue_number, repo: repo))
59
+ rescue ArgumentError, PlanMyStuff::Error => e
60
+ pms_handle_rescue(e)
27
61
  flash[:error] = e.message
28
62
  redirect_to(plan_my_stuff.issue_path(issue_number, repo: repo))
29
63
  end
30
64
 
31
65
  private
32
66
 
33
- # Redirects non-support users back to the issue page. Mirrors
34
- # +Issues::ViewersController+'s authorization check.
67
+ # Redirects non-support users back to the issue page. Mirrors +Issues::ViewersController+'s authorization check.
35
68
  #
36
69
  # @return [void]
37
70
  #
38
- def require_support_user!
71
+ def require_support_user
39
72
  return if support_user?
40
73
 
41
74
  redirect_to_unauthorized(
@@ -43,46 +76,84 @@ module PlanMyStuff
43
76
  )
44
77
  end
45
78
 
46
- # Adds an issue to the pipeline project so +Pipeline.take!+ has
47
- # something to operate on when the user clicks "Take" on an
48
- # issue that hasn't been submitted to the pipeline yet.
79
+ # Best-effort race guard for two users clicking Take on the same issue. GitHub allows multiple assignees so a
80
+ # naive second take would silently pile on; refuse instead and let the second user see who already has it.
81
+ #
82
+ # @raise [PlanMyStuff::Error] if the issue already has assignees
49
83
  #
50
- # @param issue_number [Integer]
51
- # @param repo [String, nil]
84
+ # @param issue [PlanMyStuff::Issue]
85
+ #
86
+ # @return [void]
87
+ #
88
+ def guard_already_taken!(issue)
89
+ return if issue.assignees.blank?
90
+
91
+ raise(
92
+ PlanMyStuff::Error,
93
+ "Issue ##{issue.number} is already taken by @#{issue.assignees.join(', @')}.",
94
+ )
95
+ end
96
+
97
+ # Adds an issue to the pipeline project so +Pipeline.take!+ has something to operate on when the user clicks
98
+ # "Take" on an issue that hasn't been submitted to the pipeline yet.
99
+ #
100
+ # @param issue [PlanMyStuff::Issue]
52
101
  #
53
102
  # @return [PlanMyStuff::ProjectItem]
54
103
  #
55
- def add_to_pipeline(issue_number, repo)
56
- issue = PlanMyStuff::Issue.find(issue_number, repo: repo)
57
- pipeline_number = PlanMyStuff::Pipeline.resolve_pipeline_project_number
104
+ def add_to_pipeline(issue)
105
+ pipeline_number = PlanMyStuff::Pipeline.resolve_pipeline_project_number!
58
106
  PlanMyStuff::ProjectItem.create!(issue, project_number: pipeline_number)
59
107
  end
60
108
 
61
- # Looks up the current user's GitHub login in
62
- # +config.github_login_for+ (a +{user_id => login}+ hash) and
63
- # assigns them on the project item. Assignment failures are
64
- # logged and surfaced as a flash warning but do not revert the
65
- # status move.
109
+ # Looks up the current user's GitHub login in +config.github_login_for+ (a +{user_id => login}+ hash) and
110
+ # assigns them on the project item. Assignment failures are logged and surfaced as a flash warning but do not
111
+ # revert the status move.
66
112
  #
67
113
  # @param project_item [PlanMyStuff::ProjectItem]
68
114
  #
69
115
  # @return [void]
70
116
  #
71
117
  def assign_current_user(project_item)
72
- user = pms_current_user
73
- user_id = user.present? ? PMS::UserResolver.user_id(user) : nil
74
- login = PlanMyStuff.configuration.github_login_for[user_id]
118
+ login = current_user_login
75
119
 
76
120
  if login.blank?
77
- Rails.logger.info("[PMS] No github_login_for mapping for user #{user_id}; skipping Take assignment")
121
+ Rails.logger.info('[PlanMyStuff] No github_login_for mapping for current user; skipping Take assignment')
78
122
  return
79
123
  end
80
124
 
81
125
  project_item.assign!(login)
82
126
  rescue PlanMyStuff::Error => e
83
- Rails.logger.warn("[PMS] Take assignment failed: #{e.message}")
127
+ Rails.logger.warn("[PlanMyStuff] Take assignment failed: #{e.message}")
84
128
  flash[:warning] = "Issue taken but assignment failed: #{e.message}"
85
129
  end
130
+
131
+ # GitHub login for the current user via +config.github_login_for+ (a +{user_id => login}+ hash), or +nil+ if the
132
+ # user is unmapped or unauthenticated.
133
+ #
134
+ # @return [String, nil]
135
+ #
136
+ def current_user_login
137
+ user = pms_current_user
138
+ user_id = user.present? ? PlanMyStuff::UserResolver.user_id(user) : nil
139
+ PlanMyStuff.configuration.github_login_for[user_id]
140
+ end
141
+
142
+ # Refuses the release when the current user has no login mapping or is not in the issue's assignees. Mirrors the
143
+ # +guard_already_taken!+ check on +#create+.
144
+ #
145
+ # @raise [PlanMyStuff::Error] if the user cannot release the issue
146
+ #
147
+ # @param issue [PlanMyStuff::Issue]
148
+ # @param login [String, nil]
149
+ #
150
+ # @return [void]
151
+ #
152
+ def guard_release!(issue, login)
153
+ raise(PlanMyStuff::Error, 'No GitHub login mapping for current user; cannot release.') if login.blank?
154
+
155
+ raise(PlanMyStuff::Error, "Issue ##{issue.number} is not assigned to you.") if issue.assignees.exclude?(login)
156
+ end
86
157
  end
87
158
  end
88
159
  end
@@ -8,7 +8,7 @@ module PlanMyStuff
8
8
  # POST /issues/:issue_id/viewers -> create (adds viewer(s))
9
9
  # DELETE /issues/:issue_id/viewers/:id -> destroy (removes a viewer)
10
10
  #
11
- class ViewersController < ApplicationController
11
+ class ViewersController < PlanMyStuff::ApplicationController
12
12
  # POST /issues/:issue_id/viewers
13
13
  def create
14
14
  unless support_user?
@@ -23,11 +23,15 @@ module PlanMyStuff
23
23
  return
24
24
  end
25
25
 
26
- issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
27
- issue.add_viewers(user_ids: viewer_ids, user: pms_current_user)
26
+ issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
27
+ issue.add_viewers!(user_ids: viewer_ids, user: pms_current_user)
28
28
 
29
29
  flash[:success] = 'Viewers were successfully added.'
30
30
  redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id], repo: params[:repo]))
31
+ rescue PlanMyStuff::Error, Octokit::Error => e
32
+ pms_handle_rescue(e)
33
+ flash[:error] = e.message
34
+ redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id], repo: params[:repo]))
31
35
  end
32
36
 
33
37
  # DELETE /issues/:issue_id/viewers/:id
@@ -37,11 +41,15 @@ module PlanMyStuff
37
41
  return
38
42
  end
39
43
 
40
- issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
41
- issue.remove_viewers(user_ids: [params[:id].to_i], user: pms_current_user)
44
+ issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
45
+ issue.remove_viewers!(user_ids: [params[:id].to_i], user: pms_current_user)
42
46
 
43
47
  flash[:success] = 'Viewer was successfully removed.'
44
48
  redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id], repo: params[:repo]))
49
+ rescue PlanMyStuff::Error, Octokit::Error => e
50
+ pms_handle_rescue(e)
51
+ flash[:error] = e.message
52
+ redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id], repo: params[:repo]))
45
53
  end
46
54
  end
47
55
  end
@@ -8,7 +8,7 @@ module PlanMyStuff
8
8
  # POST /issues/:issue_id/waiting -> create (enters waiting-on-user)
9
9
  # DELETE /issues/:issue_id/waiting -> destroy (clears waiting-on-user)
10
10
  #
11
- class WaitingsController < ApplicationController
11
+ class WaitingsController < PlanMyStuff::ApplicationController
12
12
  # POST /issues/:issue_id/waiting
13
13
  def create
14
14
  unless support_user?
@@ -16,12 +16,13 @@ module PlanMyStuff
16
16
  return
17
17
  end
18
18
 
19
- issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
19
+ issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
20
20
  issue.enter_waiting_on_user!(user: pms_current_user)
21
21
 
22
22
  flash[:success] = 'Issue marked as waiting on user reply.'
23
23
  redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
24
- rescue PMS::Error, ArgumentError => e
24
+ rescue PlanMyStuff::Error, ArgumentError => e
25
+ pms_handle_rescue(e)
25
26
  flash[:error] = e.message
26
27
  redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
27
28
  end
@@ -33,12 +34,13 @@ module PlanMyStuff
33
34
  return
34
35
  end
35
36
 
36
- issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
37
+ issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
37
38
  issue.clear_waiting_on_user!
38
39
 
39
40
  flash[:success] = 'Waiting-on-user state cleared.'
40
41
  redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
41
- rescue PMS::Error, ArgumentError => e
42
+ rescue PlanMyStuff::Error, ArgumentError => e
43
+ pms_handle_rescue(e)
42
44
  flash[:error] = e.message
43
45
  redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
44
46
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanMyStuff
4
- class IssuesController < ApplicationController
4
+ class IssuesController < PlanMyStuff::ApplicationController
5
5
  # GET /issues
6
6
  def index
7
7
  @page = (params[:page] || 1).to_i
@@ -10,7 +10,7 @@ module PlanMyStuff
10
10
  @labels = params[:labels].present? ? Array.wrap(params[:labels]) : []
11
11
  @repo = params[:repo]
12
12
 
13
- @issues = PMS::Issue.list(
13
+ @issues = PlanMyStuff::Issue.list(
14
14
  repo: @repo,
15
15
  state: @state,
16
16
  labels: @labels,
@@ -21,13 +21,12 @@ module PlanMyStuff
21
21
 
22
22
  # GET /issues/new
23
23
  def new
24
- @issue = PMS::Issue.new
25
- @support_user = support_user?
24
+ @issue = PlanMyStuff::Issue.new
26
25
  end
27
26
 
28
27
  # POST /issues
29
28
  def create
30
- @issue = PMS::Issue.create!(
29
+ @issue = PlanMyStuff::Issue.create!(
31
30
  title: issue_params[:title],
32
31
  body: issue_params[:body],
33
32
  labels: parse_labels(issue_params[:labels]),
@@ -36,32 +35,31 @@ module PlanMyStuff
36
35
 
37
36
  flash[:success] = 'Issue was successfully created.'
38
37
  redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
39
- rescue PMS::ValidationError => e
40
- @issue = PMS::Issue.new(title: issue_params[:title], body: issue_params[:body])
41
- @support_user = support_user?
38
+ rescue PlanMyStuff::ValidationError => e
39
+ pms_handle_rescue(e)
40
+ @issue = PlanMyStuff::Issue.new(title: issue_params[:title], body: issue_params[:body])
42
41
  flash.now[:error] = e.message
43
- render(:new, status: PMS.unprocessable_status)
42
+ render(:new, status: PlanMyStuff.unprocessable_status)
44
43
  end
45
44
 
46
45
  # GET /issues/:id
47
46
  def show
48
- @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo])
47
+ @issue = PlanMyStuff::Issue.find(params[:id].to_i, repo: params[:repo])
49
48
  @comments = filter_visible_comments(@issue.comments)
50
- @support_user = support_user?
51
- @current_user_id = pms_current_user.present? ? PMS::UserResolver.user_id(pms_current_user) : nil
52
- @pipeline_enabled = PMS.configuration.pipeline_enabled
49
+ @current_user_id = pms_current_user.present? ? PlanMyStuff::UserResolver.user_id(pms_current_user) : nil
50
+ @current_user_login = PlanMyStuff.configuration.github_login_for[@current_user_id]
51
+ @pipeline_enabled = PlanMyStuff.configuration.pipeline_enabled
53
52
  @pipeline_item = load_pipeline_item(@issue.number) if @pipeline_enabled
54
53
  end
55
54
 
56
55
  # GET /issues/:id/edit
57
56
  def edit
58
- @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo])
59
- @support_user = support_user?
57
+ @issue = PlanMyStuff::Issue.find(params[:id].to_i, repo: params[:repo])
60
58
  end
61
59
 
62
60
  # PATCH/PUT /issues/:id
63
61
  def update
64
- @issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo])
62
+ @issue = PlanMyStuff::Issue.find(params[:id].to_i, repo: params[:repo])
65
63
 
66
64
  @issue.update!(
67
65
  title: issue_params[:title],
@@ -71,14 +69,14 @@ module PlanMyStuff
71
69
 
72
70
  flash[:success] = 'Issue was successfully updated.'
73
71
  redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
74
- rescue PMS::StaleObjectError
75
- @support_user = support_user?
72
+ rescue PlanMyStuff::StaleObjectError => e
73
+ pms_handle_rescue(e)
76
74
  flash.now[:error] = 'Issue was modified by someone else. Please review the latest changes and try again.'
77
- render(:edit, status: PMS.unprocessable_status)
78
- rescue PMS::ValidationError => e
79
- @support_user = support_user?
75
+ render(:edit, status: PlanMyStuff.unprocessable_status)
76
+ rescue PlanMyStuff::ValidationError => e
77
+ pms_handle_rescue(e)
80
78
  flash.now[:error] = e.message
81
- render(:edit, status: PMS.unprocessable_status)
79
+ render(:edit, status: PlanMyStuff.unprocessable_status)
82
80
  end
83
81
 
84
82
  private
@@ -99,18 +97,16 @@ module PlanMyStuff
99
97
  comments.select { |comment| comment.visible_to?(user) }
100
98
  end
101
99
 
102
- # Looks up the pipeline ProjectItem for this issue, if any.
103
- # Returns nil when the item cannot be loaded for any reason --
104
- # issue show should never break because the pipeline lookup
105
- # failed.
100
+ # Looks up the pipeline ProjectItem for this issue, if any. Returns nil when the item cannot be loaded for any
101
+ # reason -- issue show should never break because the pipeline lookup failed.
106
102
  #
107
103
  # @param issue_number [Integer]
108
104
  #
109
105
  # @return [PlanMyStuff::ProjectItem, nil]
110
106
  #
111
107
  def load_pipeline_item(issue_number)
112
- PMS::Pipeline::IssueLinker.find_project_item(issue_number)
113
- rescue ArgumentError, PMS::Error => e
108
+ PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue_number)
109
+ rescue ArgumentError, PlanMyStuff::Error => e
114
110
  Rails.logger.warn("[PlanMyStuff] Failed to load pipeline item for issue ##{issue_number}: #{e.message}")
115
111
  nil
116
112
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PlanMyStuff
4
- class LabelsController < ApplicationController
4
+ class LabelsController < PlanMyStuff::ApplicationController
5
5
  # POST /issues/:issue_id/labels
6
6
  def create
7
7
  labels = parse_labels(params[:label_name])
@@ -11,20 +11,36 @@ module PlanMyStuff
11
11
  return
12
12
  end
13
13
 
14
- issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
15
- PMS::Label.add(issue: issue, labels: labels)
14
+ issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
15
+
16
+ missing = labels.reject { |label| PlanMyStuff::Label.exists?(repo: issue.repo, name: label) }
17
+ if missing.any?
18
+ flash[:error] = "Label#{'s' if missing.size > 1} not found in repo: #{missing.join(', ')}"
19
+ redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
20
+ return
21
+ end
22
+
23
+ PlanMyStuff::Label.add!(issue: issue, labels: labels)
16
24
 
17
25
  flash[:success] = 'Label was successfully added.'
18
26
  redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
27
+ rescue PlanMyStuff::Error, Octokit::Error => e
28
+ pms_handle_rescue(e)
29
+ flash[:error] = e.message
30
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
19
31
  end
20
32
 
21
33
  # DELETE /issues/:issue_id/labels/:id
22
34
  def destroy
23
- issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
24
- PMS::Label.remove(issue: issue, labels: [params[:id]])
35
+ issue = PlanMyStuff::Issue.find(params[:issue_id].to_i, repo: params[:repo])
36
+ PlanMyStuff::Label.remove!(issue: issue, labels: [params[:id]])
25
37
 
26
38
  flash[:success] = 'Label was successfully removed.'
27
39
  redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
40
+ rescue PlanMyStuff::Error, Octokit::Error => e
41
+ pms_handle_rescue(e)
42
+ flash[:error] = e.message
43
+ redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
28
44
  end
29
45
  end
30
46
  end