@@ -59,6 +59,7 @@ gem 'faraday', '~> 0.9.0' |
||
59 | 59 |
gem 'faraday_middleware' |
60 | 60 |
gem 'typhoeus', '~> 0.6.3' |
61 | 61 |
gem 'nokogiri', '~> 1.6.1' |
62 |
+gem 'net-ftp-list', '~> 3.2.8' |
|
62 | 63 |
|
63 | 64 |
gem 'wunderground', '~> 1.2.0' |
64 | 65 |
gem 'forecast_io', '~> 2.0.0' |
@@ -188,6 +188,7 @@ GEM |
||
188 | 188 |
multipart-post (2.0.0) |
189 | 189 |
mysql2 (0.3.16) |
190 | 190 |
naught (1.0.0) |
191 |
+ net-ftp-list (3.2.8) |
|
191 | 192 |
nokogiri (1.6.3.1) |
192 | 193 |
mini_portile (= 0.6.0) |
193 | 194 |
oauth (0.4.7) |
@@ -418,6 +419,7 @@ DEPENDENCIES |
||
418 | 419 |
liquid (~> 2.6.1) |
419 | 420 |
mqtt |
420 | 421 |
mysql2 (~> 0.3.16) |
422 |
+ net-ftp-list (~> 3.2.8) |
|
421 | 423 |
nokogiri (~> 1.6.1) |
422 | 424 |
omniauth |
423 | 425 |
omniauth-37signals |
@@ -27,6 +27,8 @@ Follow [@tectonic](https://twitter.com/tectonic) for updates as Huginn evolves, |
||
27 | 27 |
|
28 | 28 |
Want to help with Huginn? All contributions are encouraged! You could make UI improvements, add new Agents, write documentation and tutorials, or try tackling [issues tagged with #help-wanted](https://github.com/cantino/huginn/issues?direction=desc&labels=help-wanted&page=1&sort=created&state=open). |
29 | 29 |
|
30 |
+Really want an issue fixed/feature implemented? Or maybe you just want to solve some community issues and earn some extra coffee money? Then you should take a look at the [current bounties on Bountysource](https://www.bountysource.com/trackers/282580-huginn). |
|
31 |
+ |
|
30 | 32 |
Have an awesome an idea but not feeling quite up to contributing yet? Head over to our [Official 'suggest an agent' thread ](https://github.com/cantino/huginn/issues/353) and tell us about your cool idea! |
31 | 33 |
|
32 | 34 |
## Examples |
@@ -105,5 +107,5 @@ Huginn is a work in progress and is just getting started. Please get involved! |
||
105 | 107 |
|
106 | 108 |
Please fork, add specs, and send pull requests! |
107 | 109 |
|
108 |
-[](https://travis-ci.org/cantino/huginn) [](https://coveralls.io/r/cantino/huginn) [](https://bitdeli.com/free "Bitdeli Badge") [](https://gemnasium.com/cantino/huginn) |
|
110 |
+[](https://travis-ci.org/cantino/huginn) [](https://coveralls.io/r/cantino/huginn) [](https://bitdeli.com/free "Bitdeli Badge") [](https://gemnasium.com/cantino/huginn) [](https://www.bountysource.com/trackers/282580-huginn?utm_source=282580&utm_medium=shield&utm_campaign=TRACKER_BADGE) |
|
109 | 111 |
|
@@ -0,0 +1,20 @@ |
||
1 |
+$ -> |
|
2 |
+ svg = document.querySelector('.agent-diagram svg.diagram') |
|
3 |
+ overlay = document.querySelector('.agent-diagram .overlay') |
|
4 |
+ getTopLeft = (node) -> |
|
5 |
+ bbox = node.getBBox() |
|
6 |
+ point = svg.createSVGPoint() |
|
7 |
+ point.x = bbox.x + bbox.width |
|
8 |
+ point.y = bbox.y |
|
9 |
+ point.matrixTransform(node.getCTM()) |
|
10 |
+ $(svg).find('g.node[data-badge-id]').each -> |
|
11 |
+ tl = getTopLeft(this) |
|
12 |
+ $('#' + this.getAttribute('data-badge-id'), overlay).each -> |
|
13 |
+ badge = $(this) |
|
14 |
+ badge.css |
|
15 |
+ left: tl.x - badge.outerWidth() * (2/3) |
|
16 |
+ top: tl.y - badge.outerHeight() * (1/3) |
|
17 |
+ 'background-color': badge.find('.label').css('background-color') |
|
18 |
+ .show() |
|
19 |
+ return |
|
20 |
+ return |
@@ -0,0 +1,30 @@ |
||
1 |
+.agent-diagram { |
|
2 |
+ position: relative; |
|
3 |
+ z-index: auto; |
|
4 |
+ |
|
5 |
+ svg.diagram { |
|
6 |
+ position: absolute; |
|
7 |
+ z-index: 1; |
|
8 |
+ } |
|
9 |
+ |
|
10 |
+ .overlay-container { |
|
11 |
+ position: absolute; |
|
12 |
+ top: 0; |
|
13 |
+ left: 0; |
|
14 |
+ z-index: auto; |
|
15 |
+ |
|
16 |
+ .overlay { |
|
17 |
+ position: relative; |
|
18 |
+ z-index: auto; |
|
19 |
+ width: 100%; |
|
20 |
+ height: 100%; |
|
21 |
+ |
|
22 |
+ .badge { |
|
23 |
+ position: absolute; |
|
24 |
+ display: none; |
|
25 |
+ color: white !important; |
|
26 |
+ z-index: 2; |
|
27 |
+ } |
|
28 |
+ } |
|
29 |
+ } |
|
30 |
+} |
@@ -98,14 +98,6 @@ class AgentsController < ApplicationController |
||
98 | 98 |
@agent = current_user.agents.find(params[:id]) |
99 | 99 |
end |
100 | 100 |
|
101 |
- def diagram |
|
102 |
- @agents = if params[:scenario_id].present? |
|
103 |
- current_user.scenarios.find(params[:scenario_id]).agents.includes(:receivers) |
|
104 |
- else |
|
105 |
- current_user.agents.includes(:receivers) |
|
106 |
- end |
|
107 |
- end |
|
108 |
- |
|
109 | 101 |
def create |
110 | 102 |
@agent = Agent.build_for_type(params[:agent].delete(:type), |
111 | 103 |
current_user, |
@@ -0,0 +1,9 @@ |
||
1 |
+class DiagramsController < ApplicationController |
|
2 |
+ def show |
|
3 |
+ @agents = if params[:scenario_id].present? |
|
4 |
+ current_user.scenarios.find(params[:scenario_id]).agents.includes(:receivers) |
|
5 |
+ else |
|
6 |
+ current_user.agents.includes(:receivers) |
|
7 |
+ end |
|
8 |
+ end |
|
9 |
+end |
@@ -2,8 +2,8 @@ class EventsController < ApplicationController |
||
2 | 2 |
before_filter :load_event, :except => :index |
3 | 3 |
|
4 | 4 |
def index |
5 |
- if params[:agent] |
|
6 |
- @agent = current_user.agents.find(params[:agent]) |
|
5 |
+ if params[:agent_id] |
|
6 |
+ @agent = current_user.agents.find(params[:agent_id]) |
|
7 | 7 |
@events = @agent.events.page(params[:page]) |
8 | 8 |
else |
9 | 9 |
@events = current_user.events.preload(:agent).page(params[:page]) |
@@ -6,7 +6,7 @@ module DotHelper |
||
6 | 6 |
dot.close_write |
7 | 7 |
dot.read |
8 | 8 |
} rescue false) |
9 |
- svg.html_safe |
|
9 |
+ decorate_svg(svg, agents).html_safe |
|
10 | 10 |
else |
11 | 11 |
tag('img', src: URI('https://chart.googleapis.com/chart').tap { |uri| |
12 | 12 |
uri.query = URI.encode_www_form(cht: 'gv', chl: agents_dot(agents)) |
@@ -57,6 +57,13 @@ module DotHelper |
||
57 | 57 |
end |
58 | 58 |
end |
59 | 59 |
|
60 |
+ def ids(values) |
|
61 |
+ values.each_with_index { |id, i| |
|
62 |
+ raw ' ' if i > 0 |
|
63 |
+ id id |
|
64 |
+ } |
|
65 |
+ end |
|
66 |
+ |
|
60 | 67 |
def attr_list(attrs = nil) |
61 | 68 |
return if attrs.nil? |
62 | 69 |
attrs = attrs.select { |key, value| value.present? } |
@@ -86,16 +93,13 @@ module DotHelper |
||
86 | 93 |
end |
87 | 94 |
|
88 | 95 |
def statement(ids, attrs = nil) |
89 |
- Array(ids).each_with_index { |id, i| |
|
90 |
- raw ' ' if i > 0 |
|
91 |
- id id |
|
92 |
- } |
|
96 |
+ ids Array(ids) |
|
93 | 97 |
attr_list attrs |
94 | 98 |
raw ';' |
95 | 99 |
end |
96 | 100 |
|
97 |
- def block(title, &block) |
|
98 |
- raw title |
|
101 |
+ def block(*ids, &block) |
|
102 |
+ ids ids |
|
99 | 103 |
raw '{' |
100 | 104 |
block.call |
101 | 105 |
raw '}' |
@@ -112,11 +116,7 @@ module DotHelper |
||
112 | 116 |
draw(agents: agents, |
113 | 117 |
agent_id: ->agent { 'a%d' % agent.id }, |
114 | 118 |
agent_label: ->agent { |
115 |
- if agent.disabled? |
|
116 |
- '%s (Disabled)' % agent.name |
|
117 |
- else |
|
118 |
- agent.name |
|
119 |
- end.gsub(/(.{20}\S*)\s+/) { |
|
119 |
+ agent.name.gsub(/(.{20}\S*)\s+/) { |
|
120 | 120 |
# Fold after every 20+ characters |
121 | 121 |
$1 + "\n" |
122 | 122 |
} |
@@ -128,6 +128,7 @@ module DotHelper |
||
128 | 128 |
def agent_node(agent) |
129 | 129 |
node(agent_id[agent], |
130 | 130 |
label: agent_label[agent], |
131 |
+ tooltip: (agent.short_type.titleize if rich), |
|
131 | 132 |
URL: (agent_url[agent] if rich), |
132 | 133 |
style: ('rounded,dashed' if agent.disabled?), |
133 | 134 |
color: (@disabled if agent.disabled?), |
@@ -141,7 +142,7 @@ module DotHelper |
||
141 | 142 |
color: (@disabled if agent.disabled? || receiver.disabled?)) |
142 | 143 |
end |
143 | 144 |
|
144 |
- block('digraph foo') { |
|
145 |
+ block('digraph', 'Agent Event Flow') { |
|
145 | 146 |
# statement 'graph', rankdir: 'LR' |
146 | 147 |
statement 'node', |
147 | 148 |
shape: 'box', |
@@ -160,4 +161,60 @@ module DotHelper |
||
160 | 161 |
} |
161 | 162 |
} |
162 | 163 |
end |
164 |
+ |
|
165 |
+ def decorate_svg(xml, agents) |
|
166 |
+ svg = Nokogiri::XML(xml).at('svg') |
|
167 |
+ |
|
168 |
+ Nokogiri::HTML::Document.new.tap { |doc| |
|
169 |
+ doc << root = Nokogiri::XML::Node.new('div', doc) { |div| |
|
170 |
+ div['class'] = 'agent-diagram' |
|
171 |
+ } |
|
172 |
+ |
|
173 |
+ svg['class'] = 'diagram' |
|
174 |
+ |
|
175 |
+ root << svg |
|
176 |
+ root << overlay_container = Nokogiri::XML::Node.new('div', doc) { |div| |
|
177 |
+ div['class'] = 'overlay-container' |
|
178 |
+ div['style'] = "width: #{svg['width']}; height: #{svg['height']}" |
|
179 |
+ } |
|
180 |
+ overlay_container << overlay = Nokogiri::XML::Node.new('div', doc) { |div| |
|
181 |
+ div['class'] = 'overlay' |
|
182 |
+ } |
|
183 |
+ |
|
184 |
+ svg.xpath('//xmlns:g[@class="node"]', svg.namespaces).each { |node| |
|
185 |
+ agent_id = (node.xpath('./xmlns:title/text()', svg.namespaces).to_s[/\d+/] or next).to_i |
|
186 |
+ agent = agents.find { |a| a.id == agent_id } |
|
187 |
+ |
|
188 |
+ count = agent.events_count |
|
189 |
+ next unless count && count > 0 |
|
190 |
+ |
|
191 |
+ overlay << Nokogiri::XML::Node.new('a', doc) { |badge| |
|
192 |
+ badge['id'] = id = 'b%d' % agent_id |
|
193 |
+ badge['class'] = 'badge' |
|
194 |
+ badge['href'] = events_path(agent: agent) |
|
195 |
+ badge['target'] = '_blank' |
|
196 |
+ badge['title'] = "#{count} events created" |
|
197 |
+ badge.content = count.to_s |
|
198 |
+ |
|
199 |
+ node['data-badge-id'] = id |
|
200 |
+ |
|
201 |
+ badge << Nokogiri::XML::Node.new('span', doc) { |label| |
|
202 |
+ # a dummy label only to obtain the background color |
|
203 |
+ label['class'] = [ |
|
204 |
+ 'label', |
|
205 |
+ if agent.disabled? |
|
206 |
+ 'label-warning' |
|
207 |
+ elsif agent.working? |
|
208 |
+ 'label-success' |
|
209 |
+ else |
|
210 |
+ 'label-danger' |
|
211 |
+ end |
|
212 |
+ ].join(' ') |
|
213 |
+ label['style'] = 'display: none'; |
|
214 |
+ } |
|
215 |
+ } |
|
216 |
+ } |
|
217 |
+ # See also: app/assets/diagram.js.coffee |
|
218 |
+ }.at('div.agent-diagram').to_s |
|
219 |
+ end |
|
163 | 220 |
end |
@@ -25,9 +25,14 @@ module Agents |
||
25 | 25 |
|
26 | 26 |
"instructions": { |
27 | 27 |
"message": "Today's conditions look like {{conditions}} with a high temperature of {{high.celsius}} degrees Celsius.", |
28 |
- "subject": "{{data}}" |
|
28 |
+ "subject": "{{data}}", |
|
29 |
+ "created_at": "{{created_at}}" |
|
29 | 30 |
} |
30 | 31 |
|
32 |
+ Names here like `conditions`, `high` and `data` refer to the corresponding values in the Event hash. |
|
33 |
+ |
|
34 |
+ The special key `created_at` refers to the timestamp of the Event, which can be reformatted by the `date` filter, like `{{created_at | date:"at %I:%M %p" }}`. |
|
35 |
+ |
|
31 | 36 |
The upstream agent of each received event is accessible via the key `agent`, which has the following attributes: #{''.tap { |s| s << AgentDrop.instance_methods(false).map { |m| "`#{m}`" }.join(', ') }}. |
32 | 37 |
|
33 | 38 |
Have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating. |
@@ -68,8 +73,6 @@ module Agents |
||
68 | 73 |
|
69 | 74 |
If you want to retain original contents of events and only add new keys, then set `mode` to `merge`, otherwise set it to `clean`. |
70 | 75 |
|
71 |
- By default, the output event will have a `created_at` field added as well, reflecting the original Event creation time. You can skip this output by setting `skip_created_at` to `true`. |
|
72 |
- |
|
73 | 76 |
To CGI escape output (for example when creating a link), use the Liquid `uri_escape` filter, like so: |
74 | 77 |
|
75 | 78 |
{ |
@@ -82,7 +85,7 @@ module Agents |
||
82 | 85 |
after_save :clear_matchers |
83 | 86 |
|
84 | 87 |
def validate_options |
85 |
- errors.add(:base, "instructions, mode, and skip_created_at all need to be present.") unless options['instructions'].present? && options['mode'].present? && options['skip_created_at'].present? |
|
88 |
+ errors.add(:base, "instructions and mode need to be present.") unless options['instructions'].present? && options['mode'].present? |
|
86 | 89 |
|
87 | 90 |
validate_matchers |
88 | 91 |
end |
@@ -96,7 +99,6 @@ module Agents |
||
96 | 99 |
}, |
97 | 100 |
'matchers' => [], |
98 | 101 |
'mode' => "clean", |
99 |
- 'skip_created_at' => "false" |
|
100 | 102 |
} |
101 | 103 |
end |
102 | 104 |
|
@@ -110,7 +112,6 @@ module Agents |
||
110 | 112 |
opts = interpolated(event.to_liquid(payload)) |
111 | 113 |
formatted_event = opts['mode'].to_s == "merge" ? event.payload.dup : {} |
112 | 114 |
formatted_event.merge! opts['instructions'] |
113 |
- formatted_event['created_at'] = event.created_at unless opts['skip_created_at'].to_s == "true" |
|
114 | 115 |
create_event :payload => formatted_event |
115 | 116 |
end |
116 | 117 |
end |
@@ -1,4 +1,5 @@ |
||
1 | 1 |
require 'net/ftp' |
2 |
+require 'net/ftp/list' |
|
2 | 3 |
require 'uri' |
3 | 4 |
require 'time' |
4 | 5 |
|
@@ -105,34 +106,15 @@ module Agents |
||
105 | 106 |
# commands during iteration. |
106 | 107 |
list = ftp.list('-a') |
107 | 108 |
|
108 |
- month2year = {} |
|
109 |
- |
|
110 | 109 |
list.each do |line| |
111 |
- mon, day, smtn, rest = line.split(' ', 9)[5..-1] |
|
112 |
- |
|
113 |
- # Remove symlink target part if any |
|
114 |
- filename = rest[/\A(.+?)(?:\s+->\s|\z)/, 1] |
|
115 |
- |
|
110 |
+ entry = Net::FTP::List.parse line |
|
111 |
+ filename = entry.basename |
|
112 |
+ mtime = Time.parse(entry.mtime.to_s).utc |
|
113 |
+ |
|
116 | 114 |
patterns.any? { |pattern| |
117 | 115 |
File.fnmatch?(pattern, filename) |
118 | 116 |
} or next |
119 | 117 |
|
120 |
- case smtn |
|
121 |
- when /:/ |
|
122 |
- if year = month2year[mon] |
|
123 |
- mtime = Time.parse("#{mon} #{day} #{year} #{smtn} GMT") |
|
124 |
- else |
|
125 |
- log "Getting mtime of #{filename}" |
|
126 |
- mtime = ftp.mtime(filename) |
|
127 |
- month2year[mon] = mtime.year |
|
128 |
- end |
|
129 |
- else |
|
130 |
- # Do not bother calling MDTM for old files. Losing the |
|
131 |
- # time part only makes a timestamp go backwards, meaning |
|
132 |
- # that it will trigger no new event. |
|
133 |
- mtime = Time.parse("#{mon} #{day} #{smtn} GMT") |
|
134 |
- end |
|
135 |
- |
|
136 | 118 |
after < mtime or next |
137 | 119 |
|
138 | 120 |
yield filename, mtime |
@@ -193,7 +175,7 @@ module Agents |
||
193 | 175 |
found_entries[filename] |
194 | 176 |
}.each { |filename| |
195 | 177 |
create_event :payload => { |
196 |
- 'url' => (base_uri + filename).to_s, |
|
178 |
+ 'url' => "#{base_uri}#{filename}", |
|
197 | 179 |
'filename' => filename, |
198 | 180 |
'timestamp' => found_entries[filename], |
199 | 181 |
} |
@@ -31,7 +31,7 @@ module Agents |
||
31 | 31 |
end |
32 | 32 |
|
33 | 33 |
def validate_options |
34 |
- errors.add(:base, "you need to specify a hipchat auth_token") unless options['auth_token'].present? |
|
34 |
+ 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? |
|
35 | 35 |
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? |
36 | 36 |
end |
37 | 37 |
|
@@ -40,10 +40,10 @@ module Agents |
||
40 | 40 |
end |
41 | 41 |
|
42 | 42 |
def receive(incoming_events) |
43 |
- client = HipChat::Client.new(interpolated[:auth_token]) |
|
43 |
+ client = HipChat::Client.new(interpolated[:auth_token] || credential('hipchat_auth_token')) |
|
44 | 44 |
incoming_events.each do |event| |
45 | 45 |
mo = interpolated(event) |
46 |
- client[mo[:room_name]].send(mo[:username], mo[:message], :notify => mo[:notify].to_s == 'true' ? 1 : 0, :color => mo[:color]) |
|
46 |
+ client[mo[:room_name]].send(mo[:username], mo[:message], :notify => boolify(mo[:notify]) ? 1 : 0, :color => mo[:color]) |
|
47 | 47 |
end |
48 | 48 |
end |
49 | 49 |
end |
@@ -17,7 +17,7 @@ module Agents |
||
17 | 17 |
|
18 | 18 |
Simply choose a topic (think email subject line) to publish/listen to, and configure your service. |
19 | 19 |
|
20 |
- It's easy to setup your own [broker](http://jpmens.net/2013/09/01/installing-mosquitto-on-a-raspberry-pi/) or connect to a [cloud service](www.cloudmqtt.com) |
|
20 |
+ It's easy to setup your own [broker](http://jpmens.net/2013/09/01/installing-mosquitto-on-a-raspberry-pi/) or connect to a [cloud service](http://www.cloudmqtt.com) |
|
21 | 21 |
|
22 | 22 |
Hints: |
23 | 23 |
Many services run mqtts (mqtt over SSL) often with a custom certificate. |
@@ -69,7 +69,7 @@ module Agents |
||
69 | 69 |
def receive(incoming_events) |
70 | 70 |
incoming_events.each do |event| |
71 | 71 |
outgoing = interpolated(event)['payload'].presence || {} |
72 |
- if interpolated['no_merge'].to_s == 'true' |
|
72 |
+ if boolify(interpolated['no_merge']) |
|
73 | 73 |
handle outgoing, event.payload |
74 | 74 |
else |
75 | 75 |
handle outgoing.merge(event.payload), event.payload |
@@ -102,7 +102,7 @@ module Agents |
||
102 | 102 |
end |
103 | 103 |
|
104 | 104 |
def keep_event? |
105 |
- interpolated['keep_event'] == 'true' |
|
105 |
+ boolify(interpolated['keep_event']) |
|
106 | 106 |
end |
107 | 107 |
end |
108 | 108 |
end |
@@ -44,13 +44,13 @@ module Agents |
||
44 | 44 |
incoming_events.each do |event| |
45 | 45 |
message = (event.payload['message'].presence || event.payload['text'].presence || event.payload['sms'].presence).to_s |
46 | 46 |
if message.present? |
47 |
- if interpolated(event)['receive_call'].to_s == 'true' |
|
47 |
+ if boolify(interpolated(event)['receive_call']) |
|
48 | 48 |
secret = SecureRandom.hex 3 |
49 | 49 |
memory['pending_calls'][secret] = message |
50 | 50 |
make_call secret |
51 | 51 |
end |
52 | 52 |
|
53 |
- if interpolated(event)['receive_text'].to_s == 'true' |
|
53 |
+ if boolify(interpolated(event)['receive_text']) |
|
54 | 54 |
message = message.slice 0..160 |
55 | 55 |
send_message message |
56 | 56 |
end |
@@ -56,6 +56,8 @@ class EventDrop |
||
56 | 56 |
case key |
57 | 57 |
when 'agent' |
58 | 58 |
@object.agent |
59 |
+ when 'created_at' |
|
60 |
+ @object.created_at |
|
59 | 61 |
end |
60 | 62 |
end |
61 | 63 |
end |
@@ -53,7 +53,7 @@ |
||
53 | 53 |
</td> |
54 | 54 |
<td class='<%= "agent-disabled" if agent.disabled? %>'> |
55 | 55 |
<% if agent.can_create_events? %> |
56 |
- <%= link_to(agent.events_count || 0, events_path(:agent => agent.to_param)) %> |
|
56 |
+ <%= link_to(agent.events_count || 0, agent_events_path(agent)) %> |
|
57 | 57 |
<% else %> |
58 | 58 |
<span class='not-applicable'></span> |
59 | 59 |
<% end %> |
@@ -12,9 +12,8 @@ |
||
12 | 12 |
<div class="btn-group"> |
13 | 13 |
<%= link_to '<span class="glyphicon glyphicon-plus"></span> New Agent'.html_safe, new_agent_path, class: "btn btn-default" %> |
14 | 14 |
<%= link_to '<span class="glyphicon glyphicon-refresh"></span> Run event propagation'.html_safe, propagate_agents_path, method: 'post', class: "btn btn-default" %> |
15 |
- <%= link_to '<span class="glyphicon glyphicon-random"></span> View diagram'.html_safe, diagram_agents_path, class: "btn btn-default" %> |
|
15 |
+ <%= link_to '<span class="glyphicon glyphicon-random"></span> View diagram'.html_safe, diagram_path, class: "btn btn-default" %> |
|
16 | 16 |
</div> |
17 | 17 |
</div> |
18 | 18 |
</div> |
19 | 19 |
</div> |
20 |
- |
@@ -15,7 +15,7 @@ |
||
15 | 15 |
<li><a href="#logs" data-toggle="tab" data-agent-id="<%= @agent.id %>" class='<%= @agent.recent_error_logs? ? 'recent-errors' : '' %>'><span class='glyphicon glyphicon-list-alt'></span> Logs</a></li> |
16 | 16 |
|
17 | 17 |
<% if @agent.can_create_events? && @agent.events.count > 0 %> |
18 |
- <li><%= link_to '<span class="glyphicon glyphicon-random"></span> Events'.html_safe, events_path(:agent => @agent.to_param) %></li> |
|
18 |
+ <li><%= link_to '<span class="glyphicon glyphicon-random"></span> Events'.html_safe, agent_events_path(@agent) %></li> |
|
19 | 19 |
<% else %> |
20 | 20 |
<li class='disabled'><a><span class='glyphicon glyphicon-random'></span> Events</a></li> |
21 | 21 |
<% end %> |
@@ -103,7 +103,7 @@ |
||
103 | 103 |
<% if @agent.can_create_events? %> |
104 | 104 |
<p> |
105 | 105 |
<b>Events created:</b> |
106 |
- <%= link_to @agent.events.count, events_path(:agent => @agent.to_param) %> |
|
106 |
+ <%= link_to @agent.events.count, agent_events_path(@agent) %> |
|
107 | 107 |
</p> |
108 | 108 |
<% end %> |
109 | 109 |
|
@@ -1,3 +1,7 @@ |
||
1 |
+<% content_for :head do %> |
|
2 |
+ <%= javascript_include_tag "diagram" %> |
|
3 |
+<% end %> |
|
4 |
+ |
|
1 | 5 |
<div class='container'> |
2 | 6 |
<div class='row'> |
3 | 7 |
<div class='col-md-12'> |
@@ -41,7 +41,7 @@ |
||
41 | 41 |
agentPaths["New Agent"] = <%= Utils.jsonify new_agent_path %>; |
42 | 42 |
agentPaths["Account"] = <%= Utils.jsonify edit_user_registration_path %>; |
43 | 43 |
agentPaths["Events Index"] = <%= Utils.jsonify events_path %>; |
44 |
- agentPaths["View Agent Diagram"] = <%= Utils.jsonify diagram_agents_path %>; |
|
44 |
+ agentPaths["View Agent Diagram"] = <%= Utils.jsonify diagram_path %>; |
|
45 | 45 |
agentPaths["Run Event Propagation"] = { url: <%= Utils.jsonify propagate_agents_path %>, method: 'POST' }; |
46 | 46 |
|
47 | 47 |
|
@@ -15,7 +15,7 @@ |
||
15 | 15 |
|
16 | 16 |
<div class="btn-group"> |
17 | 17 |
<%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %> |
18 |
- <%= link_to '<span class="glyphicon glyphicon-random"></span> View Diagram'.html_safe, diagram_agents_path(:scenario_id => @scenario.to_param), class: "btn btn-default" %> |
|
18 |
+ <%= link_to '<span class="glyphicon glyphicon-random"></span> View Diagram'.html_safe, scenario_diagram_path(@scenario), class: "btn btn-default" %> |
|
19 | 19 |
<%= link_to '<span class="glyphicon glyphicon-edit"></span> Edit'.html_safe, edit_scenario_path(@scenario), class: "btn btn-default" %> |
20 | 20 |
<% if @scenario.source_url.present? %> |
21 | 21 |
<%= link_to '<span class="glyphicon glyphicon-plus"></span> Update'.html_safe, new_scenario_imports_path(:url => @scenario.source_url), class: "btn btn-default" %> |
@@ -61,7 +61,7 @@ Huginn::Application.configure do |
||
61 | 61 |
end |
62 | 62 |
|
63 | 63 |
# Precompile additional assets (application.js.coffee.erb, application.css, and all non-JS/CSS are already added) |
64 |
- config.assets.precompile += %w( graphing.js user_credentials.js ) |
|
64 |
+ config.assets.precompile += %w( diagram.js graphing.js user_credentials.js ) |
|
65 | 65 |
|
66 | 66 |
# Ignore bad email addresses and do not raise email delivery errors. |
67 | 67 |
# Set this to true and configure the email server for immediate delivery to raise delivery errors. |
@@ -11,7 +11,6 @@ Huginn::Application.routes.draw do |
||
11 | 11 |
post :propagate |
12 | 12 |
get :type_details |
13 | 13 |
get :event_descriptions |
14 |
- get :diagram |
|
15 | 14 |
end |
16 | 15 |
|
17 | 16 |
resources :logs, :only => [:index] do |
@@ -19,8 +18,12 @@ Huginn::Application.routes.draw do |
||
19 | 18 |
delete :clear |
20 | 19 |
end |
21 | 20 |
end |
21 |
+ |
|
22 |
+ resources :events, :only => [:index] |
|
22 | 23 |
end |
23 | 24 |
|
25 |
+ resource :diagram, :only => [:show] |
|
26 |
+ |
|
24 | 27 |
resources :events, :only => [:index, :show, :destroy] do |
25 | 28 |
member do |
26 | 29 |
post :reemit |
@@ -36,6 +39,8 @@ Huginn::Application.routes.draw do |
||
36 | 39 |
get :share |
37 | 40 |
get :export |
38 | 41 |
end |
42 |
+ |
|
43 |
+ resource :diagram, :only => [:show] |
|
39 | 44 |
end |
40 | 45 |
|
41 | 46 |
resources :user_credentials, :except => :show |
@@ -0,0 +1,21 @@ |
||
1 |
+class ConvertEfaSkipCreatedAt < ActiveRecord::Migration |
|
2 |
+ def up |
|
3 |
+ Agent.where(type: 'Agents::EventFormattingAgent').each do |agent| |
|
4 |
+ agent.options_will_change! |
|
5 |
+ unless agent.options.delete('skip_created_at').to_s == 'true' |
|
6 |
+ agent.options['instructions'] = { |
|
7 |
+ 'created_at' => '{{created_at}}' |
|
8 |
+ }.update(agent.options['instructions'] || {}) |
|
9 |
+ end |
|
10 |
+ agent.save! |
|
11 |
+ end |
|
12 |
+ end |
|
13 |
+ |
|
14 |
+ def down |
|
15 |
+ Agent.where(type: 'Agents::EventFormattingAgent').each do |agent| |
|
16 |
+ agent.options_will_change! |
|
17 |
+ agent.options['skip_created_at'] = (agent.options['instructions'] || {})['created_at'] == '{{created_at}}' |
|
18 |
+ agent.save! |
|
19 |
+ end |
|
20 |
+ end |
|
21 |
+end |
@@ -64,7 +64,7 @@ unless user.agents.where(:name => "Rain Notifier").exists? |
||
64 | 64 |
'value' => "rain|storm", |
65 | 65 |
'path' => "conditions" |
66 | 66 |
}], |
67 |
- 'message' => "Just so you know, it looks like '<conditions>' tomorrow in <location>" |
|
67 |
+ 'message' => "Just so you know, it looks like '{{conditions}}' tomorrow in {{location}}" |
|
68 | 68 |
}).save! |
69 | 69 |
end |
70 | 70 |
|
@@ -15,12 +15,12 @@ describe EventsController do |
||
15 | 15 |
|
16 | 16 |
it "can filter by Agent" do |
17 | 17 |
sign_in users(:bob) |
18 |
- get :index, :agent => agents(:bob_website_agent) |
|
18 |
+ get :index, :agent_id => agents(:bob_website_agent) |
|
19 | 19 |
assigns(:events).length.should == agents(:bob_website_agent).events.length |
20 | 20 |
assigns(:events).all? {|i| i.agent.should == agents(:bob_website_agent) }.should be_true |
21 | 21 |
|
22 | 22 |
lambda { |
23 |
- get :index, :agent => agents(:jane_website_agent) |
|
23 |
+ get :index, :agent_id => agents(:jane_website_agent) |
|
24 | 24 |
}.should raise_error(ActiveRecord::RecordNotFound) |
25 | 25 |
end |
26 | 26 |
end |
@@ -0,0 +1,5 @@ |
||
1 |
+APP_SECRET_TOKEN=notarealappsecrettoken |
|
2 |
+TWITTER_OAUTH_KEY=twitteroauthkey |
|
3 |
+TWITTER_OAUTH_SECRET=twitteroauthsecret |
|
4 |
+THIRTY_SEVEN_SIGNALS_OAUTH_KEY=TESTKEY |
|
5 |
+THIRTY_SEVEN_SIGNALS_OAUTH_SECRET=TESTSECRET |
@@ -72,7 +72,7 @@ jane_rain_notifier_agent: |
||
72 | 72 |
:value => "rain", |
73 | 73 |
:path => "conditions" |
74 | 74 |
}], |
75 |
- :message => "Just so you know, it looks like '<conditions>' tomorrow in <location>" |
|
75 |
+ :message => "Just so you know, it looks like '{{conditions}}' tomorrow in {{location}}" |
|
76 | 76 |
}.to_json.inspect %> |
77 | 77 |
|
78 | 78 |
bob_rain_notifier_agent: |
@@ -87,7 +87,7 @@ bob_rain_notifier_agent: |
||
87 | 87 |
:value => "rain", |
88 | 88 |
:path => "conditions" |
89 | 89 |
}], |
90 |
- :message => "Just so you know, it looks like '<conditions>' tomorrow in <location>" |
|
90 |
+ :message => "Just so you know, it looks like '{{conditions}}' tomorrow in {{location}}" |
|
91 | 91 |
}.to_json.inspect %> |
92 | 92 |
|
93 | 93 |
bob_twitter_user_agent: |
@@ -56,13 +56,13 @@ describe DotHelper do |
||
56 | 56 |
it "generates a DOT script" do |
57 | 57 |
agents_dot(@agents).should =~ %r{ |
58 | 58 |
\A |
59 |
- digraph \s foo \{ |
|
59 |
+ digraph \x20 "Agent \x20 Event \x20 Flow" \{ |
|
60 | 60 |
node \[ [^\]]+ \]; |
61 | 61 |
(?<foo>\w+) \[label=foo\]; |
62 | 62 |
\k<foo> -> (?<bar1>\w+) \[style=dashed\]; |
63 | 63 |
\k<foo> -> (?<bar2>\w+) \[color="\#999999"\]; |
64 | 64 |
\k<bar1> \[label=bar1\]; |
65 |
- \k<bar2> \[label="bar2 \s \(Disabled\)",style="rounded,dashed",color="\#999999",fontcolor="\#999999"\]; |
|
65 |
+ \k<bar2> \[label=bar2,style="rounded,dashed",color="\#999999",fontcolor="\#999999"\]; |
|
66 | 66 |
\k<bar2> -> (?<bar3>\w+) \[style=dashed,color="\#999999"\]; |
67 | 67 |
\k<bar3> \[label=bar3\]; |
68 | 68 |
\} |
@@ -73,15 +73,15 @@ describe DotHelper do |
||
73 | 73 |
it "generates a richer DOT script" do |
74 | 74 |
agents_dot(@agents, true).should =~ %r{ |
75 | 75 |
\A |
76 |
- digraph \s foo \{ |
|
76 |
+ digraph \x20 "Agent \x20 Event \x20 Flow" \{ |
|
77 | 77 |
node \[ [^\]]+ \]; |
78 |
- (?<foo>\w+) \[label=foo,URL="#{Regexp.quote(agent_path(@foo))}"\]; |
|
78 |
+ (?<foo>\w+) \[label=foo,tooltip="Dot \x20 Foo",URL="#{Regexp.quote(agent_path(@foo))}"\]; |
|
79 | 79 |
\k<foo> -> (?<bar1>\w+) \[style=dashed\]; |
80 | 80 |
\k<foo> -> (?<bar2>\w+) \[color="\#999999"\]; |
81 |
- \k<bar1> \[label=bar1,URL="#{Regexp.quote(agent_path(@bar1))}"\]; |
|
82 |
- \k<bar2> \[label="bar2 \s \(Disabled\)",URL="#{Regexp.quote(agent_path(@bar2))}",style="rounded,dashed",color="\#999999",fontcolor="\#999999"\]; |
|
81 |
+ \k<bar1> \[label=bar1,tooltip="Dot \x20 Bar",URL="#{Regexp.quote(agent_path(@bar1))}"\]; |
|
82 |
+ \k<bar2> \[label=bar2,tooltip="Dot \x20 Bar",URL="#{Regexp.quote(agent_path(@bar2))}",style="rounded,dashed",color="\#999999",fontcolor="\#999999"\]; |
|
83 | 83 |
\k<bar2> -> (?<bar3>\w+) \[style=dashed,color="\#999999"\]; |
84 |
- \k<bar3> \[label=bar3,URL="#{Regexp.quote(agent_path(@bar3))}"\]; |
|
84 |
+ \k<bar3> \[label=bar3,tooltip="Dot \x20 Bar",URL="#{Regexp.quote(agent_path(@bar3))}"\]; |
|
85 | 85 |
\} |
86 | 86 |
\z |
87 | 87 |
}x |
@@ -9,6 +9,8 @@ describe Agents::EventFormattingAgent do |
||
9 | 9 |
:message => "Received {{content.text}} from {{content.name}} .", |
10 | 10 |
:subject => "Weather looks like {{conditions}} according to the forecast at {{pretty_date.time}}", |
11 | 11 |
:agent => "{{agent.type}}", |
12 |
+ :created_at => "{{created_at}}", |
|
13 |
+ :created_at_iso => "{{created_at | date:'%FT%T%:z'}}", |
|
12 | 14 |
}, |
13 | 15 |
:mode => "clean", |
14 | 16 |
:matchers => [ |
@@ -18,7 +20,6 @@ describe Agents::EventFormattingAgent do |
||
18 | 20 |
:to => "pretty_date", |
19 | 21 |
}, |
20 | 22 |
], |
21 |
- :skip_created_at => "false" |
|
22 | 23 |
} |
23 | 24 |
} |
24 | 25 |
@checker = Agents::EventFormattingAgent.new(@valid_params) |
@@ -53,18 +54,12 @@ describe Agents::EventFormattingAgent do |
||
53 | 54 |
Event.last.payload[:content].should_not == nil |
54 | 55 |
end |
55 | 56 |
|
56 |
- it "should accept skip_created_at" do |
|
57 |
- @checker.receive([@event]) |
|
58 |
- Event.last.payload[:created_at].should_not == nil |
|
59 |
- @checker.options[:skip_created_at] = "true" |
|
60 |
- @checker.receive([@event]) |
|
61 |
- Event.last.payload[:created_at].should == nil |
|
62 |
- end |
|
63 |
- |
|
64 | 57 |
it "should handle Liquid templating in instructions" do |
65 | 58 |
@checker.receive([@event]) |
66 | 59 |
Event.last.payload[:message].should == "Received Some Lorem Ipsum from somevalue ." |
67 | 60 |
Event.last.payload[:agent].should == "WeatherAgent" |
61 |
+ Event.last.payload[:created_at].should == @event.created_at.to_s |
|
62 |
+ Event.last.payload[:created_at_iso].should == @event.created_at.iso8601 |
|
68 | 63 |
end |
69 | 64 |
|
70 | 65 |
it "should handle matchers and Liquid templating in instructions" do |
@@ -144,10 +139,5 @@ describe Agents::EventFormattingAgent do |
||
144 | 139 |
@checker.options[:mode] = "" |
145 | 140 |
@checker.should_not be_valid |
146 | 141 |
end |
147 |
- |
|
148 |
- it "should validate presence of skip_created_at" do |
|
149 |
- @checker.options[:skip_created_at] = "" |
|
150 |
- @checker.should_not be_valid |
|
151 |
- end |
|
152 | 142 |
end |
153 | 143 |
end |
@@ -7,19 +7,23 @@ describe Agents::FtpsiteAgent do |
||
7 | 7 |
@site = { |
8 | 8 |
'expected_update_period_in_days' => 1, |
9 | 9 |
'url' => "ftp://ftp.example.org/pub/releases/", |
10 |
- 'patterns' => ["example-*.tar.gz"], |
|
10 |
+ 'patterns' => ["example*.tar.gz"], |
|
11 | 11 |
} |
12 | 12 |
@checker = Agents::FtpsiteAgent.new(:name => "Example", :options => @site, :keep_events_for => 2) |
13 | 13 |
@checker.user = users(:bob) |
14 | 14 |
@checker.save! |
15 |
- stub(@checker).each_entry.returns { |block| |
|
16 |
- block.call("example-latest.tar.gz", Time.parse("2014-04-01T10:00:01Z")) |
|
17 |
- block.call("example-1.0.tar.gz", Time.parse("2013-10-01T10:00:00Z")) |
|
18 |
- block.call("example-1.1.tar.gz", Time.parse("2014-04-01T10:00:00Z")) |
|
19 |
- } |
|
20 | 15 |
end |
21 | 16 |
|
22 | 17 |
describe "#check" do |
18 |
+ |
|
19 |
+ before do |
|
20 |
+ stub(@checker).each_entry.returns { |block| |
|
21 |
+ block.call("example latest.tar.gz", Time.parse("2014-04-01T10:00:01Z")) |
|
22 |
+ block.call("example-1.0.tar.gz", Time.parse("2013-10-01T10:00:00Z")) |
|
23 |
+ block.call("example-1.1.tar.gz", Time.parse("2014-04-01T10:00:00Z")) |
|
24 |
+ } |
|
25 |
+ end |
|
26 |
+ |
|
23 | 27 |
it "should validate the integer fields" do |
24 | 28 |
@checker.options['expected_update_period_in_days'] = "nonsense" |
25 | 29 |
lambda { @checker.save! }.should raise_error; |
@@ -33,7 +37,7 @@ describe Agents::FtpsiteAgent do |
||
33 | 37 |
known_entries.sort_by(&:last).should == [ |
34 | 38 |
["example-1.0.tar.gz", "2013-10-01T10:00:00Z"], |
35 | 39 |
["example-1.1.tar.gz", "2014-04-01T10:00:00Z"], |
36 |
- ["example-latest.tar.gz", "2014-04-01T10:00:01Z"], |
|
40 |
+ ["example latest.tar.gz", "2014-04-01T10:00:01Z"], |
|
37 | 41 |
] |
38 | 42 |
} |
39 | 43 |
|
@@ -46,7 +50,7 @@ describe Agents::FtpsiteAgent do |
||
46 | 50 |
lambda { @checker.check }.should_not change { Event.count } |
47 | 51 |
|
48 | 52 |
stub(@checker).each_entry.returns { |block| |
49 |
- block.call("example-latest.tar.gz", Time.parse("2014-04-02T10:00:01Z")) |
|
53 |
+ block.call("example latest.tar.gz", Time.parse("2014-04-02T10:00:01Z")) |
|
50 | 54 |
|
51 | 55 |
# In the long list format the timestamp may look going |
52 | 56 |
# backwards after six months: Oct 01 10:00 -> Oct 01 2013 |
@@ -62,7 +66,7 @@ describe Agents::FtpsiteAgent do |
||
62 | 66 |
["example-1.0.tar.gz", "2013-10-01T00:00:00Z"], |
63 | 67 |
["example-1.1.tar.gz", "2014-04-01T10:00:00Z"], |
64 | 68 |
["example-1.2.tar.gz", "2014-04-02T10:00:00Z"], |
65 |
- ["example-latest.tar.gz", "2014-04-02T10:00:01Z"], |
|
69 |
+ ["example latest.tar.gz", "2014-04-02T10:00:01Z"], |
|
66 | 70 |
] |
67 | 71 |
} |
68 | 72 |
|
@@ -75,5 +79,33 @@ describe Agents::FtpsiteAgent do |
||
75 | 79 |
lambda { @checker.check }.should_not change { Event.count } |
76 | 80 |
end |
77 | 81 |
end |
82 |
+ |
|
83 |
+ describe "#each_entry" do |
|
84 |
+ before do |
|
85 |
+ stub.any_instance_of(Net::FTP).list.returns [ # Windows format |
|
86 |
+ "04-02-14 10:01AM 288720748 example latest.tar.gz", |
|
87 |
+ "04-01-14 10:05AM 288720710 no-match-example.tar.gz" |
|
88 |
+ ] |
|
89 |
+ stub(@checker).open_ftp.yields Net::FTP.new |
|
90 |
+ end |
|
91 |
+ |
|
92 |
+ it "filters out files that don't match the given format" do |
|
93 |
+ entries = [] |
|
94 |
+ @checker.each_entry { |a, b| entries.push [a, b] } |
|
95 |
+ |
|
96 |
+ entries.size.should == 1 |
|
97 |
+ filename, mtime = entries.first |
|
98 |
+ filename.should == 'example latest.tar.gz' |
|
99 |
+ mtime.should == '2014-04-02T10:01:00Z' |
|
100 |
+ end |
|
101 |
+ |
|
102 |
+ it "filters out files that are older than the given date" do |
|
103 |
+ @checker.options['after'] = '2015-10-21' |
|
104 |
+ entries = [] |
|
105 |
+ @checker.each_entry { |a, b| entries.push [a, b] } |
|
106 |
+ entries.size.should == 0 |
|
107 |
+ end |
|
108 |
+ end |
|
109 |
+ |
|
78 | 110 |
end |
79 | 111 |
end |
@@ -42,6 +42,12 @@ describe Agents::HipchatAgent do |
||
42 | 42 |
@checker.should be_valid |
43 | 43 |
end |
44 | 44 |
|
45 |
+ it "should also allow a credential" do |
|
46 |
+ @checker.options['auth_token'] = nil |
|
47 |
+ @checker.should_not be_valid |
|
48 |
+ @checker.user.user_credentials.create :credential_name => 'hipchat_auth_token', :credential_value => 'something' |
|
49 |
+ @checker.reload.should be_valid |
|
50 |
+ end |
|
45 | 51 |
end |
46 | 52 |
|
47 | 53 |
describe "#receive" do |
@@ -85,6 +85,7 @@ describe EventDrop do |
||
85 | 85 |
before do |
86 | 86 |
@event = Event.new |
87 | 87 |
@event.agent = agents(:jane_weather_agent) |
88 |
+ @event.created_at = Time.at(1400000000) |
|
88 | 89 |
@event.payload = { |
89 | 90 |
'title' => 'some title', |
90 | 91 |
'url' => 'http://some.site.example.org/', |
@@ -111,4 +112,9 @@ describe EventDrop do |
||
111 | 112 |
t = '{{agent.name}}' |
112 | 113 |
interpolate(t, @event).should eq('SF Weather') |
113 | 114 |
end |
115 |
+ |
|
116 |
+ it 'should have created_at' do |
|
117 |
+ t = '{{created_at | date:"%FT%T%z" }}' |
|
118 |
+ interpolate(t, @event).should eq('2014-05-13T09:53:20-0700') |
|
119 |
+ end |
|
114 | 120 |
end |
@@ -59,8 +59,6 @@ describe Service do |
||
59 | 59 |
stub_request(:post, "https://launchpad.37signals.com/authorization/token?client_id=TESTKEY&client_secret=TESTSECRET&refresh_token=refreshtokentest&type=refresh"). |
60 | 60 |
to_return(:status => 200, :body => '{"expires_in":1209600,"access_token": "NEWTOKEN"}', :headers => {}) |
61 | 61 |
@service.provider = '37signals' |
62 |
- ENV['THIRTY_SEVEN_SIGNALS_OAUTH_KEY'] = 'TESTKEY' |
|
63 |
- ENV['THIRTY_SEVEN_SIGNALS_OAUTH_SECRET'] = 'TESTSECRET' |
|
64 | 62 |
@service.refresh_token = 'refreshtokentest' |
65 | 63 |
@service.refresh_token! |
66 | 64 |
@service.token.should == 'NEWTOKEN' |
@@ -1,4 +1,3 @@ |
||
1 |
-# This file is copied to spec/ when you run 'rails generate rspec:install' |
|
2 | 1 |
ENV["RAILS_ENV"] ||= 'test' |
3 | 2 |
|
4 | 3 |
if ENV['COVERAGE'] |
@@ -9,6 +8,10 @@ else |
||
9 | 8 |
Coveralls.wear!('rails') |
10 | 9 |
end |
11 | 10 |
|
11 |
+# Required ENV variables that are normally set in .env are setup here for the test environment. |
|
12 |
+require 'dotenv' |
|
13 |
+Dotenv.load File.join(File.dirname(__FILE__), "env.test") |
|
14 |
+ |
|
12 | 15 |
require File.expand_path("../../config/environment", __FILE__) |
13 | 16 |
require 'rspec/rails' |
14 | 17 |
require 'rspec/autorun' |
@@ -19,7 +22,7 @@ WebMock.disable_net_connect! |
||
19 | 22 |
|
20 | 23 |
# Requires supporting ruby files with custom matchers and macros, etc, |
21 | 24 |
# in spec/support/ and its subdirectories. |
22 |
-Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} |
|
25 |
+Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } |
|
23 | 26 |
|
24 | 27 |
ActiveRecord::Migration.maintain_test_schema! |
25 | 28 |
|