@@ -85,6 +85,9 @@ group :development do |
||
85 | 85 |
gem 'better_errors', '~> 1.1' |
86 | 86 |
gem 'binding_of_caller' |
87 | 87 |
gem 'quiet_assets' |
88 |
+ gem 'guard' |
|
89 |
+ gem 'guard-livereload' |
|
90 |
+ gem 'guard-rspec' |
|
88 | 91 |
end |
89 | 92 |
|
90 | 93 |
group :development, :test do |
@@ -96,6 +99,7 @@ group :development, :test do |
||
96 | 99 |
gem 'rspec', '~> 3.0' |
97 | 100 |
gem 'rspec-collection_matchers', '~> 1.0.0' |
98 | 101 |
gem 'rspec-rails', '~> 3.0.1' |
102 |
+ gem 'rspec-html-matchers', '~> 0.6.1' |
|
99 | 103 |
gem 'shoulda-matchers' |
100 | 104 |
gem 'spring' |
101 | 105 |
gem 'spring-commands-rspec' |
@@ -55,6 +55,8 @@ GEM |
||
55 | 55 |
rails (>= 3.1) |
56 | 56 |
buftok (0.2.0) |
57 | 57 |
builder (3.2.2) |
58 |
+ celluloid (0.15.2) |
|
59 |
+ timers (~> 1.1.0) |
|
58 | 60 |
chronic (0.10.2) |
59 | 61 |
coderay (1.1.0) |
60 | 62 |
coffee-rails (4.0.1) |
@@ -108,6 +110,9 @@ GEM |
||
108 | 110 |
http_parser.rb (>= 0.6.0) |
109 | 111 |
em-socksify (0.3.0) |
110 | 112 |
eventmachine (>= 1.0.0.beta.4) |
113 |
+ em-websocket (0.5.1) |
|
114 |
+ eventmachine (>= 0.12.9) |
|
115 |
+ http_parser.rb (~> 0.6.0) |
|
111 | 116 |
equalizer (0.0.9) |
112 | 117 |
erector (0.10.0) |
113 | 118 |
treetop (>= 1.2.3) |
@@ -134,6 +139,7 @@ GEM |
||
134 | 139 |
foreman (0.63.0) |
135 | 140 |
dotenv (>= 0.7) |
136 | 141 |
thor (>= 0.13.6) |
142 |
+ formatador (0.2.5) |
|
137 | 143 |
geokit (1.8.5) |
138 | 144 |
multi_json (>= 1.3.2) |
139 | 145 |
geokit-rails (2.0.1) |
@@ -150,6 +156,19 @@ GEM |
||
150 | 156 |
retriable (>= 1.4) |
151 | 157 |
signet (>= 0.5.0) |
152 | 158 |
uuidtools (>= 2.1.0) |
159 |
+ guard (2.6.1) |
|
160 |
+ formatador (>= 0.2.4) |
|
161 |
+ listen (~> 2.7) |
|
162 |
+ lumberjack (~> 1.0) |
|
163 |
+ pry (>= 0.9.12) |
|
164 |
+ thor (>= 0.18.1) |
|
165 |
+ guard-livereload (2.2.0) |
|
166 |
+ em-websocket (~> 0.5) |
|
167 |
+ guard (~> 2.0) |
|
168 |
+ multi_json (~> 1.8) |
|
169 |
+ guard-rspec (4.3.1) |
|
170 |
+ guard (~> 2.1) |
|
171 |
+ rspec (>= 2.14, < 4.0) |
|
153 | 172 |
hashie (2.0.5) |
154 | 173 |
hike (1.2.3) |
155 | 174 |
hipchat (1.2.0) |
@@ -178,6 +197,11 @@ GEM |
||
178 | 197 |
addressable (~> 2.3) |
179 | 198 |
libv8 (3.16.14.7) |
180 | 199 |
liquid (2.6.1) |
200 |
+ listen (2.7.9) |
|
201 |
+ celluloid (>= 0.15.2) |
|
202 |
+ rb-fsevent (>= 0.9.3) |
|
203 |
+ rb-inotify (>= 0.9) |
|
204 |
+ lumberjack (1.0.9) |
|
181 | 205 |
macaddr (1.7.1) |
182 | 206 |
systemu (~> 2.6.2) |
183 | 207 |
mail (2.5.4) |
@@ -262,6 +286,9 @@ GEM |
||
262 | 286 |
thor (>= 0.18.1, < 2.0) |
263 | 287 |
raindrops (0.13.0) |
264 | 288 |
rake (10.3.2) |
289 |
+ rb-fsevent (0.9.4) |
|
290 |
+ rb-inotify (0.9.5) |
|
291 |
+ ffi (>= 0.5.0) |
|
265 | 292 |
rdoc (4.1.1) |
266 | 293 |
json (~> 1.4) |
267 | 294 |
ref (1.0.5) |
@@ -283,6 +310,9 @@ GEM |
||
283 | 310 |
rspec-expectations (3.0.4) |
284 | 311 |
diff-lcs (>= 1.2.0, < 2.0) |
285 | 312 |
rspec-support (~> 3.0.0) |
313 |
+ rspec-html-matchers (0.6.1) |
|
314 |
+ nokogiri (~> 1) |
|
315 |
+ rspec (~> 3) |
|
286 | 316 |
rspec-mocks (3.0.4) |
287 | 317 |
rspec-support (~> 3.0.0) |
288 | 318 |
rspec-rails (3.0.2) |
@@ -350,6 +380,7 @@ GEM |
||
350 | 380 |
thor (0.19.1) |
351 | 381 |
thread_safe (0.3.4) |
352 | 382 |
tilt (1.4.1) |
383 |
+ timers (1.1.0) |
|
353 | 384 |
tins (1.3.2) |
354 | 385 |
treetop (1.4.15) |
355 | 386 |
polyglot |
@@ -438,6 +469,9 @@ DEPENDENCIES |
||
438 | 469 |
geokit (~> 1.8.4) |
439 | 470 |
geokit-rails (~> 2.0.1) |
440 | 471 |
google-api-client |
472 |
+ guard |
|
473 |
+ guard-livereload |
|
474 |
+ guard-rspec |
|
441 | 475 |
hipchat (~> 1.2.0) |
442 | 476 |
httparty (~> 0.13) |
443 | 477 |
jquery-rails (~> 3.1.0) |
@@ -466,6 +500,7 @@ DEPENDENCIES |
||
466 | 500 |
rr |
467 | 501 |
rspec (~> 3.0) |
468 | 502 |
rspec-collection_matchers (~> 1.0.0) |
503 |
+ rspec-html-matchers (~> 0.6.1) |
|
469 | 504 |
rspec-rails (~> 3.0.1) |
470 | 505 |
rturk (~> 2.12.1) |
471 | 506 |
ruby-growl (~> 4.1.0) |
@@ -0,0 +1,25 @@ |
||
1 |
+ |
|
2 |
+guard 'livereload' do |
|
3 |
+ watch(%r{app/views/.+\.(erb|haml|slim)$}) |
|
4 |
+ watch(%r{app/helpers/.+\.rb}) |
|
5 |
+ watch(%r{public/.+\.(css|js|html)}) |
|
6 |
+ watch(%r{config/locales/.+\.yml}) |
|
7 |
+ # Rails Assets Pipeline |
|
8 |
+ watch(%r{(app|vendor)(/assets/\w+/(.+\.(css|js|html|png|jpg))).*}) { |m| "/assets/#{m[3]}" } |
|
9 |
+end |
|
10 |
+ |
|
11 |
+guard :rspec, cmd: 'bundle exec spring rspec' do |
|
12 |
+ watch(%r{^spec/.+_spec\.rb$}) |
|
13 |
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } |
|
14 |
+ watch('spec/spec_helper.rb') { "spec" } |
|
15 |
+ |
|
16 |
+ # Rails example |
|
17 |
+ watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } |
|
18 |
+ watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } |
|
19 |
+ watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } |
|
20 |
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" } |
|
21 |
+ watch('config/routes.rb') { "spec/routing" } |
|
22 |
+ watch('app/controllers/application_controller.rb') { "spec/controllers" } |
|
23 |
+ watch('spec/rails_helper.rb') { "spec" } |
|
24 |
+end |
|
25 |
+ |
@@ -5,6 +5,7 @@ |
||
5 | 5 |
#= require select2 |
6 | 6 |
#= require json2 |
7 | 7 |
#= require jquery.json-editor |
8 |
+#= require jquery.serializeObject |
|
8 | 9 |
#= require latlon_and_geo |
9 | 10 |
#= require spectrum |
10 | 11 |
#= require_tree ./components |
@@ -0,0 +1,76 @@ |
||
1 |
+$ -> |
|
2 |
+ getFormData = (elem) -> |
|
3 |
+ form_data = $("#edit_agent, #new_agent").serializeObject() |
|
4 |
+ attribute = $(elem).data('attribute') |
|
5 |
+ form_data['attribute'] = attribute |
|
6 |
+ delete form_data['_method'] |
|
7 |
+ form_data |
|
8 |
+ |
|
9 |
+ window.initializeFormCompletable = -> |
|
10 |
+ returnedResults = {} |
|
11 |
+ completableDefaultOptions = (input) -> |
|
12 |
+ results: [ |
|
13 |
+ (returnedResults[$(input).data('attribute')] || {text: 'Options', children: [{id: undefined, text: 'loading ...'}]}), |
|
14 |
+ { |
|
15 |
+ text: 'Current', |
|
16 |
+ children: [id: $(input).val(), text: $(input).val()] |
|
17 |
+ }, |
|
18 |
+ { |
|
19 |
+ text: 'Custom', |
|
20 |
+ children: [id: 'manualInput', text: 'manual input'] |
|
21 |
+ }, |
|
22 |
+ ] |
|
23 |
+ |
|
24 |
+ $("input[role~=validatable], select[role~=validatable]").on 'change', (e) => |
|
25 |
+ form_data = getFormData(e.currentTarget) |
|
26 |
+ form_group = $(e.currentTarget).closest('.form-group') |
|
27 |
+ $.ajax '/agents/validate', |
|
28 |
+ type: 'POST', |
|
29 |
+ data: form_data |
|
30 |
+ success: (data) -> |
|
31 |
+ form_group.addClass('has-feedback').removeClass('has-error') |
|
32 |
+ form_group.find('span').addClass('hidden') |
|
33 |
+ form_group.find('.glyphicon-ok').removeClass('hidden') |
|
34 |
+ returnedResults = {} |
|
35 |
+ error: (data) -> |
|
36 |
+ form_group.addClass('has-feedback').addClass('has-error') |
|
37 |
+ form_group.find('span').addClass('hidden') |
|
38 |
+ form_group.find('.glyphicon-remove').removeClass('hidden') |
|
39 |
+ returnedResults = {} |
|
40 |
+ |
|
41 |
+ $("input[role~=validatable], select[role~=validatable]").trigger('change') |
|
42 |
+ |
|
43 |
+ $.each $("input[role~=completable]"), (i, input) -> |
|
44 |
+ $(input).select2( |
|
45 |
+ data: -> |
|
46 |
+ completableDefaultOptions(input) |
|
47 |
+ ).on("change", (e) -> |
|
48 |
+ if e.added && e.added.id == 'manualInput' |
|
49 |
+ $(e.currentTarget).select2("destroy") |
|
50 |
+ $(e.currentTarget).val(e.removed.id) |
|
51 |
+ ) |
|
52 |
+ |
|
53 |
+ updateDropdownData = (form_data, element, data) -> |
|
54 |
+ returnedResults[form_data.attribute] = {text: 'Options', children: data} |
|
55 |
+ $(element).trigger('change') |
|
56 |
+ $(element).select2('open') |
|
57 |
+ |
|
58 |
+ $("input[role~=completable]").on 'select2-open', (e) -> |
|
59 |
+ form_data = getFormData(e.currentTarget) |
|
60 |
+ return if returnedResults[form_data.attribute] |
|
61 |
+ |
|
62 |
+ $.ajax '/agents/complete', |
|
63 |
+ type: 'POST', |
|
64 |
+ data: form_data |
|
65 |
+ success: (data) -> |
|
66 |
+ updateDropdownData(form_data, e.currentTarget, data) |
|
67 |
+ error: (data) -> |
|
68 |
+ updateDropdownData(form_data, e.currentTarget, [{id: undefined, text: 'Error loading data.'}]) |
|
69 |
+ |
|
70 |
+ $("input[type=radio][role~=form-configurable]").change (e) -> |
|
71 |
+ input = $(e.currentTarget).parents().siblings("input[data-attribute=#{$(e.currentTarget).data('attribute')}]") |
|
72 |
+ if $(e.currentTarget).val() == 'manual' |
|
73 |
+ input.removeClass('hidden') |
|
74 |
+ else |
|
75 |
+ input.val($(e.currentTarget).val()) |
|
76 |
+ input.addClass('hidden') |
@@ -18,7 +18,6 @@ class @AgentEditPage |
||
18 | 18 |
else |
19 | 19 |
$(".agent-settings").show() |
20 | 20 |
$("#agent-spinner").fadeIn() |
21 |
- $("#agent_source_ids").select2("val", {}) |
|
22 | 21 |
$(".model-errors").hide() unless firstTime |
23 | 22 |
$.getJSON "/agents/type_details", { type: type }, (json) => |
24 | 23 |
if json.can_be_scheduled |
@@ -46,11 +45,12 @@ class @AgentEditPage |
||
46 | 45 |
|
47 | 46 |
$(".description").show().html(json.description_html) if json.description_html? |
48 | 47 |
|
49 |
- $('.oauthable-form').html(json.form) if json.form? |
|
50 |
- |
|
51 | 48 |
unless firstTime |
52 |
- window.jsonEditor.json = json.options |
|
53 |
- window.jsonEditor.rebuild() |
|
49 |
+ $('.oauthable-form').html(json.oauthable) if json.oauthable? |
|
50 |
+ $('.agent-options').html(json.form_options) if json.form_options? |
|
51 |
+ window.jsonEditor = setupJsonEditor()[0] |
|
52 |
+ |
|
53 |
+ window.initializeFormCompletable() |
|
54 | 54 |
|
55 | 55 |
$("#agent-spinner").stop(true, true).fadeOut(); |
56 | 56 |
|
@@ -0,0 +1,66 @@ |
||
1 |
+module FormConfigurable |
|
2 |
+ extend ActiveSupport::Concern |
|
3 |
+ |
|
4 |
+ included do |
|
5 |
+ class_attribute :_form_configurable_fields |
|
6 |
+ self._form_configurable_fields = HashWithIndifferentAccess.new { |h,k| h[k] = [] } |
|
7 |
+ end |
|
8 |
+ |
|
9 |
+ delegate :form_configurable_attributes, to: :class |
|
10 |
+ delegate :form_configurable_fields, to: :class |
|
11 |
+ |
|
12 |
+ def is_form_configurable? |
|
13 |
+ true |
|
14 |
+ end |
|
15 |
+ |
|
16 |
+ def validate_option(method) |
|
17 |
+ if self.respond_to? "validate_#{method}".to_sym |
|
18 |
+ self.send("validate_#{method}".to_sym) |
|
19 |
+ else |
|
20 |
+ false |
|
21 |
+ end |
|
22 |
+ end |
|
23 |
+ |
|
24 |
+ def complete_option(method) |
|
25 |
+ if self.respond_to? "complete_#{method}".to_sym |
|
26 |
+ self.send("complete_#{method}".to_sym) |
|
27 |
+ end |
|
28 |
+ end |
|
29 |
+ |
|
30 |
+ module ClassMethods |
|
31 |
+ def form_configurable(name, *args) |
|
32 |
+ options = args.extract_options!.reverse_merge(roles: [], type: :string) |
|
33 |
+ |
|
34 |
+ if args.all? { |arg| arg.is_a?(Symbol) } |
|
35 |
+ options.assert_valid_keys([:type, :roles, :values]) |
|
36 |
+ end |
|
37 |
+ |
|
38 |
+ if options[:type] == :array && (options[:values].blank? || !options[:values].is_a?(Array)) |
|
39 |
+ raise ArgumentError.new('When using :array as :type you need to provide the :values as an Array') |
|
40 |
+ end |
|
41 |
+ |
|
42 |
+ if options[:roles].is_a?(Symbol) |
|
43 |
+ options[:roles] = [options[:roles]] |
|
44 |
+ end |
|
45 |
+ |
|
46 |
+ if options[:type] == :array |
|
47 |
+ options[:roles] << :completable |
|
48 |
+ class_eval <<-EOF |
|
49 |
+ def complete_#{name} |
|
50 |
+ #{options[:values]}.map { |v| {text: v, id: v} } |
|
51 |
+ end |
|
52 |
+ EOF |
|
53 |
+ end |
|
54 |
+ |
|
55 |
+ _form_configurable_fields[name] = options |
|
56 |
+ end |
|
57 |
+ |
|
58 |
+ def form_configurable_fields |
|
59 |
+ self._form_configurable_fields |
|
60 |
+ end |
|
61 |
+ |
|
62 |
+ def form_configurable_attributes |
|
63 |
+ form_configurable_fields.keys |
|
64 |
+ end |
|
65 |
+ end |
|
66 |
+end |
@@ -35,6 +35,8 @@ class AgentsController < ApplicationController |
||
35 | 35 |
|
36 | 36 |
def type_details |
37 | 37 |
@agent = Agent.build_for_type(params[:type], current_user, {}) |
38 |
+ initialize_presenter |
|
39 |
+ |
|
38 | 40 |
render :json => { |
39 | 41 |
:can_be_scheduled => @agent.can_be_scheduled?, |
40 | 42 |
:default_schedule => @agent.default_schedule, |
@@ -43,7 +45,8 @@ class AgentsController < ApplicationController |
||
43 | 45 |
:can_control_other_agents => @agent.can_control_other_agents?, |
44 | 46 |
:options => @agent.default_options, |
45 | 47 |
:description_html => @agent.html_description, |
46 |
- :form => render_to_string(partial: 'oauth_dropdown', locals: { agent: @agent }) |
|
48 |
+ :oauthable => render_to_string(partial: 'oauth_dropdown', locals: { agent: @agent }), |
|
49 |
+ :form_options => render_to_string(partial: 'options', locals: { agent: @agent }) |
|
47 | 50 |
} |
48 | 51 |
end |
49 | 52 |
|
@@ -92,6 +95,7 @@ class AgentsController < ApplicationController |
||
92 | 95 |
else |
93 | 96 |
@agent = agents.build |
94 | 97 |
end |
98 |
+ initialize_presenter |
|
95 | 99 |
|
96 | 100 |
respond_to do |format| |
97 | 101 |
format.html |
@@ -101,17 +105,18 @@ class AgentsController < ApplicationController |
||
101 | 105 |
|
102 | 106 |
def edit |
103 | 107 |
@agent = current_user.agents.find(params[:id]) |
108 |
+ initialize_presenter |
|
104 | 109 |
end |
105 | 110 |
|
106 | 111 |
def create |
107 |
- @agent = Agent.build_for_type(params[:agent].delete(:type), |
|
108 |
- current_user, |
|
109 |
- params[:agent]) |
|
112 |
+ build_agent |
|
113 |
+ |
|
110 | 114 |
respond_to do |format| |
111 | 115 |
if @agent.save |
112 | 116 |
format.html { redirect_back "'#{@agent.name}' was successfully created." } |
113 | 117 |
format.json { render json: @agent, status: :ok, location: agent_path(@agent) } |
114 | 118 |
else |
119 |
+ initialize_presenter |
|
115 | 120 |
format.html { render action: "new" } |
116 | 121 |
format.json { render json: @agent.errors, status: :unprocessable_entity } |
117 | 122 |
end |
@@ -126,6 +131,7 @@ class AgentsController < ApplicationController |
||
126 | 131 |
format.html { redirect_back "'#{@agent.name}' was successfully updated." } |
127 | 132 |
format.json { render json: @agent, status: :ok, location: agent_path(@agent) } |
128 | 133 |
else |
134 |
+ initialize_presenter |
|
129 | 135 |
format.html { render action: "edit" } |
130 | 136 |
format.json { render json: @agent.errors, status: :unprocessable_entity } |
131 | 137 |
end |
@@ -153,6 +159,22 @@ class AgentsController < ApplicationController |
||
153 | 159 |
end |
154 | 160 |
end |
155 | 161 |
|
162 |
+ def validate |
|
163 |
+ build_agent |
|
164 |
+ |
|
165 |
+ if @agent.validate_option(params[:attribute]) |
|
166 |
+ render text: 'ok' |
|
167 |
+ else |
|
168 |
+ render text: 'error', status: 403 |
|
169 |
+ end |
|
170 |
+ end |
|
171 |
+ |
|
172 |
+ def complete |
|
173 |
+ build_agent |
|
174 |
+ |
|
175 |
+ render json: @agent.complete_option(params[:attribute]) |
|
176 |
+ end |
|
177 |
+ |
|
156 | 178 |
protected |
157 | 179 |
|
158 | 180 |
# Sanitize params[:return] to prevent open redirect attacks, a common security issue. |
@@ -167,4 +189,16 @@ class AgentsController < ApplicationController |
||
167 | 189 |
|
168 | 190 |
redirect_to path, notice: message |
169 | 191 |
end |
192 |
+ |
|
193 |
+ def build_agent |
|
194 |
+ @agent = Agent.build_for_type(params[:agent].delete(:type), |
|
195 |
+ current_user, |
|
196 |
+ params[:agent]) |
|
197 |
+ end |
|
198 |
+ |
|
199 |
+ def initialize_presenter |
|
200 |
+ if @agent.present? && @agent.is_form_configurable? |
|
201 |
+ @agent = FormConfigurableAgentPresenter.new(@agent, view_context) |
|
202 |
+ end |
|
203 |
+ end |
|
170 | 204 |
end |
@@ -88,6 +88,10 @@ class Agent < ActiveRecord::Base |
||
88 | 88 |
# Implement me in your subclass of Agent. |
89 | 89 |
end |
90 | 90 |
|
91 |
+ def is_form_configurable? |
|
92 |
+ false |
|
93 |
+ end |
|
94 |
+ |
|
91 | 95 |
def receive_web_request(params, method, format) |
92 | 96 |
# Implement me in your subclass of Agent. |
93 | 97 |
["not implemented", 404] |
@@ -1,21 +1,16 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class BasecampAgent < Agent |
3 |
- cannot_receive_events! |
|
4 |
- |
|
3 |
+ include FormConfigurable |
|
5 | 4 |
include Oauthable |
6 | 5 |
valid_oauth_providers :'37signals' |
7 | 6 |
|
7 |
+ cannot_receive_events! |
|
8 |
+ |
|
8 | 9 |
description <<-MD |
9 | 10 |
The BasecampAgent checks a Basecamp project for new Events |
10 | 11 |
|
11 | 12 |
To be able to use this Agent you need to authenticate with 37signals in the [Services](/services) section first. |
12 | 13 |
|
13 |
- You need to provide the `project_id` of the project you want to monitor. |
|
14 |
- If you have your Basecamp project opened in your browser you can find the user_id and project_id as follows: |
|
15 |
- |
|
16 |
- `https://basecamp.com/123456/projects/` |
|
17 |
- project_id |
|
18 |
- `-explore-basecamp` |
|
19 | 14 |
MD |
20 | 15 |
|
21 | 16 |
event_description <<-MD |
@@ -50,6 +45,14 @@ module Agents |
||
50 | 45 |
} |
51 | 46 |
end |
52 | 47 |
|
48 |
+ form_configurable :project_id, roles: :completable |
|
49 |
+ |
|
50 |
+ def complete_project_id |
|
51 |
+ service.prepare_request |
|
52 |
+ response = HTTParty.get projects_url, request_options.merge(query_parameters) |
|
53 |
+ response.map { |p| {text: "#{p['name']} (#{p['id']})", id: p['id']}} |
|
54 |
+ end |
|
55 |
+ |
|
53 | 56 |
def validate_options |
54 | 57 |
errors.add(:base, "you need to specify the basecamp project id of which you want to receive events") unless options['project_id'].present? |
55 | 58 |
end |
@@ -60,8 +63,8 @@ module Agents |
||
60 | 63 |
|
61 | 64 |
def check |
62 | 65 |
service.prepare_request |
63 |
- reponse = HTTParty.get request_url, request_options.merge(query_parameters) |
|
64 |
- events = JSON.parse(reponse.body) |
|
66 |
+ response = HTTParty.get events_url, request_options.merge(query_parameters) |
|
67 |
+ events = JSON.parse(response.body) |
|
65 | 68 |
if !memory[:last_event].nil? |
66 | 69 |
events.each do |event| |
67 | 70 |
create_event :payload => event |
@@ -72,8 +75,16 @@ module Agents |
||
72 | 75 |
end |
73 | 76 |
|
74 | 77 |
private |
75 |
- def request_url |
|
76 |
- "https://basecamp.com/#{URI.encode(service.options[:user_id].to_s)}/api/v1/projects/#{URI.encode(interpolated[:project_id].to_s)}/events.json" |
|
78 |
+ def base_url |
|
79 |
+ "https://basecamp.com/#{URI.encode(service.options[:user_id].to_s)}/api/v1/" |
|
80 |
+ end |
|
81 |
+ |
|
82 |
+ def events_url |
|
83 |
+ base_url + "projects/#{URI.encode(interpolated[:project_id].to_s)}/events.json" |
|
84 |
+ end |
|
85 |
+ |
|
86 |
+ def projects_url |
|
87 |
+ base_url + "projects.json" |
|
77 | 88 |
end |
78 | 89 |
|
79 | 90 |
def request_options |
@@ -1,5 +1,7 @@ |
||
1 | 1 |
module Agents |
2 | 2 |
class HipchatAgent < Agent |
3 |
+ include FormConfigurable |
|
4 |
+ |
|
3 | 5 |
cannot_be_scheduled! |
4 | 6 |
cannot_create_events! |
5 | 7 |
|
@@ -15,8 +17,10 @@ module Agents |
||
15 | 17 |
|
16 | 18 |
Change the `room_name` to the name of the room you want to send notifications to. |
17 | 19 |
|
18 |
- You can provide a `username` and a `message`. When sending a HTML formatted message change `format` to "html". |
|
19 |
- If you want your message to notify the room members change `notify` to "true". |
|
20 |
+ You can provide a `username` and a `message`. If you want to use mentions change `format` to "text" ([details](https://www.hipchat.com/docs/api/method/rooms/message)). |
|
21 |
+ |
|
22 |
+ If you want your message to notify the room members change `notify` to "True". |
|
23 |
+ |
|
20 | 24 |
Modify the background color of your message via the `color` attribute (one of "yellow", "red", "green", "purple", "gray", or "random") |
21 | 25 |
|
22 | 26 |
Have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating. |
@@ -30,9 +34,29 @@ module Agents |
||
30 | 34 |
'message' => "Hello from Huginn!", |
31 | 35 |
'notify' => false, |
32 | 36 |
'color' => 'yellow', |
37 |
+ 'format' => 'html' |
|
33 | 38 |
} |
34 | 39 |
end |
35 | 40 |
|
41 |
+ form_configurable :auth_token, roles: :validatable |
|
42 |
+ form_configurable :room_name, roles: :completable |
|
43 |
+ form_configurable :username |
|
44 |
+ form_configurable :message, type: :text |
|
45 |
+ form_configurable :notify, type: :boolean |
|
46 |
+ form_configurable :color, type: :array, values: ['yellow', 'red', 'green', 'purple', 'gray', 'random'] |
|
47 |
+ form_configurable :format, type: :array, values: ['html', 'text'] |
|
48 |
+ |
|
49 |
+ def validate_auth_token |
|
50 |
+ client.rooms |
|
51 |
+ true |
|
52 |
+ rescue HipChat::UnknownResponseCode |
|
53 |
+ return false |
|
54 |
+ end |
|
55 |
+ |
|
56 |
+ def complete_room_name |
|
57 |
+ client.rooms.collect { |room| {text: room.name, id: room.name} } |
|
58 |
+ end |
|
59 |
+ |
|
36 | 60 |
def validate_options |
37 | 61 |
errors.add(:base, "you need to specify a hipchat auth_token or provide a credential named hipchat_auth_token") unless options['auth_token'].present? || credential('hipchat_auth_token').present? |
38 | 62 |
errors.add(:base, "you need to specify a room_name or a room_name_path") if options['room_name'].blank? && options['room_name_path'].blank? |
@@ -45,12 +69,17 @@ module Agents |
||
45 | 69 |
def receive(incoming_events) |
46 | 70 |
incoming_events.each do |event| |
47 | 71 |
mo = interpolated(event) |
48 |
- client[mo[:room_name]].send(mo[:username][0..14], mo[:message], :notify => boolify(mo[:notify]), :color => mo[:color]) |
|
72 |
+ client[mo[:room_name]].send(mo[:username][0..14], mo[:message], |
|
73 |
+ notify: boolify(mo[:notify]), |
|
74 |
+ color: mo[:color], |
|
75 |
+ message_format: mo[:format].presence || 'html' |
|
76 |
+ ) |
|
49 | 77 |
end |
50 | 78 |
end |
51 | 79 |
|
80 |
+ private |
|
52 | 81 |
def client |
53 |
- @client ||= HipChat::Client.new(interpolated[:auth_token] || credential('hipchat_auth_token')) |
|
82 |
+ @client ||= HipChat::Client.new(interpolated[:auth_token].presence || credential('hipchat_auth_token')) |
|
54 | 83 |
end |
55 | 84 |
end |
56 | 85 |
end |
@@ -0,0 +1,44 @@ |
||
1 |
+require 'delegate' |
|
2 |
+ |
|
3 |
+class Decorator < SimpleDelegator |
|
4 |
+ def class |
|
5 |
+ __getobj__.class |
|
6 |
+ end |
|
7 |
+end |
|
8 |
+ |
|
9 |
+class FormConfigurableAgentPresenter < Decorator |
|
10 |
+ def initialize(agent, view) |
|
11 |
+ @agent = agent |
|
12 |
+ @view = view |
|
13 |
+ super(agent) |
|
14 |
+ end |
|
15 |
+ |
|
16 |
+ def option_field_for(attribute) |
|
17 |
+ data = @agent.form_configurable_fields[attribute] |
|
18 |
+ value = @agent.options[attribute.to_s] || @agent.default_options[attribute.to_s] |
|
19 |
+ html_options = {role: (data[:roles] + ['form-configurable']).join(' '), data: {attribute: attribute}} |
|
20 |
+ |
|
21 |
+ case data[:type] |
|
22 |
+ when :text |
|
23 |
+ @view.text_area_tag "agent[options][#{attribute}]", value, html_options.merge(class: 'form-control', rows: 3) |
|
24 |
+ when :boolean |
|
25 |
+ @view.content_tag 'div' do |
|
26 |
+ @view.concat(@view.content_tag('label', class: 'radio-inline') do |
|
27 |
+ @view.concat @view.radio_button_tag "agent[options][#{attribute}_radio]", 'true', @agent.send(:boolify, value) == true, html_options |
|
28 |
+ @view.concat "True" |
|
29 |
+ end) |
|
30 |
+ @view.concat(@view.content_tag('label', class: 'radio-inline') do |
|
31 |
+ @view.concat @view.radio_button_tag "agent[options][#{attribute}_radio]", 'false', @agent.send(:boolify, value) == false, html_options |
|
32 |
+ @view.concat "False" |
|
33 |
+ end) |
|
34 |
+ @view.concat(@view.content_tag('label', class: 'radio-inline') do |
|
35 |
+ @view.concat @view.radio_button_tag "agent[options][#{attribute}_radio]", 'manual', @agent.send(:boolify, value) == nil, html_options |
|
36 |
+ @view.concat "Manual Input" |
|
37 |
+ end) |
|
38 |
+ @view.concat(@view.text_field_tag "agent[options][#{attribute}]", value, html_options.merge(:class => "form-control #{@agent.send(:boolify, value) != nil ? 'hidden' : ''}")) |
|
39 |
+ end |
|
40 |
+ when :array, :string |
|
41 |
+ @view.text_field_tag "agent[options][#{attribute}]", value, html_options.merge(:class => 'form-control') |
|
42 |
+ end |
|
43 |
+ end |
|
44 |
+end |
@@ -16,8 +16,7 @@ |
||
16 | 16 |
<div class="col-md-6"> |
17 | 17 |
<div class="row"> |
18 | 18 |
|
19 |
- <!-- Form controls width restricted --> |
|
20 |
- <div class="col-md-8"> |
|
19 |
+ <div class="col-md-12"> |
|
21 | 20 |
<% if @agent.new_record? %> |
22 | 21 |
<div class="form-group type-select"> |
23 | 22 |
<%= f.label :type %> |
@@ -27,7 +26,7 @@ |
||
27 | 26 |
</div> |
28 | 27 |
|
29 | 28 |
<div class="agent-settings"> |
30 |
- <div class="col-md-8"> |
|
29 |
+ <div class="col-md-12"> |
|
31 | 30 |
<div class="form-group"> |
32 | 31 |
<%= f.label :name %> |
33 | 32 |
<%= f.text_field :name, :class => 'form-control' %> |
@@ -105,19 +104,8 @@ |
||
105 | 104 |
|
106 | 105 |
</div> |
107 | 106 |
|
108 |
- <!-- Form controls full width --> |
|
109 |
- <div class="col-md-12"> |
|
110 |
- <div class="form-group"> |
|
111 |
- <%= f.label :options %> |
|
112 |
- <span class="glyphicon glyphicon-question-sign hover-help" data-content="In this JSON hash, interpolation is available in almost all values using the Liquid templating language.<p>Available template variables include the following:<dl><dt><code>message</code>, <code>url</code>, etc.</dt><dd>Refers to the corresponding key's value of each incoming event's payload.</dd><dt><code>agent</code></dt><dd>Refers to the agent that created each incoming event. It has attributes like <code>type</code>, <code>name</code> and <code>options</code>, so <code>{{agent.type}}</code> will expand to <code>WebsiteAgent</code> if an incoming event is created by that agent.</dd></dl></p><p>To access user credentials, use the <code>credential</code> tag like this: <code>{% credential <em>bare_key_name</em> %}</code></p>"></span> |
|
113 |
- <textarea rows="15" id="agent_options" name="agent[options]" class="form-control live-json-editor"> |
|
114 |
- <%= Utils.jsonify((@agent.new_record? && @agent.options == {}) ? @agent.default_options : @agent.options) %> |
|
115 |
- </textarea> |
|
116 |
- </div> |
|
117 |
- |
|
118 |
- <div class="form-group"> |
|
119 |
- <%= f.submit "Save", :class => "btn btn-primary" %> |
|
120 |
- </div> |
|
107 |
+ <div class="col-md-12 agent-options"> |
|
108 |
+ <%= render partial: 'options', locals: { agent: @agent } %> |
|
121 | 109 |
</div> |
122 | 110 |
</div> |
123 | 111 |
</div> |
@@ -0,0 +1,27 @@ |
||
1 |
+<% if agent.is_form_configurable? %> |
|
2 |
+ <fieldset> |
|
3 |
+ <% if agent.persisted? %> |
|
4 |
+ <%= hidden_field_tag 'agent[type]', @agent.type %> |
|
5 |
+ <% end %> |
|
6 |
+ <legend>Options</legend> |
|
7 |
+ <% agent.form_configurable_attributes.each do |attribute| %> |
|
8 |
+ <div class="form-group"> |
|
9 |
+ <%= label_tag attribute %> |
|
10 |
+ <%= agent.option_field_for(attribute) %> |
|
11 |
+ <span class="glyphicon glyphicon-ok form-control-feedback hidden"></span> |
|
12 |
+ <span class="glyphicon glyphicon-remove form-control-feedback hidden"></span> |
|
13 |
+ </div> |
|
14 |
+ <% end %> |
|
15 |
+ </fieldset> |
|
16 |
+<% else %> |
|
17 |
+ <div class="form-group"> |
|
18 |
+ <%= label_tag :options %> |
|
19 |
+ <span class="glyphicon glyphicon-question-sign hover-help" data-content="In this JSON hash, interpolation is available in almost all values using the Liquid templating language.<p>Available template variables include the following:<dl><dt><code>message</code>, <code>url</code>, etc.</dt><dd>Refers to the corresponding key's value of each incoming event's payload.</dd><dt><code>agent</code></dt><dd>Refers to the agent that created each incoming event. It has attributes like <code>type</code>, <code>name</code> and <code>options</code>, so <code>{{agent.type}}</code> will expand to <code>WebsiteAgent</code> if an incoming event is created by that agent.</dd></dl></p><p>To access user credentials, use the <code>credential</code> tag like this: <code>{% credential <em>bare_key_name</em> %}</code></p>"></span> |
|
20 |
+ <textarea rows="15" id="agent_options" name="agent[options]" class="form-control live-json-editor"> |
|
21 |
+ <%= Utils.jsonify((agent.new_record? && agent.options == {}) ? agent.default_options : agent.options) %> |
|
22 |
+ </textarea> |
|
23 |
+ </div> |
|
24 |
+<% end %> |
|
25 |
+<div class="form-group"> |
|
26 |
+ <%= submit_tag "Save", :class => "btn btn-primary" %> |
|
27 |
+</div> |
@@ -9,8 +9,9 @@ |
||
9 | 9 |
</div> |
10 | 10 |
</div> |
11 | 11 |
</div> |
12 |
- |
|
13 |
- <%= render 'form' %> |
|
12 |
+ <div id="agent-form"> |
|
13 |
+ <%= render 'form' %> |
|
14 |
+ </div> |
|
14 | 15 |
|
15 | 16 |
<hr> |
16 | 17 |
|
@@ -8,7 +8,9 @@ |
||
8 | 8 |
</h2> |
9 | 9 |
</div> |
10 | 10 |
|
11 |
- <%= render 'form' %> |
|
11 |
+ <div id="agent-form"> |
|
12 |
+ <%= render 'form' %> |
|
13 |
+ </div> |
|
12 | 14 |
|
13 | 15 |
<hr> |
14 | 16 |
|
@@ -13,7 +13,7 @@ module Huginn |
||
13 | 13 |
# -- all .rb files in that directory are automatically loaded. |
14 | 14 |
|
15 | 15 |
# Custom directories with classes and modules you want to be autoloadable. |
16 |
- config.autoload_paths += %W(#{config.root}/lib) |
|
16 |
+ config.autoload_paths += %W(#{config.root}/lib #{config.root}/app/presenters) |
|
17 | 17 |
|
18 | 18 |
# Activate observers that should always be running. |
19 | 19 |
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer |
@@ -11,6 +11,8 @@ Huginn::Application.routes.draw do |
||
11 | 11 |
post :propagate |
12 | 12 |
get :type_details |
13 | 13 |
get :event_descriptions |
14 |
+ post :validate |
|
15 |
+ post :complete |
|
14 | 16 |
end |
15 | 17 |
|
16 | 18 |
resources :logs, :only => [:index] do |
@@ -0,0 +1,56 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe FormConfigurable do |
|
4 |
+ class Agent1 |
|
5 |
+ include FormConfigurable |
|
6 |
+ |
|
7 |
+ def validate_test |
|
8 |
+ true |
|
9 |
+ end |
|
10 |
+ |
|
11 |
+ def complete_test |
|
12 |
+ [{name: 'test', value: 1234}] |
|
13 |
+ end |
|
14 |
+ end |
|
15 |
+ |
|
16 |
+ class Agent2 < Agent |
|
17 |
+ end |
|
18 |
+ |
|
19 |
+ before(:all) do |
|
20 |
+ @agent1 = Agent1.new |
|
21 |
+ @agent2 = Agent2.new |
|
22 |
+ end |
|
23 |
+ |
|
24 |
+ it "#is_form_configurable" do |
|
25 |
+ expect(@agent1.is_form_configurable?).to be true |
|
26 |
+ expect(@agent2.is_form_configurable?).to be false |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ describe "#validete_option" do |
|
30 |
+ it "should call the validation method if it is defined" do |
|
31 |
+ expect(@agent1.validate_option('test')).to be true |
|
32 |
+ end |
|
33 |
+ |
|
34 |
+ it "should return false of the method is undefined" do |
|
35 |
+ expect(@agent1.validate_option('undefined')).to be false |
|
36 |
+ end |
|
37 |
+ end |
|
38 |
+ |
|
39 |
+ it "#complete_option" do |
|
40 |
+ expect(@agent1.complete_option('test')).to eq [{name: 'test', value: 1234}] |
|
41 |
+ end |
|
42 |
+ |
|
43 |
+ describe "#form_configurable" do |
|
44 |
+ it "should raise an ArgumentError for invalid options" do |
|
45 |
+ expect { Agent1.form_configurable(:test, invalid: true) }.to raise_error(ArgumentError) |
|
46 |
+ end |
|
47 |
+ |
|
48 |
+ it "should raise an ArgumentError when not providing an array with type: array" do |
|
49 |
+ expect { Agent1.form_configurable(:test, type: :array, values: 1) }.to raise_error(ArgumentError) |
|
50 |
+ end |
|
51 |
+ |
|
52 |
+ it "should not require any options for the default values" do |
|
53 |
+ expect { Agent1.form_configurable(:test) }.to change(Agent1, :form_configurable_attributes).by(['test']) |
|
54 |
+ end |
|
55 |
+ end |
|
56 |
+end |
@@ -307,4 +307,44 @@ describe AgentsController do |
||
307 | 307 |
expect(response).to redirect_to scenario_path(scenarios(:bob_weather)) |
308 | 308 |
end |
309 | 309 |
end |
310 |
+ |
|
311 |
+ describe "#form_configurable actions" do |
|
312 |
+ before(:each) do |
|
313 |
+ @params = {attribute: 'auth_token', agent: valid_attributes(:type => "Agents::HipchatAgent", options: {auth_token: '12345'})} |
|
314 |
+ sign_in users(:bob) |
|
315 |
+ end |
|
316 |
+ describe "POST validate" do |
|
317 |
+ |
|
318 |
+ it "returns with status 200 when called with a valid option" do |
|
319 |
+ any_instance_of(Agents::HipchatAgent) do |klass| |
|
320 |
+ stub(klass).validate_option { true } |
|
321 |
+ end |
|
322 |
+ |
|
323 |
+ post :validate, @params |
|
324 |
+ expect(response.status).to eq 200 |
|
325 |
+ end |
|
326 |
+ |
|
327 |
+ it "returns with status 403 when called with an invalid option" do |
|
328 |
+ any_instance_of(Agents::HipchatAgent) do |klass| |
|
329 |
+ stub(klass).validate_option { false } |
|
330 |
+ end |
|
331 |
+ |
|
332 |
+ post :validate, @params |
|
333 |
+ expect(response.status).to eq 403 |
|
334 |
+ end |
|
335 |
+ end |
|
336 |
+ |
|
337 |
+ describe "POST complete" do |
|
338 |
+ it "callsAgent#complete_option and renders json" do |
|
339 |
+ any_instance_of(Agents::HipchatAgent) do |klass| |
|
340 |
+ stub(klass).complete_option { [{name: 'test', value: 1}] } |
|
341 |
+ end |
|
342 |
+ |
|
343 |
+ post :complete, @params |
|
344 |
+ expect(response.status).to eq 200 |
|
345 |
+ expect(response.header['Content-Type']).to include('application/json') |
|
346 |
+ |
|
347 |
+ end |
|
348 |
+ end |
|
349 |
+ end |
|
310 | 350 |
end |
@@ -5,8 +5,21 @@ describe Agents::BasecampAgent do |
||
5 | 5 |
it_behaves_like Oauthable |
6 | 6 |
|
7 | 7 |
before(:each) do |
8 |
- stub_request(:get, /json$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"}) |
|
9 |
- stub_request(:get, /02:00$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"}) |
|
8 |
+ stub_request(:get, /events.json$/).to_return( |
|
9 |
+ :body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), |
|
10 |
+ :status => 200, |
|
11 |
+ :headers => {"Content-Type" => "text/json"} |
|
12 |
+ ) |
|
13 |
+ stub_request(:get, /projects.json$/).to_return( |
|
14 |
+ :body => JSON.dump([{name: 'test', id: 1234},{name: 'test1', id: 1235}]), |
|
15 |
+ :status => 200, |
|
16 |
+ :headers => {"Content-Type" => "text/json"} |
|
17 |
+ ) |
|
18 |
+ stub_request(:get, /02:00$/).to_return( |
|
19 |
+ :body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), |
|
20 |
+ :status => 200, |
|
21 |
+ :headers => {"Content-Type" => "text/json"} |
|
22 |
+ ) |
|
10 | 23 |
@valid_params = { :project_id => 6789 } |
11 | 24 |
|
12 | 25 |
@checker = Agents::BasecampAgent.new(:name => "somename", :options => @valid_params) |
@@ -32,10 +45,13 @@ describe Agents::BasecampAgent do |
||
32 | 45 |
expect(@checker.send(:request_options)).to eq({:headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)", "Authorization" => 'Bearer "1234token"'}}) |
33 | 46 |
end |
34 | 47 |
|
35 |
- it "should generate the currect request url" do |
|
36 |
- expect(@checker.send(:request_url)).to eq("https://basecamp.com/12345/api/v1/projects/6789/events.json") |
|
48 |
+ it "should generate the correct events url" do |
|
49 |
+ expect(@checker.send(:events_url)).to eq("https://basecamp.com/12345/api/v1/projects/6789/events.json") |
|
37 | 50 |
end |
38 | 51 |
|
52 |
+ it "should generate the correct projects url" do |
|
53 |
+ expect(@checker.send(:projects_url)).to eq("https://basecamp.com/12345/api/v1/projects.json") |
|
54 |
+ end |
|
39 | 55 |
|
40 | 56 |
it "should not provide the since attribute on first run" do |
41 | 57 |
expect(@checker.send(:query_parameters)).to eq({}) |
@@ -48,6 +64,13 @@ describe Agents::BasecampAgent do |
||
48 | 64 |
expect(@checker.reload.send(:query_parameters)).to eq({:query => {:since => time}}) |
49 | 65 |
end |
50 | 66 |
end |
67 |
+ |
|
68 |
+ describe "#complete_project_id" do |
|
69 |
+ it "should return a array of hashes" do |
|
70 |
+ expect(@checker.complete_project_id).to eq [{text: 'test (1234)', id: 1234}, {text: 'test1 (1235)', id: 1235}] |
|
71 |
+ end |
|
72 |
+ end |
|
73 |
+ |
|
51 | 74 |
describe "#check" do |
52 | 75 |
it "should not emit events on its first run" do |
53 | 76 |
expect { @checker.check }.to change { Event.count }.by(0) |
@@ -60,7 +83,7 @@ describe Agents::BasecampAgent do |
||
60 | 83 |
end |
61 | 84 |
|
62 | 85 |
describe "#working?" do |
63 |
- it "it is working when at least one event was emited" do |
|
86 |
+ it "it is working when at least one event was emitted" do |
|
64 | 87 |
expect(@checker).not_to be_working |
65 | 88 |
@checker.memory[:last_event] = '2014-04-17T10:25:31.000+02:00' |
66 | 89 |
@checker.check |
@@ -50,6 +50,31 @@ describe Agents::HipchatAgent do |
||
50 | 50 |
end |
51 | 51 |
end |
52 | 52 |
|
53 |
+ describe "#validate_auth_token" do |
|
54 |
+ it "should return true when valid" do |
|
55 |
+ any_instance_of(HipChat::Client) do |klass| |
|
56 |
+ stub(klass).rooms { true } |
|
57 |
+ end |
|
58 |
+ expect(@checker.validate_auth_token).to be true |
|
59 |
+ end |
|
60 |
+ |
|
61 |
+ it "should return false when invalid" do |
|
62 |
+ any_instance_of(HipChat::Client) do |klass| |
|
63 |
+ stub(klass).rooms { raise HipChat::UnknownResponseCode.new } |
|
64 |
+ end |
|
65 |
+ expect(@checker.validate_auth_token).to be false |
|
66 |
+ end |
|
67 |
+ end |
|
68 |
+ |
|
69 |
+ describe "#complete_room_name" do |
|
70 |
+ it "should return a array of hashes" do |
|
71 |
+ any_instance_of(HipChat::Client) do |klass| |
|
72 |
+ stub(klass).rooms { [OpenStruct.new(name: 'test'), OpenStruct.new(name: 'test1')] } |
|
73 |
+ end |
|
74 |
+ expect(@checker.complete_room_name).to eq [{text: 'test', id: 'test'},{text: 'test1', id: 'test1'}] |
|
75 |
+ end |
|
76 |
+ end |
|
77 |
+ |
|
53 | 78 |
describe "#receive" do |
54 | 79 |
it "send a message to the hipchat" do |
55 | 80 |
any_instance_of(HipChat::Room) do |obj| |
@@ -15,7 +15,7 @@ describe Service do |
||
15 | 15 |
expect(@service.global).to eq(false) |
16 | 16 |
end |
17 | 17 |
|
18 |
- it "disconnects agents and disables them if the previously global service is made private again", focus: true do |
|
18 |
+ it "disconnects agents and disables them if the previously global service is made private again" do |
|
19 | 19 |
agent = agents(:bob_basecamp_agent) |
20 | 20 |
jane_agent = agents(:jane_basecamp_agent) |
21 | 21 |
|
@@ -0,0 +1,40 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe FormConfigurableAgentPresenter do |
|
4 |
+ class FormConfigurableAgentPresenterAgent < Agent |
|
5 |
+ include FormConfigurable |
|
6 |
+ |
|
7 |
+ form_configurable :string, roles: :validatable |
|
8 |
+ form_configurable :text, type: :text, roles: :completable |
|
9 |
+ form_configurable :boolean, type: :boolean |
|
10 |
+ form_configurable :array, type: :array, values: [1, 2, 3] |
|
11 |
+ end |
|
12 |
+ |
|
13 |
+ before(:all) do |
|
14 |
+ @presenter = FormConfigurableAgentPresenter.new(FormConfigurableAgentPresenterAgent.new, ActionController::Base.new.view_context) |
|
15 |
+ end |
|
16 |
+ |
|
17 |
+ it "works for the type :string" do |
|
18 |
+ expect(@presenter.option_field_for(:string)).to( |
|
19 |
+ have_tag('input', with: {:'data-attribute' => 'string', role: 'validatable form-configurable', type: 'text', name: 'agent[options][string]'}) |
|
20 |
+ ) |
|
21 |
+ end |
|
22 |
+ |
|
23 |
+ it "works for the type :text" do |
|
24 |
+ expect(@presenter.option_field_for(:text)).to( |
|
25 |
+ have_tag('textarea', with: {:'data-attribute' => 'text', role: 'completable form-configurable', name: 'agent[options][text]'}) |
|
26 |
+ ) |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ it "works for the type :boolean" do |
|
30 |
+ expect(@presenter.option_field_for(:boolean)).to( |
|
31 |
+ have_tag('input', with: {:'data-attribute' => 'boolean', role: 'form-configurable', name: 'agent[options][boolean_radio]', type: 'radio'}) |
|
32 |
+ ) |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ it "works for the type :array" do |
|
36 |
+ expect(@presenter.option_field_for(:array)).to( |
|
37 |
+ have_tag('input', with: {:'data-attribute' => 'array', role: 'completable form-configurable', type: 'text', name: 'agent[options][array]'}) |
|
38 |
+ ) |
|
39 |
+ end |
|
40 |
+end |
@@ -0,0 +1,40 @@ |
||
1 |
+// |
|
2 |
+// Use internal $.serializeArray to get list of form elements which is |
|
3 |
+// consistent with $.serialize |
|
4 |
+// |
|
5 |
+// From version 2.0.0, $.serializeObject will stop converting [name] values |
|
6 |
+// to camelCase format. This is *consistent* with other serialize methods: |
|
7 |
+// |
|
8 |
+// - $.serialize |
|
9 |
+// - $.serializeArray |
|
10 |
+// |
|
11 |
+// If you require camel casing, you can either download version 1.0.4 or map |
|
12 |
+// them yourself. |
|
13 |
+// |
|
14 |
+ |
|
15 |
+(function($){ |
|
16 |
+ $.fn.serializeObject = function () { |
|
17 |
+ "use strict"; |
|
18 |
+ |
|
19 |
+ var result = {}; |
|
20 |
+ var extend = function (i, element) { |
|
21 |
+ var node = result[element.name]; |
|
22 |
+ |
|
23 |
+ // If node with same name exists already, need to convert it to an array as it |
|
24 |
+ // is a multi-value field (i.e., checkboxes) |
|
25 |
+ |
|
26 |
+ if ('undefined' !== typeof node && node !== null) { |
|
27 |
+ if ($.isArray(node)) { |
|
28 |
+ node.push(element.value); |
|
29 |
+ } else { |
|
30 |
+ result[element.name] = [node, element.value]; |
|
31 |
+ } |
|
32 |
+ } else { |
|
33 |
+ result[element.name] = element.value; |
|
34 |
+ } |
|
35 |
+ }; |
|
36 |
+ |
|
37 |
+ $.each(this.serializeArray(), extend); |
|
38 |
+ return result; |
|
39 |
+ }; |
|
40 |
+})(jQuery); |