plan_my_stuff 0.2.0 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +569 -38
- data/app/controllers/plan_my_stuff/comments_controller.rb +5 -1
- data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +102 -0
- data/app/controllers/plan_my_stuff/issues/closures_controller.rb +37 -0
- data/app/controllers/plan_my_stuff/issues/links_controller.rb +127 -0
- data/app/controllers/plan_my_stuff/issues/takes_controller.rb +88 -0
- data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +48 -0
- data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +47 -0
- data/app/controllers/plan_my_stuff/issues_controller.rb +22 -55
- data/app/controllers/plan_my_stuff/labels_controller.rb +4 -4
- data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +75 -0
- data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +40 -0
- data/app/controllers/plan_my_stuff/project_items_controller.rb +0 -75
- data/app/controllers/plan_my_stuff/projects_controller.rb +65 -1
- data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +54 -0
- data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +39 -0
- data/app/controllers/plan_my_stuff/testing_projects_controller.rb +93 -0
- data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +148 -0
- data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +284 -0
- data/app/jobs/plan_my_stuff/application_job.rb +9 -0
- data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +81 -0
- data/app/views/plan_my_stuff/comments/partials/_form.html.erb +7 -0
- data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +87 -0
- data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +2 -2
- data/app/views/plan_my_stuff/issues/partials/_links.html.erb +70 -0
- data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +2 -2
- data/app/views/plan_my_stuff/issues/show.html.erb +46 -3
- data/app/views/plan_my_stuff/projects/edit.html.erb +7 -0
- data/app/views/plan_my_stuff/projects/index.html.erb +17 -1
- data/app/views/plan_my_stuff/projects/new.html.erb +7 -0
- data/app/views/plan_my_stuff/projects/partials/_form.html.erb +29 -0
- data/app/views/plan_my_stuff/projects/show.html.erb +11 -4
- data/app/views/plan_my_stuff/testing_project_items/new.html.erb +12 -0
- data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +22 -0
- data/app/views/plan_my_stuff/testing_projects/edit.html.erb +7 -0
- data/app/views/plan_my_stuff/testing_projects/new.html.erb +7 -0
- data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +39 -0
- data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +51 -0
- data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +35 -0
- data/app/views/plan_my_stuff/testing_projects/show.html.erb +65 -0
- data/config/routes.rb +38 -15
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +138 -5
- data/lib/plan_my_stuff/application_record.rb +144 -0
- data/lib/plan_my_stuff/approval.rb +80 -0
- data/lib/plan_my_stuff/archive/sweep.rb +85 -0
- data/lib/plan_my_stuff/archive.rb +14 -0
- data/lib/plan_my_stuff/aws_sns_simulator.rb +110 -0
- data/lib/plan_my_stuff/base_metadata.rb +0 -11
- data/lib/plan_my_stuff/base_project.rb +661 -0
- data/lib/plan_my_stuff/base_project_item.rb +562 -0
- data/lib/plan_my_stuff/base_project_metadata.rb +16 -0
- data/lib/plan_my_stuff/cache.rb +197 -0
- data/lib/plan_my_stuff/client.rb +7 -0
- data/lib/plan_my_stuff/comment.rb +174 -54
- data/lib/plan_my_stuff/configuration.rb +254 -8
- data/lib/plan_my_stuff/custom_fields.rb +31 -17
- data/lib/plan_my_stuff/engine.rb +0 -4
- data/lib/plan_my_stuff/errors.rb +49 -0
- data/lib/plan_my_stuff/graphql/queries.rb +392 -0
- data/lib/plan_my_stuff/issue.rb +1477 -174
- data/lib/plan_my_stuff/issue_metadata.rb +122 -0
- data/lib/plan_my_stuff/label.rb +82 -11
- data/lib/plan_my_stuff/link.rb +144 -0
- data/lib/plan_my_stuff/notifications.rb +142 -0
- data/lib/plan_my_stuff/pipeline/issue_linker.rb +62 -0
- data/lib/plan_my_stuff/pipeline/status.rb +44 -0
- data/lib/plan_my_stuff/pipeline.rb +293 -0
- data/lib/plan_my_stuff/project.rb +62 -468
- data/lib/plan_my_stuff/project_item.rb +3 -417
- data/lib/plan_my_stuff/project_item_metadata.rb +55 -0
- data/lib/plan_my_stuff/project_metadata.rb +47 -0
- data/lib/plan_my_stuff/reminders/closer.rb +70 -0
- data/lib/plan_my_stuff/reminders/fire.rb +129 -0
- data/lib/plan_my_stuff/reminders/sweep.rb +54 -0
- data/lib/plan_my_stuff/reminders.rb +16 -0
- data/lib/plan_my_stuff/test_helpers.rb +260 -15
- data/lib/plan_my_stuff/testing_project.rb +291 -0
- data/lib/plan_my_stuff/testing_project_item.rb +184 -0
- data/lib/plan_my_stuff/testing_project_metadata.rb +94 -0
- data/lib/plan_my_stuff/user_resolver.rb +8 -3
- data/lib/plan_my_stuff/version.rb +1 -1
- data/lib/plan_my_stuff/webhook_replayer.rb +280 -0
- data/lib/plan_my_stuff.rb +16 -0
- data/lib/tasks/plan_my_stuff.rake +163 -0
- metadata +54 -2
|
@@ -11,10 +11,14 @@ module PlanMyStuff
|
|
|
11
11
|
body: comment_params[:body],
|
|
12
12
|
user: pms_current_user,
|
|
13
13
|
visibility: comment_params[:visibility]&.to_sym || :public,
|
|
14
|
+
waiting_on_reply: comment_params[:waiting_on_reply] == '1',
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
flash[:success] = 'Comment was successfully created.'
|
|
17
18
|
redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
|
|
19
|
+
rescue PMS::LockedIssueError
|
|
20
|
+
flash[:error] = 'This issue is locked; no new comments can be posted.'
|
|
21
|
+
redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
# GET /issues/:issue_id/comments/:id/edit
|
|
@@ -58,7 +62,7 @@ module PlanMyStuff
|
|
|
58
62
|
|
|
59
63
|
# @return [ActionController::Parameters]
|
|
60
64
|
def comment_params
|
|
61
|
-
params.require(:comment).permit(:body, :visibility)
|
|
65
|
+
params.require(:comment).permit(:body, :visibility, :waiting_on_reply)
|
|
62
66
|
end
|
|
63
67
|
|
|
64
68
|
# Loads the issue and comment from params.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
module Issues
|
|
5
|
+
# CRUD for manager approvals on an issue. Backs the approvals panel
|
|
6
|
+
# on the issue show view.
|
|
7
|
+
#
|
|
8
|
+
# POST /issues/:issue_id/approvals -> create (adds approver(s))
|
|
9
|
+
# PATCH /issues/:issue_id/approvals/:id -> update (approves or revokes)
|
|
10
|
+
# DELETE /issues/:issue_id/approvals/:id -> destroy (removes an approver)
|
|
11
|
+
#
|
|
12
|
+
class ApprovalsController < ApplicationController
|
|
13
|
+
# POST /issues/:issue_id/approvals
|
|
14
|
+
def create
|
|
15
|
+
unless support_user?
|
|
16
|
+
redirect_to_unauthorized(show_path)
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
user_ids = parse_viewer_ids(approval_params[:user_ids])
|
|
21
|
+
if user_ids.blank?
|
|
22
|
+
flash[:error] = 'No valid user IDs provided.'
|
|
23
|
+
redirect_to(show_path)
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
|
|
28
|
+
issue.request_approvals!(user_ids: user_ids, user: pms_current_user)
|
|
29
|
+
|
|
30
|
+
flash[:success] = 'Approvers were successfully added.'
|
|
31
|
+
redirect_to(show_path)
|
|
32
|
+
rescue PMS::AuthorizationError, PMS::ValidationError => e
|
|
33
|
+
flash[:error] = e.message
|
|
34
|
+
redirect_to(show_path)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# PATCH /issues/:issue_id/approvals/:id
|
|
38
|
+
def update
|
|
39
|
+
status = approval_params[:status].to_s
|
|
40
|
+
target_id = params[:id].to_i
|
|
41
|
+
|
|
42
|
+
if %w[approved pending].exclude?(status)
|
|
43
|
+
head(:bad_request)
|
|
44
|
+
return
|
|
45
|
+
end
|
|
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
|
|
49
|
+
|
|
50
|
+
if status == 'approved'
|
|
51
|
+
unless caller_id == target_id
|
|
52
|
+
redirect_to_unauthorized(show_path)
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
issue.approve!(user: pms_current_user)
|
|
56
|
+
flash[:success] = 'Approval recorded.'
|
|
57
|
+
else
|
|
58
|
+
if caller_id != target_id && !support_user?
|
|
59
|
+
redirect_to_unauthorized(show_path)
|
|
60
|
+
return
|
|
61
|
+
end
|
|
62
|
+
issue.revoke_approval!(user: pms_current_user, target_user_id: target_id)
|
|
63
|
+
flash[:success] = 'Approval revoked.'
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
redirect_to(show_path)
|
|
67
|
+
rescue PMS::AuthorizationError, PMS::ValidationError => e
|
|
68
|
+
flash[:error] = e.message
|
|
69
|
+
redirect_to(show_path)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# DELETE /issues/:issue_id/approvals/:id
|
|
73
|
+
def destroy
|
|
74
|
+
unless support_user?
|
|
75
|
+
redirect_to_unauthorized(show_path)
|
|
76
|
+
return
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
|
|
80
|
+
issue.remove_approvers!(user_ids: [params[:id].to_i], user: pms_current_user)
|
|
81
|
+
|
|
82
|
+
flash[:success] = 'Approver was successfully removed.'
|
|
83
|
+
redirect_to(show_path)
|
|
84
|
+
rescue PMS::AuthorizationError, PMS::ValidationError => e
|
|
85
|
+
flash[:error] = e.message
|
|
86
|
+
redirect_to(show_path)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
# @return [ActionController::Parameters]
|
|
92
|
+
def approval_params
|
|
93
|
+
params.fetch(:approval, {}).permit(:status, :user_ids)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @return [String]
|
|
97
|
+
def show_path
|
|
98
|
+
plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo])
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
module Issues
|
|
5
|
+
# Handles closing and reopening issues via CRUD-style routes.
|
|
6
|
+
# Backs the Close/Reopen buttons on the issue show view (T-044).
|
|
7
|
+
#
|
|
8
|
+
# POST /issues/:issue_id/closure -> create (closes)
|
|
9
|
+
# DELETE /issues/:issue_id/closure -> destroy (reopens)
|
|
10
|
+
#
|
|
11
|
+
class ClosuresController < ApplicationController
|
|
12
|
+
# POST /issues/:issue_id/closure
|
|
13
|
+
def create
|
|
14
|
+
issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
|
|
15
|
+
issue.update!(state: :closed)
|
|
16
|
+
|
|
17
|
+
flash[:success] = 'Issue was successfully closed.'
|
|
18
|
+
redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
|
|
19
|
+
rescue PMS::Error, ArgumentError => e
|
|
20
|
+
flash[:error] = e.message
|
|
21
|
+
redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# DELETE /issues/:issue_id/closure
|
|
25
|
+
def destroy
|
|
26
|
+
issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
|
|
27
|
+
issue.update!(state: :open)
|
|
28
|
+
|
|
29
|
+
flash[:success] = 'Issue was successfully reopened.'
|
|
30
|
+
redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
|
|
31
|
+
rescue PMS::Error, ArgumentError => e
|
|
32
|
+
flash[:error] = e.message
|
|
33
|
+
redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
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.
|
|
8
|
+
#
|
|
9
|
+
# POST /issues/:issue_id/links -> create (adds a link)
|
|
10
|
+
# DELETE /issues/:issue_id/links/:id -> destroy (removes a link)
|
|
11
|
+
#
|
|
12
|
+
class LinksController < ApplicationController
|
|
13
|
+
NATIVE_DISPATCH = {
|
|
14
|
+
'related' => { add: :add_related!, remove: :remove_related! },
|
|
15
|
+
'blocked_by' => { add: :add_blocker!, remove: :remove_blocker! },
|
|
16
|
+
'sub_ticket' => { add: :add_sub_issue!, remove: :remove_sub_issue! },
|
|
17
|
+
'parent' => { add: :set_parent!, remove: :remove_parent! },
|
|
18
|
+
'duplicate_of' => { add: :mark_duplicate! },
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# POST /issues/:issue_id/links
|
|
22
|
+
def create
|
|
23
|
+
type = link_params[:type].to_s
|
|
24
|
+
unless dispatch_allowed?(type, :add)
|
|
25
|
+
redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
|
|
26
|
+
return
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
|
|
30
|
+
link = add_link(issue, type)
|
|
31
|
+
|
|
32
|
+
flash[:success] = "Linked #{link}"
|
|
33
|
+
redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
|
|
34
|
+
rescue PMS::ValidationError, ActiveModel::ValidationError, ArgumentError => e
|
|
35
|
+
flash[:error] = e.message
|
|
36
|
+
redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# DELETE /issues/:issue_id/links/:id
|
|
40
|
+
def destroy
|
|
41
|
+
type, repo, number = parse_composite_id(params[:id])
|
|
42
|
+
unless dispatch_allowed?(type, :remove)
|
|
43
|
+
redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
|
|
44
|
+
return
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
|
|
48
|
+
remove_link(issue, type, repo: repo, number: number)
|
|
49
|
+
|
|
50
|
+
flash[:success] = "Unlinked #{repo}##{number}"
|
|
51
|
+
redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
|
|
52
|
+
rescue PMS::ValidationError, ActiveModel::ValidationError, ArgumentError => e
|
|
53
|
+
flash[:error] = e.message
|
|
54
|
+
redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
# @return [ActionController::Parameters]
|
|
60
|
+
def link_params
|
|
61
|
+
params.require(:link).permit(:type, :issue_number, :repo)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Only +:related+ may be mutated by non-support users. +destroy+
|
|
65
|
+
# is not defined for +:duplicate_of+ (reopen via GitHub).
|
|
66
|
+
#
|
|
67
|
+
# @return [Boolean]
|
|
68
|
+
#
|
|
69
|
+
def dispatch_allowed?(type, action)
|
|
70
|
+
entry = NATIVE_DISPATCH[type]
|
|
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
|
+
|
|
75
|
+
support_user?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @return [PMS::Link]
|
|
79
|
+
def add_link(issue, type)
|
|
80
|
+
method = NATIVE_DISPATCH.dig(type, :add)
|
|
81
|
+
target = {
|
|
82
|
+
type: type,
|
|
83
|
+
issue_number: link_params[:issue_number].to_i,
|
|
84
|
+
repo: link_params[:repo].presence || issue.repo.to_s,
|
|
85
|
+
}
|
|
86
|
+
case type
|
|
87
|
+
when 'related', 'duplicate_of'
|
|
88
|
+
issue.public_send(method, target, user: pms_current_user)
|
|
89
|
+
else
|
|
90
|
+
issue.public_send(method, target)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @return [void]
|
|
95
|
+
def remove_link(issue, type, repo:, number:)
|
|
96
|
+
method = NATIVE_DISPATCH.dig(type, :remove)
|
|
97
|
+
target = { type: type, issue_number: number, repo: repo }
|
|
98
|
+
case type
|
|
99
|
+
when 'parent'
|
|
100
|
+
issue.public_send(method)
|
|
101
|
+
when 'related'
|
|
102
|
+
issue.public_send(method, target, user: pms_current_user)
|
|
103
|
+
else
|
|
104
|
+
issue.public_send(method, target)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
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.
|
|
113
|
+
#
|
|
114
|
+
# @param id [String]
|
|
115
|
+
#
|
|
116
|
+
# @return [Array(String, String, Integer)] type, repo, number
|
|
117
|
+
#
|
|
118
|
+
def parse_composite_id(id)
|
|
119
|
+
decoded = CGI.unescape(id.to_s)
|
|
120
|
+
type, repo, number = decoded.split(':', 3)
|
|
121
|
+
raise(ArgumentError, "Invalid link id: #{id.inspect}") if number.blank?
|
|
122
|
+
|
|
123
|
+
[type, repo, number.to_i]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
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.
|
|
9
|
+
#
|
|
10
|
+
class TakesController < ApplicationController
|
|
11
|
+
before_action :require_support_user!
|
|
12
|
+
|
|
13
|
+
# POST /issues/:issue_id/take
|
|
14
|
+
def create
|
|
15
|
+
issue_number = params[:issue_id].to_i
|
|
16
|
+
repo = params[:repo]
|
|
17
|
+
|
|
18
|
+
project_item = PlanMyStuff::Pipeline::IssueLinker.find_project_item(issue_number)
|
|
19
|
+
project_item ||= add_to_pipeline(issue_number, repo)
|
|
20
|
+
|
|
21
|
+
PlanMyStuff::Pipeline.take!(project_item)
|
|
22
|
+
assign_current_user(project_item)
|
|
23
|
+
flash[:success] ||= "Issue ##{issue_number} taken."
|
|
24
|
+
|
|
25
|
+
redirect_to(plan_my_stuff.issue_path(issue_number, repo: repo))
|
|
26
|
+
rescue ArgumentError, PlanMyStuff::Error => e
|
|
27
|
+
flash[:error] = e.message
|
|
28
|
+
redirect_to(plan_my_stuff.issue_path(issue_number, repo: repo))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Redirects non-support users back to the issue page. Mirrors
|
|
34
|
+
# +Issues::ViewersController+'s authorization check.
|
|
35
|
+
#
|
|
36
|
+
# @return [void]
|
|
37
|
+
#
|
|
38
|
+
def require_support_user!
|
|
39
|
+
return if support_user?
|
|
40
|
+
|
|
41
|
+
redirect_to_unauthorized(
|
|
42
|
+
plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]),
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
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.
|
|
49
|
+
#
|
|
50
|
+
# @param issue_number [Integer]
|
|
51
|
+
# @param repo [String, nil]
|
|
52
|
+
#
|
|
53
|
+
# @return [PlanMyStuff::ProjectItem]
|
|
54
|
+
#
|
|
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
|
|
58
|
+
PlanMyStuff::ProjectItem.create!(issue, project_number: pipeline_number)
|
|
59
|
+
end
|
|
60
|
+
|
|
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.
|
|
66
|
+
#
|
|
67
|
+
# @param project_item [PlanMyStuff::ProjectItem]
|
|
68
|
+
#
|
|
69
|
+
# @return [void]
|
|
70
|
+
#
|
|
71
|
+
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]
|
|
75
|
+
|
|
76
|
+
if login.blank?
|
|
77
|
+
Rails.logger.info("[PMS] No github_login_for mapping for user #{user_id}; skipping Take assignment")
|
|
78
|
+
return
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
project_item.assign!(login)
|
|
82
|
+
rescue PlanMyStuff::Error => e
|
|
83
|
+
Rails.logger.warn("[PMS] Take assignment failed: #{e.message}")
|
|
84
|
+
flash[:warning] = "Issue taken but assignment failed: #{e.message}"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
module Issues
|
|
5
|
+
# Handles adding and removing viewers from the visibility allowlist via CRUD-style routes.
|
|
6
|
+
# Backs the viewer management UI on the issue edit view (T-045).
|
|
7
|
+
#
|
|
8
|
+
# POST /issues/:issue_id/viewers -> create (adds viewer(s))
|
|
9
|
+
# DELETE /issues/:issue_id/viewers/:id -> destroy (removes a viewer)
|
|
10
|
+
#
|
|
11
|
+
class ViewersController < ApplicationController
|
|
12
|
+
# POST /issues/:issue_id/viewers
|
|
13
|
+
def create
|
|
14
|
+
unless support_user?
|
|
15
|
+
redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
viewer_ids = parse_viewer_ids(params[:viewer_ids])
|
|
20
|
+
if viewer_ids.blank?
|
|
21
|
+
flash[:error] = 'No valid viewer IDs provided.'
|
|
22
|
+
redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id], repo: params[:repo]))
|
|
23
|
+
return
|
|
24
|
+
end
|
|
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)
|
|
28
|
+
|
|
29
|
+
flash[:success] = 'Viewers were successfully added.'
|
|
30
|
+
redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id], repo: params[:repo]))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# DELETE /issues/:issue_id/viewers/:id
|
|
34
|
+
def destroy
|
|
35
|
+
unless support_user?
|
|
36
|
+
redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
|
|
37
|
+
return
|
|
38
|
+
end
|
|
39
|
+
|
|
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)
|
|
42
|
+
|
|
43
|
+
flash[:success] = 'Viewer was successfully removed.'
|
|
44
|
+
redirect_to(plan_my_stuff.edit_issue_path(params[:issue_id], repo: params[:repo]))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
module Issues
|
|
5
|
+
# Toggles the waiting-on-user state on an issue via CRUD-style routes.
|
|
6
|
+
# Backs the "Mark waiting" / "Mark replied" button on the issue show view.
|
|
7
|
+
#
|
|
8
|
+
# POST /issues/:issue_id/waiting -> create (enters waiting-on-user)
|
|
9
|
+
# DELETE /issues/:issue_id/waiting -> destroy (clears waiting-on-user)
|
|
10
|
+
#
|
|
11
|
+
class WaitingsController < ApplicationController
|
|
12
|
+
# POST /issues/:issue_id/waiting
|
|
13
|
+
def create
|
|
14
|
+
unless support_user?
|
|
15
|
+
redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
|
|
20
|
+
issue.enter_waiting_on_user!(user: pms_current_user)
|
|
21
|
+
|
|
22
|
+
flash[:success] = 'Issue marked as waiting on user reply.'
|
|
23
|
+
redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
|
|
24
|
+
rescue PMS::Error, ArgumentError => e
|
|
25
|
+
flash[:error] = e.message
|
|
26
|
+
redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# DELETE /issues/:issue_id/waiting
|
|
30
|
+
def destroy
|
|
31
|
+
unless support_user?
|
|
32
|
+
redirect_to_unauthorized(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
|
|
37
|
+
issue.clear_waiting_on_user!
|
|
38
|
+
|
|
39
|
+
flash[:success] = 'Waiting-on-user state cleared.'
|
|
40
|
+
redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
|
|
41
|
+
rescue PMS::Error, ArgumentError => e
|
|
42
|
+
flash[:error] = e.message
|
|
43
|
+
redirect_to(plan_my_stuff.issue_path(params[:issue_id], repo: params[:repo]))
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -49,6 +49,8 @@ module PlanMyStuff
|
|
|
49
49
|
@comments = filter_visible_comments(@issue.comments)
|
|
50
50
|
@support_user = support_user?
|
|
51
51
|
@current_user_id = pms_current_user.present? ? PMS::UserResolver.user_id(pms_current_user) : nil
|
|
52
|
+
@pipeline_enabled = PMS.configuration.pipeline_enabled
|
|
53
|
+
@pipeline_item = load_pipeline_item(@issue.number) if @pipeline_enabled
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
# GET /issues/:id/edit
|
|
@@ -73,58 +75,10 @@ module PlanMyStuff
|
|
|
73
75
|
@support_user = support_user?
|
|
74
76
|
flash.now[:error] = 'Issue was modified by someone else. Please review the latest changes and try again.'
|
|
75
77
|
render(:edit, status: PMS.unprocessable_status)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
@issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo])
|
|
81
|
-
@issue.update!(state: :closed)
|
|
82
|
-
|
|
83
|
-
flash[:success] = 'Issue was successfully closed.'
|
|
84
|
-
redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# PATCH /issues/:id/reopen
|
|
88
|
-
def reopen
|
|
89
|
-
@issue = PMS::Issue.find(params[:id].to_i, repo: params[:repo])
|
|
90
|
-
@issue.update!(state: :open)
|
|
91
|
-
|
|
92
|
-
flash[:success] = 'Issue was successfully reopened.'
|
|
93
|
-
redirect_to(plan_my_stuff.issue_path(@issue.number, repo: @issue.repo.full_name))
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# POST /issues/:id/add_viewers
|
|
97
|
-
def add_viewers
|
|
98
|
-
unless support_user?
|
|
99
|
-
redirect_to_unauthorized(plan_my_stuff.issue_path(params[:id], repo: params[:repo]))
|
|
100
|
-
return
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
viewer_ids = parse_viewer_ids(params[:viewer_ids])
|
|
104
|
-
if viewer_ids.blank?
|
|
105
|
-
flash[:error] = 'No valid viewer IDs provided.'
|
|
106
|
-
redirect_to(plan_my_stuff.edit_issue_path(params[:id], repo: params[:repo]))
|
|
107
|
-
return
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
PMS::Issue.add_viewers(number: params[:id].to_i, user_ids: viewer_ids, repo: params[:repo])
|
|
111
|
-
|
|
112
|
-
flash[:success] = 'Viewers were successfully added.'
|
|
113
|
-
redirect_to(plan_my_stuff.edit_issue_path(params[:id], repo: params[:repo]))
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# DELETE /issues/:id/remove_viewer
|
|
117
|
-
def remove_viewer
|
|
118
|
-
unless support_user?
|
|
119
|
-
redirect_to_unauthorized(plan_my_stuff.issue_path(params[:id], repo: params[:repo]))
|
|
120
|
-
return
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
viewer_id = params[:viewer_id].to_i
|
|
124
|
-
PMS::Issue.remove_viewers(number: params[:id].to_i, user_ids: [viewer_id], repo: params[:repo])
|
|
125
|
-
|
|
126
|
-
flash[:success] = 'Viewer was successfully removed.'
|
|
127
|
-
redirect_to(plan_my_stuff.edit_issue_path(params[:id], repo: params[:repo]))
|
|
78
|
+
rescue PMS::ValidationError => e
|
|
79
|
+
@support_user = support_user?
|
|
80
|
+
flash.now[:error] = e.message
|
|
81
|
+
render(:edit, status: PMS.unprocessable_status)
|
|
128
82
|
end
|
|
129
83
|
|
|
130
84
|
private
|
|
@@ -135,7 +89,6 @@ module PlanMyStuff
|
|
|
135
89
|
end
|
|
136
90
|
|
|
137
91
|
# Filters comments to only those visible to the current user.
|
|
138
|
-
# Falls back to showing all comments if no current_user is available.
|
|
139
92
|
#
|
|
140
93
|
# @param comments [Array<PlanMyStuff::Comment>]
|
|
141
94
|
#
|
|
@@ -143,9 +96,23 @@ module PlanMyStuff
|
|
|
143
96
|
#
|
|
144
97
|
def filter_visible_comments(comments)
|
|
145
98
|
user = pms_current_user
|
|
146
|
-
return comments unless user
|
|
147
|
-
|
|
148
99
|
comments.select { |comment| comment.visible_to?(user) }
|
|
149
100
|
end
|
|
101
|
+
|
|
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.
|
|
106
|
+
#
|
|
107
|
+
# @param issue_number [Integer]
|
|
108
|
+
#
|
|
109
|
+
# @return [PlanMyStuff::ProjectItem, nil]
|
|
110
|
+
#
|
|
111
|
+
def load_pipeline_item(issue_number)
|
|
112
|
+
PMS::Pipeline::IssueLinker.find_project_item(issue_number)
|
|
113
|
+
rescue ArgumentError, PMS::Error => e
|
|
114
|
+
Rails.logger.warn("[PlanMyStuff] Failed to load pipeline item for issue ##{issue_number}: #{e.message}")
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
150
117
|
end
|
|
151
118
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module PlanMyStuff
|
|
4
4
|
class LabelsController < ApplicationController
|
|
5
5
|
# POST /issues/:issue_id/labels
|
|
6
|
-
def
|
|
6
|
+
def create
|
|
7
7
|
labels = parse_labels(params[:label_name])
|
|
8
8
|
if labels.blank?
|
|
9
9
|
flash[:error] = 'Label name is required.'
|
|
@@ -18,10 +18,10 @@ module PlanMyStuff
|
|
|
18
18
|
redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
# DELETE /issues/:issue_id/labels/:
|
|
22
|
-
def
|
|
21
|
+
# DELETE /issues/:issue_id/labels/:id
|
|
22
|
+
def destroy
|
|
23
23
|
issue = PMS::Issue.find(params[:issue_id].to_i, repo: params[:repo])
|
|
24
|
-
PMS::Label.remove(issue: issue, labels: [params[:
|
|
24
|
+
PMS::Label.remove(issue: issue, labels: [params[:id]])
|
|
25
25
|
|
|
26
26
|
flash[:success] = 'Label was successfully removed.'
|
|
27
27
|
redirect_to(plan_my_stuff.issue_path(issue.number, repo: issue.repo.full_name))
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
module ProjectItems
|
|
5
|
+
# Handles assigning and unassigning project item assignees via CRUD-style routes.
|
|
6
|
+
# Backs the assign/unassign forms on the project board view (T-047).
|
|
7
|
+
#
|
|
8
|
+
# PATCH /projects/:project_id/items/:item_id/assignment -> update (assign)
|
|
9
|
+
# DELETE /projects/:project_id/items/:item_id/assignment -> destroy (unassign)
|
|
10
|
+
#
|
|
11
|
+
class AssignmentsController < ApplicationController
|
|
12
|
+
# PATCH /projects/:project_id/items/:item_id/assignment
|
|
13
|
+
def update
|
|
14
|
+
item = find_project_item
|
|
15
|
+
assignees = parse_assignees(params[:assignee])
|
|
16
|
+
|
|
17
|
+
item.assign!(assignees)
|
|
18
|
+
|
|
19
|
+
flash[:success] = "Item assigned to #{assignees.join(', ')}."
|
|
20
|
+
redirect_to(plan_my_stuff.project_path(params[:project_id]))
|
|
21
|
+
rescue ArgumentError, PMS::Error => e
|
|
22
|
+
flash[:error] = e.message
|
|
23
|
+
redirect_to(plan_my_stuff.project_path(params[:project_id]))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# DELETE /projects/:project_id/items/:item_id/assignment
|
|
27
|
+
def destroy
|
|
28
|
+
if params[:username].blank?
|
|
29
|
+
flash[:error] = 'Username is required to unassign.'
|
|
30
|
+
redirect_to(plan_my_stuff.project_path(params[:project_id]))
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
item = find_project_item
|
|
35
|
+
current_assignees = item.field_values['Assignees'] || []
|
|
36
|
+
remaining = current_assignees - [params[:username]]
|
|
37
|
+
|
|
38
|
+
item.assign!(remaining)
|
|
39
|
+
|
|
40
|
+
flash[:success] = "#{params[:username]} unassigned."
|
|
41
|
+
redirect_to(plan_my_stuff.project_path(params[:project_id]))
|
|
42
|
+
rescue ArgumentError, PMS::Error => e
|
|
43
|
+
flash[:error] = e.message
|
|
44
|
+
redirect_to(plan_my_stuff.project_path(params[:project_id]))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Finds the project item by item_id within the given project.
|
|
50
|
+
#
|
|
51
|
+
# @return [PlanMyStuff::ProjectItem]
|
|
52
|
+
#
|
|
53
|
+
def find_project_item
|
|
54
|
+
project = PMS::Project.find(params[:project_id].to_i)
|
|
55
|
+
item = project.items.find { |i| i.id == params[:item_id] }
|
|
56
|
+
|
|
57
|
+
raise(PMS::APIError, "Item not found: #{params[:item_id]}") unless item
|
|
58
|
+
|
|
59
|
+
item
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Splits a comma-separated assignees string into an array.
|
|
63
|
+
#
|
|
64
|
+
# @param assignees_string [String, nil]
|
|
65
|
+
#
|
|
66
|
+
# @return [Array<String>]
|
|
67
|
+
#
|
|
68
|
+
def parse_assignees(assignees_string)
|
|
69
|
+
return [] if assignees_string.blank?
|
|
70
|
+
|
|
71
|
+
assignees_string.split(',').filter_map { |a| a.strip.presence }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|