Merge pull request #3 from cantino/dsander-omniauth

Moving ENV variables

Dominik Sander 10 jaren geleden
bovenliggende
commit
8cf4b3d3bf
37 gewijzigde bestanden met toevoegingen van 277 en 110 verwijderingen
  1. 1 0
      Gemfile
  2. 2 0
      Gemfile.lock
  3. 3 1
      README.md
  4. 20 0
      app/assets/javascripts/diagram.js.coffee
  5. 30 0
      app/assets/stylesheets/diagram.css.scss
  6. 0 8
      app/controllers/agents_controller.rb
  7. 9 0
      app/controllers/diagrams_controller.rb
  8. 2 2
      app/controllers/events_controller.rb
  9. 70 13
      app/helpers/dot_helper.rb
  10. 7 6
      app/models/agents/event_formatting_agent.rb
  11. 6 24
      app/models/agents/ftpsite_agent.rb
  12. 3 3
      app/models/agents/hipchat_agent.rb
  13. 1 1
      app/models/agents/mqtt_agent.rb
  14. 1 1
      app/models/agents/post_agent.rb
  15. 1 1
      app/models/agents/trigger_agent.rb
  16. 2 2
      app/models/agents/twilio_agent.rb
  17. 2 0
      app/models/event.rb
  18. 1 1
      app/views/agents/_table.html.erb
  19. 1 2
      app/views/agents/index.html.erb
  20. 2 2
      app/views/agents/show.html.erb
  21. 4 0
      app/views/agents/diagram.html.erb
  22. 1 1
      app/views/layouts/application.html.erb
  23. 1 1
      app/views/scenarios/show.html.erb
  24. 1 1
      config/environments/production.rb
  25. 6 1
      config/routes.rb
  26. 21 0
      db/migrate/20140730005210_convert_efa_skip_created_at.rb
  27. 1 1
      db/seeds.rb
  28. 2 2
      spec/controllers/events_controller_spec.rb
  29. 5 0
      spec/env.test
  30. 2 2
      spec/fixtures/agents.yml
  31. 7 7
      spec/helpers/dot_helper_spec.rb
  32. 4 14
      spec/models/agents/event_formatting_agent_spec.rb
  33. 41 9
      spec/models/agents/ftpsite_agent_spec.rb
  34. 6 0
      spec/models/agents/hipchat_agent_spec.rb
  35. 6 0
      spec/models/event_spec.rb
  36. 0 2
      spec/models/service_spec.rb
  37. 5 2
      spec/spec_helper.rb

+ 1 - 0
Gemfile

@@ -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'

+ 2 - 0
Gemfile.lock

@@ -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

+ 3 - 1
README.md

@@ -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
-[![Build Status](https://travis-ci.org/cantino/huginn.png)](https://travis-ci.org/cantino/huginn) [![Coverage Status](https://coveralls.io/repos/cantino/huginn/badge.png)](https://coveralls.io/r/cantino/huginn) [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/cantino/huginn/trend.png)](https://bitdeli.com/free "Bitdeli Badge") [![Dependency Status](https://gemnasium.com/cantino/huginn.svg)](https://gemnasium.com/cantino/huginn)
110
+[![Build Status](https://travis-ci.org/cantino/huginn.png)](https://travis-ci.org/cantino/huginn) [![Coverage Status](https://coveralls.io/repos/cantino/huginn/badge.png)](https://coveralls.io/r/cantino/huginn) [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/cantino/huginn/trend.png)](https://bitdeli.com/free "Bitdeli Badge") [![Dependency Status](https://gemnasium.com/cantino/huginn.svg)](https://gemnasium.com/cantino/huginn) [![Bountysource](https://www.bountysource.com/badge/tracker?tracker_id=282580)](https://www.bountysource.com/trackers/282580-huginn?utm_source=282580&utm_medium=shield&utm_campaign=TRACKER_BADGE)
109 111
 

+ 20 - 0
app/assets/javascripts/diagram.js.coffee

@@ -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

+ 30 - 0
app/assets/stylesheets/diagram.css.scss

@@ -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
+}

+ 0 - 8
app/controllers/agents_controller.rb

@@ -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,

+ 9 - 0
app/controllers/diagrams_controller.rb

@@ -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 - 2
app/controllers/events_controller.rb

@@ -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])

+ 70 - 13
app/helpers/dot_helper.rb

@@ -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

+ 7 - 6
app/models/agents/event_formatting_agent.rb

@@ -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

+ 6 - 24
app/models/agents/ftpsite_agent.rb

@@ -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
         }

+ 3 - 3
app/models/agents/hipchat_agent.rb

@@ -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

+ 1 - 1
app/models/agents/mqtt_agent.rb

@@ -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.

+ 1 - 1
app/models/agents/post_agent.rb

@@ -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

+ 1 - 1
app/models/agents/trigger_agent.rb

@@ -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

+ 2 - 2
app/models/agents/twilio_agent.rb

@@ -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

+ 2 - 0
app/models/event.rb

@@ -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

+ 1 - 1
app/views/agents/_table.html.erb

@@ -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 %>

+ 1 - 2
app/views/agents/index.html.erb

@@ -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
-

+ 2 - 2
app/views/agents/show.html.erb

@@ -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
 

+ 4 - 0
app/views/agents/diagram.html.erb

@@ -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'>

+ 1 - 1
app/views/layouts/application.html.erb

@@ -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
 

+ 1 - 1
app/views/scenarios/show.html.erb

@@ -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" %>

+ 1 - 1
config/environments/production.rb

@@ -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.

+ 6 - 1
config/routes.rb

@@ -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

+ 21 - 0
db/migrate/20140730005210_convert_efa_skip_created_at.rb

@@ -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

+ 1 - 1
db/seeds.rb

@@ -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
 

+ 2 - 2
spec/controllers/events_controller_spec.rb

@@ -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

+ 5 - 0
spec/env.test

@@ -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

+ 2 - 2
spec/fixtures/agents.yml

@@ -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:

+ 7 - 7
spec/helpers/dot_helper_spec.rb

@@ -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

+ 4 - 14
spec/models/agents/event_formatting_agent_spec.rb

@@ -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

+ 41 - 9
spec/models/agents/ftpsite_agent_spec.rb

@@ -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

+ 6 - 0
spec/models/agents/hipchat_agent_spec.rb

@@ -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

+ 6 - 0
spec/models/event_spec.rb

@@ -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

+ 0 - 2
spec/models/service_spec.rb

@@ -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'

+ 5 - 2
spec/spec_helper.rb

@@ -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