plan_my_stuff 0.1.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 +7 -0
- data/LICENSE +28 -0
- data/README.md +284 -0
- data/app/controllers/plan_my_stuff/application_controller.rb +76 -0
- data/app/controllers/plan_my_stuff/comments_controller.rb +82 -0
- data/app/controllers/plan_my_stuff/issues_controller.rb +145 -0
- data/app/controllers/plan_my_stuff/labels_controller.rb +30 -0
- data/app/controllers/plan_my_stuff/project_items_controller.rb +93 -0
- data/app/controllers/plan_my_stuff/projects_controller.rb +17 -0
- data/app/views/plan_my_stuff/comments/edit.html.erb +16 -0
- data/app/views/plan_my_stuff/comments/partials/_form.html.erb +32 -0
- data/app/views/plan_my_stuff/issues/edit.html.erb +12 -0
- data/app/views/plan_my_stuff/issues/index.html.erb +37 -0
- data/app/views/plan_my_stuff/issues/new.html.erb +7 -0
- data/app/views/plan_my_stuff/issues/partials/_form.html.erb +41 -0
- data/app/views/plan_my_stuff/issues/partials/_labels.html.erb +23 -0
- data/app/views/plan_my_stuff/issues/partials/_viewers.html.erb +32 -0
- data/app/views/plan_my_stuff/issues/show.html.erb +58 -0
- data/app/views/plan_my_stuff/projects/index.html.erb +13 -0
- data/app/views/plan_my_stuff/projects/show.html.erb +101 -0
- data/config/routes.rb +25 -0
- data/lib/generators/plan_my_stuff/install/install_generator.rb +38 -0
- data/lib/generators/plan_my_stuff/install/templates/initializer.rb +106 -0
- data/lib/generators/plan_my_stuff/views/views_generator.rb +22 -0
- data/lib/plan_my_stuff/application_record.rb +39 -0
- data/lib/plan_my_stuff/base_metadata.rb +136 -0
- data/lib/plan_my_stuff/client.rb +143 -0
- data/lib/plan_my_stuff/comment.rb +360 -0
- data/lib/plan_my_stuff/comment_metadata.rb +56 -0
- data/lib/plan_my_stuff/configuration.rb +139 -0
- data/lib/plan_my_stuff/custom_fields.rb +65 -0
- data/lib/plan_my_stuff/engine.rb +11 -0
- data/lib/plan_my_stuff/errors.rb +87 -0
- data/lib/plan_my_stuff/issue.rb +486 -0
- data/lib/plan_my_stuff/issue_metadata.rb +111 -0
- data/lib/plan_my_stuff/label.rb +59 -0
- data/lib/plan_my_stuff/markdown.rb +83 -0
- data/lib/plan_my_stuff/metadata_parser.rb +53 -0
- data/lib/plan_my_stuff/project.rb +504 -0
- data/lib/plan_my_stuff/project_item.rb +414 -0
- data/lib/plan_my_stuff/test_helpers.rb +501 -0
- data/lib/plan_my_stuff/user_resolver.rb +61 -0
- data/lib/plan_my_stuff/verifier.rb +102 -0
- data/lib/plan_my_stuff/version.rb +19 -0
- data/lib/plan_my_stuff.rb +69 -0
- data/lib/tasks/plan_my_stuff.rake +23 -0
- metadata +126 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
class CommentMetadata < BaseMetadata
|
|
5
|
+
# @return [Boolean] true if this comment holds the issue's body content
|
|
6
|
+
attr_accessor :issue_body
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
# Builds a CommentMetadata from a parsed hash (e.g. from MetadataParser)
|
|
10
|
+
#
|
|
11
|
+
# @param hash [Hash]
|
|
12
|
+
#
|
|
13
|
+
# @return [CommentMetadata]
|
|
14
|
+
#
|
|
15
|
+
def from_hash(hash)
|
|
16
|
+
metadata = new
|
|
17
|
+
apply_common_from_hash(metadata, hash)
|
|
18
|
+
metadata.issue_body = hash[:issue_body] || false
|
|
19
|
+
|
|
20
|
+
metadata
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Builds a new CommentMetadata for comment creation, auto-filling gem defaults
|
|
24
|
+
#
|
|
25
|
+
# @param user [Object, Integer] user object or user_id
|
|
26
|
+
# @param visibility [String] "public" or "internal"
|
|
27
|
+
# @param custom_fields [Hash] app-defined field values
|
|
28
|
+
# @param issue_body [Boolean] whether this comment holds the issue body
|
|
29
|
+
#
|
|
30
|
+
# @return [CommentMetadata]
|
|
31
|
+
#
|
|
32
|
+
def build(user:, visibility: 'internal', custom_fields: {}, issue_body: false)
|
|
33
|
+
metadata = new
|
|
34
|
+
apply_common_build(metadata, user: user, visibility: visibility, custom_fields_data: custom_fields)
|
|
35
|
+
metadata.issue_body = issue_body
|
|
36
|
+
|
|
37
|
+
metadata
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize
|
|
42
|
+
super
|
|
43
|
+
@issue_body = false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Boolean]
|
|
47
|
+
def issue_body?
|
|
48
|
+
issue_body == true
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [Hash]
|
|
52
|
+
def to_h
|
|
53
|
+
super.merge(issue_body: issue_body)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
class Configuration
|
|
5
|
+
# @return [String] GitHub PAT with repo and project scopes. Required.
|
|
6
|
+
attr_accessor :access_token
|
|
7
|
+
|
|
8
|
+
# @return [String] GitHub organization name. Required.
|
|
9
|
+
attr_accessor :organization
|
|
10
|
+
|
|
11
|
+
# @return [Symbol, nil] default repo key used when callers omit repo: param
|
|
12
|
+
attr_accessor :default_repo
|
|
13
|
+
|
|
14
|
+
# @return [Integer, nil] default GitHub Projects V2 number for add_to_project calls
|
|
15
|
+
attr_accessor :default_project_number
|
|
16
|
+
|
|
17
|
+
# @return [String] consuming app's user model class name, constantized for lookups
|
|
18
|
+
attr_accessor :user_class
|
|
19
|
+
|
|
20
|
+
# @return [Symbol] method called on user object to get display name for comment headers
|
|
21
|
+
attr_accessor :display_name_method
|
|
22
|
+
|
|
23
|
+
# @return [Symbol] method called on user object to extract the app-side user ID
|
|
24
|
+
attr_accessor :user_id_method
|
|
25
|
+
|
|
26
|
+
# Determines if a user is support staff. Symbol (method name on user) or Proc that
|
|
27
|
+
# receives the user object and returns boolean.
|
|
28
|
+
#
|
|
29
|
+
# @return [Symbol, Proc]
|
|
30
|
+
#
|
|
31
|
+
attr_accessor :support_method
|
|
32
|
+
|
|
33
|
+
# @return [Symbol] which markdown gem to use: :commonmarker or :redcarpet
|
|
34
|
+
attr_accessor :markdown_renderer
|
|
35
|
+
|
|
36
|
+
# Default options passed to the markdown renderer. Per-call options in
|
|
37
|
+
# Markdown.render merge on top of these.
|
|
38
|
+
#
|
|
39
|
+
# For :commonmarker - passed as `options:` to `Commonmarker.to_html`
|
|
40
|
+
# e.g. `{ render: { hardbreaks: true } }`
|
|
41
|
+
#
|
|
42
|
+
# For :redcarpet - :render_options and :renderer are extracted for the HTML renderer;
|
|
43
|
+
# remaining keys are passed as extensions to `Redcarpet::Markdown.new`
|
|
44
|
+
# e.g. `{ render_options: { hard_wrap: true, no_styles: true }, autolink: true }`
|
|
45
|
+
#
|
|
46
|
+
# @return [Hash]
|
|
47
|
+
#
|
|
48
|
+
attr_accessor :markdown_options
|
|
49
|
+
|
|
50
|
+
# Proc returning boolean, or nil (always send). When it returns false the request is
|
|
51
|
+
# deferred to a background job instead of hitting GitHub.
|
|
52
|
+
#
|
|
53
|
+
# @return [Proc, nil]
|
|
54
|
+
#
|
|
55
|
+
attr_accessor :should_send_request
|
|
56
|
+
|
|
57
|
+
# Map of action type to job class name for deferred requests.
|
|
58
|
+
# Keys: :create_ticket, :post_comment, :update_status.
|
|
59
|
+
#
|
|
60
|
+
# @return [Hash{Symbol => String}]
|
|
61
|
+
#
|
|
62
|
+
attr_accessor :job_classes
|
|
63
|
+
|
|
64
|
+
# @return [Proc, nil] custom notifier for deferred requests, or nil to use DeferredMailer
|
|
65
|
+
attr_accessor :deferred_notifier
|
|
66
|
+
|
|
67
|
+
# @return [String, nil] sender address for built-in deferred request notifications
|
|
68
|
+
attr_accessor :deferred_email_from
|
|
69
|
+
|
|
70
|
+
# @return [String, nil] recipient address for built-in deferred request notifications
|
|
71
|
+
attr_accessor :deferred_email_to
|
|
72
|
+
|
|
73
|
+
# App-defined field definitions stored in issue/comment metadata.
|
|
74
|
+
# Keys are field names, values are hashes with :type and :required.
|
|
75
|
+
#
|
|
76
|
+
# @return [Hash{Symbol => Hash}]
|
|
77
|
+
#
|
|
78
|
+
attr_accessor :custom_fields
|
|
79
|
+
|
|
80
|
+
# @return [String, nil] URL prefix for building user-facing ticket URLs in the consuming app
|
|
81
|
+
attr_accessor :issues_url_prefix
|
|
82
|
+
|
|
83
|
+
# @return [String, nil] name of the consuming app, stored in metadata (e.g. "Atlas")
|
|
84
|
+
attr_accessor :app_name
|
|
85
|
+
|
|
86
|
+
# Named repo configs. Set via config.repos[:element] = 'BrandsInsurance/Element'.
|
|
87
|
+
#
|
|
88
|
+
# @return [Hash{Symbol => String}]
|
|
89
|
+
#
|
|
90
|
+
attr_reader :repos
|
|
91
|
+
|
|
92
|
+
# @return [Configuration]
|
|
93
|
+
def initialize
|
|
94
|
+
@repos = {}
|
|
95
|
+
@user_class = 'User'
|
|
96
|
+
@display_name_method = :to_s
|
|
97
|
+
@user_id_method = :id
|
|
98
|
+
@support_method = :support?
|
|
99
|
+
@markdown_renderer = :commonmarker
|
|
100
|
+
@markdown_options = {}
|
|
101
|
+
@job_classes = {}
|
|
102
|
+
@custom_fields = {}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Sets the authentication block for engine controllers.
|
|
106
|
+
#
|
|
107
|
+
# @return [void]
|
|
108
|
+
#
|
|
109
|
+
def authenticate_with(&block)
|
|
110
|
+
if block
|
|
111
|
+
@authenticate_with = block
|
|
112
|
+
else
|
|
113
|
+
@authenticate_with
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Validates that required configuration options are set.
|
|
118
|
+
#
|
|
119
|
+
# @raise [ConfigurationError] if required options are missing
|
|
120
|
+
#
|
|
121
|
+
# @return [void]
|
|
122
|
+
#
|
|
123
|
+
def validate!
|
|
124
|
+
missing = []
|
|
125
|
+
missing << 'access_token' if access_token.nil? || access_token.to_s.strip.empty?
|
|
126
|
+
missing << 'organization' if organization.nil? || organization.to_s.strip.empty?
|
|
127
|
+
|
|
128
|
+
return if missing.empty?
|
|
129
|
+
|
|
130
|
+
raise(
|
|
131
|
+
ConfigurationError,
|
|
132
|
+
"Missing required PlanMyStuff configuration: #{missing.join(', ')}",
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
class ConfigurationError < StandardError
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
# Dynamic accessor object for app-defined custom fields stored in metadata.
|
|
5
|
+
# Backed by the config.custom_fields schema, provides both hash-style and
|
|
6
|
+
# method-style access to field values.
|
|
7
|
+
class CustomFields
|
|
8
|
+
# @param schema [Hash{Symbol => Hash}] field definitions from config.custom_fields
|
|
9
|
+
# @param data [Hash] parsed field data from metadata JSON
|
|
10
|
+
#
|
|
11
|
+
def initialize(schema, data = {})
|
|
12
|
+
@schema = schema || {}
|
|
13
|
+
@data = (data || {}).transform_keys(&:to_sym)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param key [Symbol, String]
|
|
17
|
+
#
|
|
18
|
+
# @return [Object]
|
|
19
|
+
#
|
|
20
|
+
def [](key)
|
|
21
|
+
@data[key.to_sym]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param key [Symbol, String]
|
|
25
|
+
# @param value [Object]
|
|
26
|
+
#
|
|
27
|
+
# @return [Object]
|
|
28
|
+
#
|
|
29
|
+
def []=(key, value)
|
|
30
|
+
@data[key.to_sym] = value
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Hash]
|
|
34
|
+
def to_h
|
|
35
|
+
@data.dup
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [String]
|
|
39
|
+
def to_json(...)
|
|
40
|
+
to_h.to_json(...)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
46
|
+
key = method_name.to_s.delete_suffix('=').to_sym
|
|
47
|
+
@schema.key?(key) || @data.key?(key) || super
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def method_missing(method_name, *args)
|
|
51
|
+
name = method_name.to_s
|
|
52
|
+
|
|
53
|
+
if name.end_with?('=')
|
|
54
|
+
key = name.delete_suffix('=').to_sym
|
|
55
|
+
if @schema.key?(key) || @data.key?(key)
|
|
56
|
+
return @data[key] = args.first
|
|
57
|
+
end
|
|
58
|
+
elsif @schema.key?(method_name) || @data.key?(method_name)
|
|
59
|
+
return @data[method_name]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
super
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlanMyStuff
|
|
4
|
+
# Base error for all PlanMyStuff errors
|
|
5
|
+
class Error < StandardError
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# Raised when GitHub REST API returns a non-success HTTP status
|
|
9
|
+
class APIError < Error
|
|
10
|
+
# @return [Integer]
|
|
11
|
+
attr_reader :status
|
|
12
|
+
|
|
13
|
+
# @param message [String]
|
|
14
|
+
# @param status [Integer]
|
|
15
|
+
#
|
|
16
|
+
def initialize(message = nil, status: nil)
|
|
17
|
+
@status = status
|
|
18
|
+
super(message)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Raised when GitHub GraphQL API returns errors in the response body
|
|
23
|
+
class GraphQLError < Error
|
|
24
|
+
# @return [Array<Hash>]
|
|
25
|
+
attr_reader :errors
|
|
26
|
+
|
|
27
|
+
# @param message [String]
|
|
28
|
+
# @param errors [Array<Hash>]
|
|
29
|
+
#
|
|
30
|
+
def initialize(message = nil, errors: [])
|
|
31
|
+
@errors = errors
|
|
32
|
+
super(message)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Raised when GitHub rate limit is exhausted (429 or rate limit headers)
|
|
37
|
+
class RateLimitError < Error
|
|
38
|
+
# @return [Time]
|
|
39
|
+
attr_reader :retry_after
|
|
40
|
+
|
|
41
|
+
# @param message [String]
|
|
42
|
+
# @param retry_after [Time]
|
|
43
|
+
#
|
|
44
|
+
def initialize(message = nil, retry_after: nil)
|
|
45
|
+
@retry_after = retry_after
|
|
46
|
+
super(message)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Raised when an object has been modified remotely since it was loaded
|
|
51
|
+
class StaleObjectError < Error
|
|
52
|
+
# @return [Time, nil]
|
|
53
|
+
attr_reader :local_updated_at
|
|
54
|
+
|
|
55
|
+
# @return [Time, nil]
|
|
56
|
+
attr_reader :remote_updated_at
|
|
57
|
+
|
|
58
|
+
# @param message [String]
|
|
59
|
+
# @param local_updated_at [Time, nil]
|
|
60
|
+
# @param remote_updated_at [Time, nil]
|
|
61
|
+
#
|
|
62
|
+
def initialize(message = nil, local_updated_at: nil, remote_updated_at: nil)
|
|
63
|
+
@local_updated_at = local_updated_at
|
|
64
|
+
@remote_updated_at = remote_updated_at
|
|
65
|
+
super(message)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Raised when custom field validation fails (Phase 1)
|
|
70
|
+
class ValidationError < Error
|
|
71
|
+
# @return [String, nil]
|
|
72
|
+
attr_reader :field
|
|
73
|
+
|
|
74
|
+
# @return [Symbol, nil]
|
|
75
|
+
attr_reader :expected_type
|
|
76
|
+
|
|
77
|
+
# @param message [String]
|
|
78
|
+
# @param field [String, nil]
|
|
79
|
+
# @param expected_type [Symbol, nil]
|
|
80
|
+
#
|
|
81
|
+
def initialize(message = nil, field: nil, expected_type: nil)
|
|
82
|
+
@field = field
|
|
83
|
+
@expected_type = expected_type
|
|
84
|
+
super(message)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|