plan_my_stuff 0.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -1
- data/README.md +100 -103
- data/app/controllers/plan_my_stuff/application_controller.rb +22 -3
- data/app/controllers/plan_my_stuff/comments_controller.rb +14 -16
- data/app/controllers/plan_my_stuff/issues/approvals_controller.rb +23 -13
- data/app/controllers/plan_my_stuff/issues/closures_controller.rb +7 -5
- data/app/controllers/plan_my_stuff/issues/links_controller.rb +14 -18
- data/app/controllers/plan_my_stuff/issues/takes_controller.rb +99 -28
- data/app/controllers/plan_my_stuff/issues/viewers_controller.rb +13 -5
- data/app/controllers/plan_my_stuff/issues/waitings_controller.rb +7 -5
- data/app/controllers/plan_my_stuff/issues_controller.rb +24 -28
- data/app/controllers/plan_my_stuff/labels_controller.rb +21 -5
- data/app/controllers/plan_my_stuff/project_items/assignments_controller.rb +13 -6
- data/app/controllers/plan_my_stuff/project_items/statuses_controller.rb +5 -4
- data/app/controllers/plan_my_stuff/project_items_controller.rb +30 -5
- data/app/controllers/plan_my_stuff/projects_controller.rb +16 -16
- data/app/controllers/plan_my_stuff/testing_project_items/results_controller.rb +21 -11
- data/app/controllers/plan_my_stuff/testing_project_items_controller.rb +9 -4
- data/app/controllers/plan_my_stuff/testing_projects_controller.rb +30 -14
- data/app/controllers/plan_my_stuff/webhooks/aws_controller.rb +50 -17
- data/app/controllers/plan_my_stuff/webhooks/github_controller.rb +32 -49
- data/app/jobs/plan_my_stuff/application_job.rb +2 -3
- data/app/jobs/plan_my_stuff/reminders_sweep_job.rb +15 -22
- data/app/views/plan_my_stuff/comments/edit.html.erb +1 -3
- data/app/views/plan_my_stuff/comments/partials/_form.html.erb +1 -0
- data/app/views/plan_my_stuff/issues/edit.html.erb +2 -4
- data/app/views/plan_my_stuff/issues/index.html.erb +2 -2
- data/app/views/plan_my_stuff/issues/new.html.erb +2 -4
- data/app/views/plan_my_stuff/issues/partials/_approvals.html.erb +23 -2
- data/app/views/plan_my_stuff/issues/partials/_form.html.erb +1 -0
- data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +2 -1
- data/app/views/plan_my_stuff/issues/partials/_links.html.erb +50 -7
- data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +2 -1
- data/app/views/plan_my_stuff/issues/show.html.erb +5 -2
- data/app/views/plan_my_stuff/partials/_flash.html.erb +4 -0
- data/app/views/plan_my_stuff/projects/edit.html.erb +1 -3
- data/app/views/plan_my_stuff/projects/index.html.erb +1 -1
- data/app/views/plan_my_stuff/projects/new.html.erb +1 -3
- data/app/views/plan_my_stuff/projects/partials/_form.html.erb +1 -0
- data/app/views/plan_my_stuff/projects/show.html.erb +13 -3
- data/app/views/plan_my_stuff/testing_project_items/new.html.erb +1 -3
- data/app/views/plan_my_stuff/testing_project_items/results/new.html.erb +1 -3
- data/app/views/plan_my_stuff/testing_projects/edit.html.erb +1 -3
- data/app/views/plan_my_stuff/testing_projects/new.html.erb +1 -3
- data/app/views/plan_my_stuff/testing_projects/partials/_form.html.erb +4 -3
- data/app/views/plan_my_stuff/testing_projects/partials/_item.html.erb +1 -0
- data/app/views/plan_my_stuff/testing_projects/partials/items/_form.html.erb +1 -0
- data/app/views/plan_my_stuff/testing_projects/show.html.erb +2 -2
- data/config/routes.rb +2 -2
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +56 -3
- data/lib/plan_my_stuff/approval.rb +12 -4
- data/lib/plan_my_stuff/aws_sns_simulator.rb +12 -6
- data/lib/plan_my_stuff/base_metadata.rb +4 -15
- data/lib/plan_my_stuff/base_project.rb +68 -55
- data/lib/plan_my_stuff/base_project_item.rb +61 -57
- data/lib/plan_my_stuff/base_project_metadata.rb +1 -1
- data/lib/plan_my_stuff/client.rb +136 -48
- data/lib/plan_my_stuff/comment.rb +57 -57
- data/lib/plan_my_stuff/comment_metadata.rb +1 -1
- data/lib/plan_my_stuff/configuration.rb +95 -82
- data/lib/plan_my_stuff/errors.rb +10 -10
- data/lib/plan_my_stuff/graphql/queries.rb +1 -1
- data/lib/plan_my_stuff/issue.rb +501 -322
- data/lib/plan_my_stuff/issue_metadata.rb +10 -10
- data/lib/plan_my_stuff/label.rb +32 -16
- data/lib/plan_my_stuff/link.rb +15 -15
- data/lib/plan_my_stuff/markdown.rb +12 -6
- data/lib/plan_my_stuff/metadata_parser.rb +3 -1
- data/lib/plan_my_stuff/notifications.rb +1 -1
- data/lib/plan_my_stuff/pipeline/completed_sweep.rb +2 -2
- data/lib/plan_my_stuff/pipeline/issue_linker.rb +1 -1
- data/lib/plan_my_stuff/pipeline.rb +61 -83
- data/lib/plan_my_stuff/project.rb +4 -4
- data/lib/plan_my_stuff/project_item_metadata.rb +1 -1
- data/lib/plan_my_stuff/project_metadata.rb +1 -1
- data/lib/plan_my_stuff/reminders/closer.rb +1 -1
- data/lib/plan_my_stuff/reminders/fire.rb +3 -3
- data/lib/plan_my_stuff/reminders/sweep.rb +4 -4
- data/lib/plan_my_stuff/repo.rb +12 -6
- data/lib/plan_my_stuff/test_helpers.rb +11 -11
- data/lib/plan_my_stuff/testing_project.rb +12 -11
- data/lib/plan_my_stuff/testing_project_item.rb +11 -9
- data/lib/plan_my_stuff/testing_project_metadata.rb +2 -2
- data/lib/plan_my_stuff/version.rb +1 -1
- data/lib/plan_my_stuff/webhook_replayer.rb +14 -2
- data/lib/plan_my_stuff.rb +26 -2
- data/lib/tasks/plan_my_stuff.rake +33 -20
- metadata +3 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<% end %>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
|
-
<% if @support_user && @project.metadata.subject_urls.
|
|
10
|
+
<% if @support_user && @project.metadata.subject_urls.present? %>
|
|
11
11
|
<p>
|
|
12
12
|
<strong>Subject URLs:</strong>
|
|
13
13
|
<% @project.metadata.subject_urls.each do |url| %>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
<p><strong>Due:</strong> <%= @project.metadata.due_date %></p>
|
|
21
21
|
<% end %>
|
|
22
22
|
|
|
23
|
-
<% if @statuses.
|
|
23
|
+
<% if @statuses.present? %>
|
|
24
24
|
<table>
|
|
25
25
|
<thead>
|
|
26
26
|
<tr>
|
data/config/routes.rb
CHANGED
|
@@ -12,7 +12,7 @@ PlanMyStuff::Engine.routes.draw do
|
|
|
12
12
|
if config.pipeline_enabled
|
|
13
13
|
resource(
|
|
14
14
|
:take,
|
|
15
|
-
only:
|
|
15
|
+
only: %i[create destroy],
|
|
16
16
|
controller: config.controller_for(:'issues/takes'),
|
|
17
17
|
)
|
|
18
18
|
end
|
|
@@ -32,7 +32,7 @@ PlanMyStuff::Engine.routes.draw do
|
|
|
32
32
|
|
|
33
33
|
if mount_groups.fetch(:projects, true)
|
|
34
34
|
resources :projects, except: %i[destroy], controller: config.controller_for(:projects) do
|
|
35
|
-
resources :items, only: %i[create], controller: config.controller_for(:project_items) do
|
|
35
|
+
resources :items, only: %i[create destroy], controller: config.controller_for(:project_items) do
|
|
36
36
|
resource :status, only: %i[update], controller: config.controller_for(:'project_items/statuses')
|
|
37
37
|
resource :assignment, only: %i[update destroy], controller: config.controller_for(:'project_items/assignments')
|
|
38
38
|
end
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
PlanMyStuff.configure do |config|
|
|
4
4
|
# --------------------------------------------------------------------------
|
|
5
5
|
# Authentication (required)
|
|
6
6
|
# --------------------------------------------------------------------------
|
|
7
7
|
# GitHub PAT with `repo` and `project` scopes. All API calls use this token.
|
|
8
8
|
config.access_token = Rails.application.credentials.dig(:plan_my_stuff, :github_token)
|
|
9
9
|
|
|
10
|
+
# Classic PAT (requires `repo` scope) for the Issues Import API (golden-comet-preview).
|
|
11
|
+
# Fine-grained tokens are not supported by that endpoint.
|
|
12
|
+
# config.import_access_token = Rails.application.credentials.dig(:plan_my_stuff, :github_import_token)
|
|
13
|
+
|
|
10
14
|
# --------------------------------------------------------------------------
|
|
11
15
|
# Organization (required)
|
|
12
16
|
# --------------------------------------------------------------------------
|
|
@@ -93,8 +97,13 @@ PMS.configure do |config|
|
|
|
93
97
|
# --------------------------------------------------------------------------
|
|
94
98
|
# URL prefix
|
|
95
99
|
# --------------------------------------------------------------------------
|
|
96
|
-
# Prefix for building user-facing ticket URLs in your app.
|
|
97
|
-
#
|
|
100
|
+
# Prefix for building user-facing ticket URLs in your app. The issue
|
|
101
|
+
# number is appended to form `Issue#user_link`, which the gem also
|
|
102
|
+
# writes as the visible body of the GitHub issue.
|
|
103
|
+
# Recommended default:
|
|
104
|
+
# url_options = Rails.application.config.action_mailer.default_url_options
|
|
105
|
+
# config.issues_url_prefix =
|
|
106
|
+
# "#{url_options[:protocol] || 'http'}://#{url_options[:host]}/issues"
|
|
98
107
|
|
|
99
108
|
# --------------------------------------------------------------------------
|
|
100
109
|
# Request gateway
|
|
@@ -114,6 +123,18 @@ PMS.configure do |config|
|
|
|
114
123
|
# does not pass an explicit user: kwarg. Proc/lambda called at event time.
|
|
115
124
|
# config.current_user = -> { Current.user }
|
|
116
125
|
|
|
126
|
+
# --------------------------------------------------------------------------
|
|
127
|
+
# Controller rescue hook
|
|
128
|
+
# --------------------------------------------------------------------------
|
|
129
|
+
# Invoked from every gem controller rescue block, after the gem logs the
|
|
130
|
+
# error class/message and stack trace via Rails.logger.error and just
|
|
131
|
+
# before the user-facing flash + redirect/render. Forwards the rescued
|
|
132
|
+
# exception so consuming apps can fire monitoring even though the rescue
|
|
133
|
+
# swallows the error. Receives the rescued exception; return value is
|
|
134
|
+
# ignored. Defaults to nil (no-op).
|
|
135
|
+
#
|
|
136
|
+
# config.controller_rescue = ->(error) { MonitoringService.notice_error(error) }
|
|
137
|
+
|
|
117
138
|
# --------------------------------------------------------------------------
|
|
118
139
|
# Custom fields
|
|
119
140
|
# --------------------------------------------------------------------------
|
|
@@ -142,6 +163,22 @@ PMS.configure do |config|
|
|
|
142
163
|
# Testing-project-only fields (merged on top of shared, context wins on conflicts):
|
|
143
164
|
# config.testing_custom_fields = {}
|
|
144
165
|
|
|
166
|
+
# --------------------------------------------------------------------------
|
|
167
|
+
# Issue types (org-side renames)
|
|
168
|
+
# --------------------------------------------------------------------------
|
|
169
|
+
# The gem speaks in canonical type names: 'Bug', 'Feature',
|
|
170
|
+
# 'IT Issue / Hardware', 'Other', 'Performance', 'Question', 'Task'.
|
|
171
|
+
# Symbol shortcuts (`:bug`, `:feature`, `:it_issue`, `:other`, `:performance`,
|
|
172
|
+
# `:question`, `:task`) resolve to those canonical names automatically.
|
|
173
|
+
# If your GitHub org uses different names for any of them, map the
|
|
174
|
+
# canonical name to your org's name here and PMS will translate on
|
|
175
|
+
# write. Missing keys pass through unchanged.
|
|
176
|
+
#
|
|
177
|
+
# config.issue_types = {
|
|
178
|
+
# 'Bug' => 'User Bug',
|
|
179
|
+
# 'Feature' => 'Enhancement',
|
|
180
|
+
# }
|
|
181
|
+
|
|
145
182
|
# --------------------------------------------------------------------------
|
|
146
183
|
# Release pipeline
|
|
147
184
|
# --------------------------------------------------------------------------
|
|
@@ -157,6 +194,22 @@ PMS.configure do |config|
|
|
|
157
194
|
# 'Completed' => 'Done',
|
|
158
195
|
# }
|
|
159
196
|
|
|
197
|
+
# Display name for the Testing single-select field on the pipeline project.
|
|
198
|
+
# config.pipeline_testing_field_name = 'Testing'
|
|
199
|
+
|
|
200
|
+
# Display labels for the canonical Testing field option keys.
|
|
201
|
+
# config.pipeline_testing_values = {
|
|
202
|
+
# active: 'Testing',
|
|
203
|
+
# inactive: 'Not testing',
|
|
204
|
+
# }
|
|
205
|
+
|
|
206
|
+
# Whether the pipeline sweep removes aged-out Completed items.
|
|
207
|
+
# config.pipeline_completion_purge_enabled = true
|
|
208
|
+
|
|
209
|
+
# Hours after a project item's last update at which the sweep removes it
|
|
210
|
+
# from the pipeline if its status is Completed.
|
|
211
|
+
# config.pipeline_completion_ttl_hours = 24
|
|
212
|
+
|
|
160
213
|
# config.main_branch = 'main'
|
|
161
214
|
# config.production_branch = 'production'
|
|
162
215
|
|
|
@@ -5,14 +5,14 @@ require 'active_model'
|
|
|
5
5
|
module PlanMyStuff
|
|
6
6
|
# Value object representing a single manager approval on an +Issue+.
|
|
7
7
|
# Persisted in +IssueMetadata#approvals+ and returned from
|
|
8
|
-
# +Issue.request_approvals!+, +Issue.approve!+, and
|
|
8
|
+
# +Issue.request_approvals!+, +Issue.approve!+, +Issue.reject!+, and
|
|
9
9
|
# +Issue.revoke_approval!+.
|
|
10
10
|
#
|
|
11
11
|
# Mirrors +PlanMyStuff::Link+: +ActiveModel::Attributes+-backed, with
|
|
12
12
|
# +Serializers::JSON+ for round-trip through the metadata blob.
|
|
13
13
|
#
|
|
14
14
|
class Approval
|
|
15
|
-
STATUSES = %w[pending approved].freeze
|
|
15
|
+
STATUSES = %w[pending approved rejected].freeze
|
|
16
16
|
|
|
17
17
|
include ActiveModel::Model
|
|
18
18
|
include ActiveModel::Attributes
|
|
@@ -20,10 +20,12 @@ module PlanMyStuff
|
|
|
20
20
|
|
|
21
21
|
# @return [Integer] app-side user id of the required approver
|
|
22
22
|
attribute :user_id, :integer
|
|
23
|
-
# @return [String] +"pending"+ or +"
|
|
23
|
+
# @return [String] +"pending"+, +"approved"+, or +"rejected"+
|
|
24
24
|
attribute :status, :string, default: 'pending'
|
|
25
25
|
# @return [DateTime, nil] timestamp when status flipped to +"approved"+
|
|
26
26
|
attribute :approved_at, :datetime
|
|
27
|
+
# @return [DateTime, nil] timestamp when status flipped to +"rejected"+
|
|
28
|
+
attribute :rejected_at, :datetime
|
|
27
29
|
|
|
28
30
|
validates :user_id, presence: true, numericality: { greater_than: 0, only_integer: true }
|
|
29
31
|
validates :status, inclusion: { in: STATUSES }
|
|
@@ -38,6 +40,11 @@ module PlanMyStuff
|
|
|
38
40
|
status == 'approved'
|
|
39
41
|
end
|
|
40
42
|
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
def rejected?
|
|
45
|
+
status == 'rejected'
|
|
46
|
+
end
|
|
47
|
+
|
|
41
48
|
# Lazy-resolves the app-side user for this approval.
|
|
42
49
|
# Not memoized -- +PlanMyStuff::UserResolver+ owns caching.
|
|
43
50
|
#
|
|
@@ -52,7 +59,8 @@ module PlanMyStuff
|
|
|
52
59
|
{
|
|
53
60
|
user_id: user_id,
|
|
54
61
|
status: status,
|
|
55
|
-
approved_at: approved_at
|
|
62
|
+
approved_at: PlanMyStuff.format_time(approved_at),
|
|
63
|
+
rejected_at: PlanMyStuff.format_time(rejected_at),
|
|
56
64
|
}
|
|
57
65
|
end
|
|
58
66
|
|
|
@@ -27,15 +27,21 @@ module PlanMyStuff
|
|
|
27
27
|
# +production_commit_sha+ from +PlanMyStuff.configuration+ (this
|
|
28
28
|
# task is dev-only, so there's no reason to parameterize them).
|
|
29
29
|
#
|
|
30
|
+
# @raise [PlanMyStuff::ConfigurationError] if sns details are missing
|
|
31
|
+
#
|
|
30
32
|
# @param endpoint_url [String] full URL including path
|
|
31
33
|
# @param event_name [String] ECS eventName (default completed)
|
|
32
34
|
#
|
|
33
35
|
# @return [Net::HTTPResponse]
|
|
34
36
|
#
|
|
35
|
-
def post(endpoint_url:, event_name: DEFAULT_EVENT)
|
|
37
|
+
def post!(endpoint_url:, event_name: DEFAULT_EVENT)
|
|
36
38
|
config = PlanMyStuff.configuration
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
if config.sns_topic_arn.blank?
|
|
40
|
+
raise(PlanMyStuff::ConfigurationError, 'PlanMyStuff.configuration.sns_topic_arn is blank')
|
|
41
|
+
end
|
|
42
|
+
if config.aws_service_identifier.blank?
|
|
43
|
+
raise(PlanMyStuff::ConfigurationError, 'PlanMyStuff.configuration.aws_service_identifier is blank')
|
|
44
|
+
end
|
|
39
45
|
|
|
40
46
|
topic_arn = config.sns_topic_arn
|
|
41
47
|
service_arn = "arn:aws:ecs:us-east-1:000000000000:service/simulated-cluster/#{config.aws_service_identifier}"
|
|
@@ -73,7 +79,7 @@ module PlanMyStuff
|
|
|
73
79
|
'eventType' => 'INFO',
|
|
74
80
|
'eventName' => event_name,
|
|
75
81
|
'deploymentId' => "ecs-svc/#{SecureRandom.hex(8)}",
|
|
76
|
-
'updatedAt' => Time.now.utc
|
|
82
|
+
'updatedAt' => PlanMyStuff.format_time(Time.now.utc),
|
|
77
83
|
}
|
|
78
84
|
detail['commitSha'] = commit_sha if commit_sha
|
|
79
85
|
|
|
@@ -83,7 +89,7 @@ module PlanMyStuff
|
|
|
83
89
|
'detail-type' => 'ECS Deployment State Change',
|
|
84
90
|
'source' => 'aws.ecs',
|
|
85
91
|
'account' => '000000000000',
|
|
86
|
-
'time' => Time.now.utc
|
|
92
|
+
'time' => PlanMyStuff.format_time(Time.now.utc),
|
|
87
93
|
'region' => 'us-east-1',
|
|
88
94
|
'resources' => [service_arn],
|
|
89
95
|
'detail' => detail,
|
|
@@ -92,7 +98,7 @@ module PlanMyStuff
|
|
|
92
98
|
|
|
93
99
|
# @return [Hash]
|
|
94
100
|
def build_sns_envelope(message:, topic_arn:)
|
|
95
|
-
now = Time.now.utc
|
|
101
|
+
now = PlanMyStuff.format_time(Time.now.utc)
|
|
96
102
|
{
|
|
97
103
|
'Type' => 'Notification',
|
|
98
104
|
'MessageId' => SecureRandom.uuid,
|
|
@@ -40,7 +40,7 @@ module PlanMyStuff
|
|
|
40
40
|
metadata.app_name = hash[:app_name]
|
|
41
41
|
metadata.created_by = hash[:created_by]
|
|
42
42
|
metadata.visibility = hash.fetch(:visibility, 'internal')
|
|
43
|
-
metadata.custom_fields = CustomFields.new(
|
|
43
|
+
metadata.custom_fields = PlanMyStuff::CustomFields.new(
|
|
44
44
|
custom_fields_schema,
|
|
45
45
|
hash[:custom_fields] || {},
|
|
46
46
|
)
|
|
@@ -69,10 +69,10 @@ module PlanMyStuff
|
|
|
69
69
|
metadata.gem_version = PlanMyStuff::VERSION::STRING
|
|
70
70
|
metadata.rails_env = (defined?(Rails) && Rails.respond_to?(:env)) ? Rails.env.to_s : nil
|
|
71
71
|
metadata.app_name = config.app_name
|
|
72
|
-
resolved = UserResolver.resolve(user)
|
|
73
|
-
metadata.created_by = resolved.present? ? UserResolver.user_id(resolved) : nil
|
|
72
|
+
resolved = PlanMyStuff::UserResolver.resolve(user)
|
|
73
|
+
metadata.created_by = resolved.present? ? PlanMyStuff::UserResolver.user_id(resolved) : nil
|
|
74
74
|
metadata.visibility = visibility
|
|
75
|
-
metadata.custom_fields = CustomFields.new(
|
|
75
|
+
metadata.custom_fields = PlanMyStuff::CustomFields.new(
|
|
76
76
|
custom_fields_schema,
|
|
77
77
|
custom_fields_data,
|
|
78
78
|
)
|
|
@@ -119,8 +119,6 @@ module PlanMyStuff
|
|
|
119
119
|
|
|
120
120
|
# Validates custom fields against the schema.
|
|
121
121
|
#
|
|
122
|
-
# @raise [ActiveModel::ValidationError] if validation fails
|
|
123
|
-
#
|
|
124
122
|
# @return [true]
|
|
125
123
|
#
|
|
126
124
|
def validate_custom_fields!
|
|
@@ -131,14 +129,5 @@ module PlanMyStuff
|
|
|
131
129
|
def to_json(...)
|
|
132
130
|
to_h.to_json(...)
|
|
133
131
|
end
|
|
134
|
-
|
|
135
|
-
private
|
|
136
|
-
|
|
137
|
-
# @return [String, nil]
|
|
138
|
-
def format_time(time)
|
|
139
|
-
return if time.nil?
|
|
140
|
-
|
|
141
|
-
time.utc.iso8601
|
|
142
|
-
end
|
|
143
132
|
end
|
|
144
133
|
end
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module PlanMyStuff
|
|
4
|
-
# Shared base for GitHub Projects V2 wrappers. Holds attribute definitions,
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# behavior and optional filtering on +find+/+list+.
|
|
4
|
+
# Shared base for GitHub Projects V2 wrappers. Holds attribute definitions, generic find/list/update machinery,
|
|
5
|
+
# hydration, and instance helpers. Concrete subclasses (Project, TestingProject) add their own +create!+ behavior
|
|
6
|
+
# and optional filtering on +find+/+list+.
|
|
8
7
|
#
|
|
9
|
-
# Not instantiated directly. +find+ and +list+ dispatch via +build_detail
|
|
10
|
-
#
|
|
11
|
-
# +kind+ field in the project readme.
|
|
8
|
+
# Not instantiated directly. +find+ and +list+ dispatch via +build_detail+/+build_summary+ to the correct concrete
|
|
9
|
+
# subclass based on the metadata +kind+ field in the project readme.
|
|
12
10
|
class BaseProject < PlanMyStuff::ApplicationRecord
|
|
13
11
|
MAX_AUTO_PAGINATE_ITEMS = 500
|
|
14
12
|
ITEMS_PER_PAGE = 100
|
|
@@ -45,9 +43,10 @@ module PlanMyStuff
|
|
|
45
43
|
attribute :has_next_page
|
|
46
44
|
|
|
47
45
|
class << self
|
|
48
|
-
# Generic find - returns whichever concrete project type is at the given
|
|
49
|
-
#
|
|
50
|
-
#
|
|
46
|
+
# Generic find - returns whichever concrete project type is at the given number, dispatching on metadata kind.
|
|
47
|
+
# Subclasses may override to apply filtering (e.g. Project raises for testing projects by default).
|
|
48
|
+
#
|
|
49
|
+
# @raise [ArgumentError] if paginate mode is invalid
|
|
51
50
|
#
|
|
52
51
|
# @param number [Integer]
|
|
53
52
|
# @param paginate [Symbol] :auto (default) or :cursor
|
|
@@ -68,9 +67,8 @@ module PlanMyStuff
|
|
|
68
67
|
end
|
|
69
68
|
end
|
|
70
69
|
|
|
71
|
-
# Generic list - returns all projects in the configured organization,
|
|
72
|
-
#
|
|
73
|
-
# Subclasses may override to apply filtering.
|
|
70
|
+
# Generic list - returns all projects in the configured organization, each dispatched to its concrete type
|
|
71
|
+
# (Project or TestingProject). Subclasses may override to apply filtering.
|
|
74
72
|
#
|
|
75
73
|
# @return [Array<PlanMyStuff::BaseProject>]
|
|
76
74
|
#
|
|
@@ -124,7 +122,7 @@ module PlanMyStuff
|
|
|
124
122
|
end
|
|
125
123
|
|
|
126
124
|
body = readme.nil? ? parsed[:body] : readme
|
|
127
|
-
update_input[:readme] = PlanMyStuff::MetadataParser.serialize(existing_metadata, body)
|
|
125
|
+
update_input[:readme] = PlanMyStuff::MetadataParser.serialize!(existing_metadata, body)
|
|
128
126
|
end
|
|
129
127
|
|
|
130
128
|
PlanMyStuff.client.graphql(
|
|
@@ -135,10 +133,9 @@ module PlanMyStuff
|
|
|
135
133
|
find(project_number)
|
|
136
134
|
end
|
|
137
135
|
|
|
138
|
-
# Clones an existing GitHub Project into the configured organization.
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
# concrete subclass (Project or TestingProject) based on the cloned readme.
|
|
136
|
+
# Clones an existing GitHub Project into the configured organization. The copy inherits all custom fields and
|
|
137
|
+
# board layout from the source. Returns the newly created project via +find+, dispatched to the correct concrete
|
|
138
|
+
# subclass (Project or TestingProject) based on the cloned readme.
|
|
142
139
|
#
|
|
143
140
|
# @param source_number [Integer] project number of the project to copy from
|
|
144
141
|
# @param title [String] title for the new project
|
|
@@ -168,11 +165,13 @@ module PlanMyStuff
|
|
|
168
165
|
|
|
169
166
|
# Resolves a project number, falling back to config.default_project_number.
|
|
170
167
|
#
|
|
168
|
+
# @raise [ArgumentError] if project_number is nil and config.default_project_number is not set
|
|
169
|
+
#
|
|
171
170
|
# @param project_number [Integer, nil]
|
|
172
171
|
#
|
|
173
172
|
# @return [Integer]
|
|
174
173
|
#
|
|
175
|
-
def resolve_default_project_number(project_number)
|
|
174
|
+
def resolve_default_project_number!(project_number)
|
|
176
175
|
return project_number if project_number.present?
|
|
177
176
|
|
|
178
177
|
PlanMyStuff.configuration.default_project_number ||
|
|
@@ -181,8 +180,8 @@ module PlanMyStuff
|
|
|
181
180
|
|
|
182
181
|
private
|
|
183
182
|
|
|
184
|
-
# Builds a summary Project from a list query node.
|
|
185
|
-
#
|
|
183
|
+
# Builds a summary Project from a list query node. Dispatches to TestingProject when the readme metadata has
|
|
184
|
+
# kind: "testing".
|
|
186
185
|
#
|
|
187
186
|
# @param node [Hash]
|
|
188
187
|
#
|
|
@@ -193,12 +192,12 @@ module PlanMyStuff
|
|
|
193
192
|
parsed_meta = PlanMyStuff::MetadataParser.parse(raw_readme)
|
|
194
193
|
klass = dispatch_project_class(parsed_meta[:metadata])
|
|
195
194
|
project = klass.new
|
|
196
|
-
project.__send__(:hydrate_summary, node)
|
|
195
|
+
project.__send__(:hydrate_summary, node, raw_readme: raw_readme, parsed_meta: parsed_meta)
|
|
197
196
|
project
|
|
198
197
|
end
|
|
199
198
|
|
|
200
|
-
# Builds a detailed Project from a find query response.
|
|
201
|
-
#
|
|
199
|
+
# Builds a detailed Project from a find query response. Dispatches to TestingProject when the readme metadata
|
|
200
|
+
# has kind: "testing".
|
|
202
201
|
#
|
|
203
202
|
# @param graphql_project [Hash]
|
|
204
203
|
# @param items [Array<Hash>]
|
|
@@ -222,8 +221,8 @@ module PlanMyStuff
|
|
|
222
221
|
project
|
|
223
222
|
end
|
|
224
223
|
|
|
225
|
-
# Returns the appropriate project class based on the metadata kind field.
|
|
226
|
-
#
|
|
224
|
+
# Returns the appropriate project class based on the metadata kind field. Always dispatches to a concrete
|
|
225
|
+
# subclass (never BaseProject itself).
|
|
227
226
|
#
|
|
228
227
|
# @param meta_hash [Hash]
|
|
229
228
|
#
|
|
@@ -280,8 +279,8 @@ module PlanMyStuff
|
|
|
280
279
|
)
|
|
281
280
|
end
|
|
282
281
|
|
|
283
|
-
# Fetches a single page of project data. Returns a lightweight hash
|
|
284
|
-
#
|
|
282
|
+
# Fetches a single page of project data. Returns a lightweight hash for pagination loop consumption (not a
|
|
283
|
+
# Project instance).
|
|
285
284
|
#
|
|
286
285
|
# @param org [String]
|
|
287
286
|
# @param number [Integer]
|
|
@@ -328,7 +327,7 @@ module PlanMyStuff
|
|
|
328
327
|
number: content[:number],
|
|
329
328
|
url: content[:url],
|
|
330
329
|
state: content[:state],
|
|
331
|
-
repo: repo_name.present? ? PlanMyStuff::Repo.resolve(repo_name) : nil,
|
|
330
|
+
repo: repo_name.present? ? PlanMyStuff::Repo.resolve!(repo_name) : nil,
|
|
332
331
|
status: extract_item_status(field_values),
|
|
333
332
|
field_values: parse_field_values(field_values),
|
|
334
333
|
updated_at: item[:updatedAt],
|
|
@@ -405,14 +404,24 @@ module PlanMyStuff
|
|
|
405
404
|
#
|
|
406
405
|
# @return [Hash] with :id and :options keys
|
|
407
406
|
#
|
|
407
|
+
def status_field
|
|
408
|
+
status_field!
|
|
409
|
+
rescue
|
|
410
|
+
nil
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Returns the Status single-select field definition.
|
|
414
|
+
#
|
|
408
415
|
# @raise [PlanMyStuff::APIError] if no Status field exists
|
|
409
416
|
#
|
|
410
|
-
|
|
411
|
-
|
|
417
|
+
# @return [Hash] with :id and :options keys
|
|
418
|
+
#
|
|
419
|
+
def status_field!
|
|
420
|
+
status_field = fields.find { |f| f[:name] == 'Status' && f[:options] }
|
|
412
421
|
|
|
413
|
-
raise(APIError, "No 'Status' field found on project ##{number}")
|
|
422
|
+
raise(PlanMyStuff::APIError, "No 'Status' field found on project ##{number}") if status_field.nil?
|
|
414
423
|
|
|
415
|
-
|
|
424
|
+
status_field
|
|
416
425
|
end
|
|
417
426
|
|
|
418
427
|
# @return [Boolean]
|
|
@@ -422,8 +431,6 @@ module PlanMyStuff
|
|
|
422
431
|
|
|
423
432
|
# Persists the project. Creates if new, updates if persisted.
|
|
424
433
|
#
|
|
425
|
-
# @raise [PlanMyStuff::StaleObjectError] on update if stale
|
|
426
|
-
#
|
|
427
434
|
# @return [self]
|
|
428
435
|
#
|
|
429
436
|
def save!
|
|
@@ -447,13 +454,11 @@ module PlanMyStuff
|
|
|
447
454
|
self
|
|
448
455
|
end
|
|
449
456
|
|
|
450
|
-
# Updates this project on GitHub. Raises StaleObjectError if the remote
|
|
451
|
-
#
|
|
457
|
+
# Updates this project on GitHub. Raises StaleObjectError if the remote has been modified since this instance was
|
|
458
|
+
# loaded.
|
|
452
459
|
#
|
|
453
460
|
# @param attrs [Hash] attributes to update (title:, readme:, description:, metadata:)
|
|
454
461
|
#
|
|
455
|
-
# @raise [PlanMyStuff::StaleObjectError] if remote updated_at differs from local
|
|
456
|
-
#
|
|
457
462
|
# @return [self]
|
|
458
463
|
#
|
|
459
464
|
def update!(**attrs)
|
|
@@ -479,9 +484,8 @@ module PlanMyStuff
|
|
|
479
484
|
|
|
480
485
|
private
|
|
481
486
|
|
|
482
|
-
# Kwargs derived from +metadata+ for forwarding to +self.class.create!+
|
|
483
|
-
#
|
|
484
|
-
# Subclasses override to include type-specific metadata attributes.
|
|
487
|
+
# Kwargs derived from +metadata+ for forwarding to +self.class.create!+ so +save!+ preserves in-memory metadata
|
|
488
|
+
# mutations on new records. Subclasses override to include type-specific metadata attributes.
|
|
485
489
|
#
|
|
486
490
|
# @return [Hash]
|
|
487
491
|
#
|
|
@@ -493,9 +497,8 @@ module PlanMyStuff
|
|
|
493
497
|
}
|
|
494
498
|
end
|
|
495
499
|
|
|
496
|
-
# Metadata hash forwarded to the class +update!+ so +save!+ preserves
|
|
497
|
-
#
|
|
498
|
-
# the instance has no PMS metadata to avoid clobbering remote values.
|
|
500
|
+
# Metadata hash forwarded to the class +update!+ so +save!+ preserves in-memory metadata mutations on persisted
|
|
501
|
+
# records. Returns +nil+ when the instance has no PMS metadata to avoid clobbering remote values.
|
|
499
502
|
#
|
|
500
503
|
# @return [Hash, nil]
|
|
501
504
|
#
|
|
@@ -511,7 +514,7 @@ module PlanMyStuff
|
|
|
511
514
|
#
|
|
512
515
|
# @return [void]
|
|
513
516
|
#
|
|
514
|
-
def hydrate_summary(node)
|
|
517
|
+
def hydrate_summary(node, raw_readme:, parsed_meta:)
|
|
515
518
|
@github_response = node
|
|
516
519
|
self.id = node[:id]
|
|
517
520
|
self.number = node[:number]
|
|
@@ -520,6 +523,9 @@ module PlanMyStuff
|
|
|
520
523
|
self.url = node[:url]
|
|
521
524
|
self.closed = node[:closed]
|
|
522
525
|
self.updated_at = parse_github_time(node[:updatedAt])
|
|
526
|
+
self.raw_readme = raw_readme
|
|
527
|
+
self.metadata = build_project_metadata(parsed_meta[:metadata])
|
|
528
|
+
self.readme = parsed_meta[:body]
|
|
523
529
|
persisted!
|
|
524
530
|
end
|
|
525
531
|
|
|
@@ -553,6 +559,9 @@ module PlanMyStuff
|
|
|
553
559
|
self.items = items.map { |item_hash| item_class.build(item_hash, project: self) }
|
|
554
560
|
self.next_cursor = next_cursor
|
|
555
561
|
self.has_next_page = has_next_page
|
|
562
|
+
|
|
563
|
+
status_field!
|
|
564
|
+
|
|
556
565
|
persisted!
|
|
557
566
|
end
|
|
558
567
|
|
|
@@ -579,11 +588,13 @@ module PlanMyStuff
|
|
|
579
588
|
self.items = other.items
|
|
580
589
|
self.next_cursor = other.next_cursor
|
|
581
590
|
self.has_next_page = other.has_next_page
|
|
591
|
+
|
|
592
|
+
status_field!
|
|
593
|
+
|
|
582
594
|
persisted!
|
|
583
595
|
end
|
|
584
596
|
|
|
585
|
-
# Raises StaleObjectError if the remote project has been modified
|
|
586
|
-
# since this instance was loaded.
|
|
597
|
+
# Raises StaleObjectError if the remote project has been modified since this instance was loaded.
|
|
587
598
|
#
|
|
588
599
|
# @raise [PlanMyStuff::StaleObjectError]
|
|
589
600
|
#
|
|
@@ -607,8 +618,8 @@ module PlanMyStuff
|
|
|
607
618
|
))
|
|
608
619
|
end
|
|
609
620
|
|
|
610
|
-
# Builds the appropriate metadata object for this project instance.
|
|
611
|
-
#
|
|
621
|
+
# Builds the appropriate metadata object for this project instance. Dispatches to TestingProjectMetadata when
|
|
622
|
+
# kind is "testing".
|
|
612
623
|
#
|
|
613
624
|
# @param meta_hash [Hash]
|
|
614
625
|
#
|
|
@@ -629,11 +640,11 @@ module PlanMyStuff
|
|
|
629
640
|
# @return [Array<Hash>]
|
|
630
641
|
#
|
|
631
642
|
def extract_statuses(fields_nodes)
|
|
632
|
-
|
|
643
|
+
field = fields_nodes.find { |f| f[:name] == 'Status' && f.key?(:options) }
|
|
633
644
|
|
|
634
|
-
return [] unless
|
|
645
|
+
return [] unless field
|
|
635
646
|
|
|
636
|
-
(
|
|
647
|
+
(field[:options] || []).map do |opt|
|
|
637
648
|
{ id: opt[:id], name: opt[:name] }
|
|
638
649
|
end
|
|
639
650
|
end
|
|
@@ -650,8 +661,10 @@ module PlanMyStuff
|
|
|
650
661
|
end
|
|
651
662
|
end
|
|
652
663
|
|
|
653
|
-
# Returns the item class used to build items for this project type.
|
|
654
|
-
#
|
|
664
|
+
# Returns the item class used to build items for this project type. Subclasses override to return a
|
|
665
|
+
# domain-specific item class.
|
|
666
|
+
#
|
|
667
|
+
# @raise [NotImplementedError] if not implemented in subclass
|
|
655
668
|
#
|
|
656
669
|
# @return [Class]
|
|
657
670
|
#
|