Merge branch 'master' into human_task_agent

Andrew Cantino 11 ans auparavant
Parent
Commettre
d766eb6cd3

+ 44 - 12
app/assets/javascripts/application.js.coffee.erb

@@ -8,13 +8,14 @@
8 8
 #= require ./worker-checker
9 9
 #= require_self
10 10
 
11
-setupJsonEditor = ->
11
+window.setupJsonEditor = ($editor = $(".live-json-editor")) ->
12 12
   JSONEditor.prototype.ADD_IMG = '<%= image_path 'json-editor/add.png' %>'
13 13
   JSONEditor.prototype.DELETE_IMG = '<%= image_path 'json-editor/delete.png' %>'
14
-  if $(".live-json-editor").length
15
-    window.jsonEditor = new JSONEditor($(".live-json-editor"), 400, 500)
16
-    window.jsonEditor.doTruncation true
17
-    window.jsonEditor.showFunctionButtons()
14
+  if $editor.length
15
+    jsonEditor = new JSONEditor($editor, $editor.data('width') || 400, $editor.data('height') || 500)
16
+    jsonEditor.doTruncation true
17
+    jsonEditor.showFunctionButtons()
18
+    return jsonEditor
18 19
 
19 20
 hideSchedule = ->
20 21
   $(".schedule-region select").hide()
@@ -45,7 +46,7 @@ showEventDescriptions = ->
45 46
 
46 47
 $(document).ready ->
47 48
   # JSON Editor
48
-  setupJsonEditor()
49
+  window.jsonEditor = setupJsonEditor()
49 50
 
50 51
   # Select2 Selects
51 52
   $(".select2").select2(width: 'resolve')
@@ -54,7 +55,35 @@ $(document).ready ->
54 55
   if $(".flash").length
55 56
     setTimeout((-> $(".flash").slideUp(-> $(".flash").remove())), 5000)
56 57
 
57
-  # Agent Show
58
+  # Agent Navigation
59
+  $agentNavigate = $('#agent-navigate')
60
+  $agentNavigate.typeahead(
61
+    minLength: 0,
62
+    items: 15,
63
+    source: agentNames
64
+  ).on("change", (e) ->
65
+    if agentPaths[$agentNavigate.val()]
66
+      $('#agent-navigate').closest(".navbar-search").find(".spinner").show()
67
+      navigationData = agentPaths[$agentNavigate.val()]
68
+      if !(navigationData instanceof Object) || !navigationData.method || navigationData.method == 'GET'
69
+        window.location = navigationData.url || navigationData
70
+      else
71
+        $("<a href='#{navigationData.url}' data-method='#{navigationData.method}'></a>").appendTo($("body")).click()
72
+
73
+  ).on("focus", (e) ->
74
+    $agentNavigate.val ''
75
+  ).on("blur", (e) ->
76
+    $agentNavigate.val ''
77
+  )
78
+
79
+  # Pressing '/' selects the search box.
80
+  $("body").on "keypress", (e) ->
81
+    if e.keyCode == 47 # The '/' key
82
+      if e.target.nodeName == "BODY"
83
+        e.preventDefault()
84
+        $agentNavigate.focus()
85
+
86
+# Agent Show
58 87
   fetchLogs = (e) ->
59 88
     agentId = $(e.target).closest("[data-agent-id]").data("agent-id")
60 89
     e.preventDefault()
@@ -76,16 +105,19 @@ $(document).ready ->
76 105
         $("#logs .spinner").stop(true, true).fadeOut ->
77 106
           $("#logs .refresh, #logs .clear").show()
78 107
 
79
-  $(".agent-show #show-tabs a[href='#logs']").on "click", fetchLogs
80
-  $("#logs .refresh").on "click", fetchLogs
81
-  $("#logs .clear").on "click", clearLogs
108
+  $(".agent-show #show-tabs a[href='#logs'], #logs .refresh").on "click", fetchLogs
109
+  $(".agent-show #logs .clear").on "click", clearLogs
110
+
111
+  if tab = window.location.href.match(/tab=(\w+)\b/i)?[1]
112
+    if tab in ["details", "logs"]
113
+      $(".agent-show .nav-tabs li a[href='##{tab}']").click()
82 114
 
83 115
   # Editing Agents
84 116
   $("#agent_source_ids").on "change", showEventDescriptions
85 117
 
86 118
   $("#agent_type").on "change", ->
87 119
     if window.jsonEditor?
88
-      $(".spinner").fadeIn();
120
+      $("#agent-spinner").fadeIn();
89 121
       $("#agent_source_ids").select2("val", {});
90 122
       $(".event-descriptions").html("").hide()
91 123
       $.getJSON "/agents/type_details", { type: $(@).val() }, (json) =>
@@ -104,7 +136,7 @@ $(document).ready ->
104 136
         window.jsonEditor.json = json.options
105 137
         window.jsonEditor.rebuild()
106 138
 
107
-        $(".spinner").stop(true, true).fadeOut();
139
+        $("#agent-spinner").stop(true, true).fadeOut();
108 140
 
109 141
   $("#agent_type").change() if $("#agent_type").length
110 142
 

+ 20 - 8
app/assets/stylesheets/application.css.scss.erb

@@ -51,10 +51,6 @@ table.events {
51 51
   margin-left: 0 !important;
52 52
 }
53 53
 
54
-#job-indicator, #event-indicator {
55
-  display: none;
56
-}
57
-
58 54
 img.odin {
59 55
   position: relative;
60 56
   top: -32px;
@@ -86,14 +82,30 @@ img.spinner {
86 82
   overflow: hidden;
87 83
 }
88 84
 
85
+span.not-applicable:after {
86
+  color: #bbbbbb;
87
+  content: "n/a";
88
+}
89
+
90
+// Navbar
91
+
92
+#job-indicator, #event-indicator {
93
+  display: none;
94
+}
95
+
96
+.navbar-search > .spinner {
97
+  position: absolute;
98
+  top: -1px;
99
+  right: 1px;
100
+}
101
+
89 102
 // Flash
90 103
 
91 104
 .flash {
92 105
   position: fixed;
93
-  width: 500px;
106
+  width: 210px;
94 107
   z-index: 99999;
95
-  top: 1px;
96
-  margin-left: 250px;
108
+  right: 20px;
97 109
 
98 110
   .alert {
99 111
   }
@@ -109,4 +121,4 @@ img.spinner {
109 121
   &.refresh {
110 122
     margin: 0 10px;
111 123
   }
112
-}
124
+}

+ 10 - 0
app/controllers/agents_controller.rb

@@ -8,6 +8,16 @@ class AgentsController < ApplicationController
8 8
     end
9 9
   end
10 10
 
11
+  def handle_details_post
12
+    @agent = current_user.agents.find(params[:id])
13
+    if @agent.respond_to?(:handle_details_post)
14
+      render :json => @agent.handle_details_post(params) || {}
15
+    else
16
+      @agent.error "#handle_details_post called on an instance of #{@agent.class} that does not define it."
17
+      head 500
18
+    end
19
+  end
20
+
11 21
   def run
12 22
     agent = current_user.agents.find(params[:id])
13 23
     Agent.async_check(agent.id)

+ 1 - 1
app/helpers/application_helper.rb

@@ -11,7 +11,7 @@ module ApplicationHelper
11 11
     if agent.working?
12 12
       '<span class="label label-success">Yes</span>'.html_safe
13 13
     else
14
-      '<span class="label label-warning">No</span>'.html_safe
14
+      link_to '<span class="label label-warning">No</span>'.html_safe, agent_path(agent, :tab => (agent.recent_error_logs? ? 'logs' : 'details'))
15 15
     end
16 16
   end
17 17
 end

+ 1 - 1
app/mailers/system_mailer.rb

@@ -2,7 +2,7 @@ class SystemMailer < ActionMailer::Base
2 2
   default :from => ENV['EMAIL_FROM_ADDRESS'] || 'you@example.com'
3 3
 
4 4
   def send_message(options)
5
-    @lines = options[:lines]
5
+    @groups = options[:groups]
6 6
     @headline = options[:headline]
7 7
     mail :to => options[:to], :subject => options[:subject]
8 8
   end

+ 22 - 2
app/models/agent.rb

@@ -88,7 +88,11 @@ class Agent < ActiveRecord::Base
88 88
   end
89 89
 
90 90
   def create_event(attrs)
91
-    events.create!({ :user => user }.merge(attrs))
91
+    if can_create_events?
92
+      events.create!({ :user => user }.merge(attrs))
93
+    else
94
+      error "This Agent cannot create events!"
95
+    end
92 96
   end
93 97
 
94 98
   def validate_schedule
@@ -117,7 +121,7 @@ class Agent < ActiveRecord::Base
117 121
   end
118 122
 
119 123
   def last_event_at
120
-    @memoized_last_event_at ||= events.select(:created_at).first.try(:created_at)
124
+    @memoized_last_event_at ||= most_recent_event.try(:created_at)
121 125
   end
122 126
 
123 127
   def default_schedule
@@ -140,6 +144,14 @@ class Agent < ActiveRecord::Base
140 144
     !cannot_receive_events?
141 145
   end
142 146
 
147
+  def cannot_create_events?
148
+    self.class.cannot_create_events?
149
+  end
150
+
151
+  def can_create_events?
152
+    !cannot_create_events?
153
+  end
154
+
143 155
   def set_last_checked_event_id
144 156
     if newest_event_id = Event.order("id desc").limit(1).pluck(:id).first
145 157
       self.last_checked_event_id = newest_event_id
@@ -170,6 +182,14 @@ class Agent < ActiveRecord::Base
170 182
       @default_schedule
171 183
     end
172 184
 
185
+    def cannot_create_events!
186
+      @cannot_create_events = true
187
+    end
188
+
189
+    def cannot_create_events?
190
+      !!@cannot_create_events
191
+    end
192
+
173 193
     def cannot_receive_events!
174 194
       @cannot_receive_events = true
175 195
     end

+ 13 - 13
app/models/agents/digest_email_agent.rb

@@ -3,6 +3,8 @@ module Agents
3 3
     MAIN_KEYS = %w[title message text main value].map(&:to_sym)
4 4
     default_schedule "5am"
5 5
 
6
+    cannot_create_events!
7
+
6 8
     description <<-MD
7 9
       The DigestEmailAgent collects any Events sent to it and sends them all via email when run.
8 10
       The email will be sent to your account's address and will have a `subject` and an optional `headline` before
@@ -37,30 +39,28 @@ module Agents
37 39
 
38 40
     def check
39 41
       if self.memory[:queue] && self.memory[:queue].length > 0
40
-        lines = self.memory[:queue].map {|item| present(item) }
42
+        groups = self.memory[:queue].map { |payload| present(payload) }
41 43
         log "Sending digest mail to #{user.email}"
42
-        SystemMailer.delay.send_message(:to => user.email, :subject => options[:subject], :headline => options[:headline], :lines => lines)
44
+        SystemMailer.delay.send_message(:to => user.email, :subject => options[:subject], :headline => options[:headline], :groups => groups)
43 45
         self.memory[:queue] = []
44 46
       end
45 47
     end
46 48
 
47
-    def present(item)
48
-      if item.is_a?(Hash)
49
+    def present(payload)
50
+      if payload.is_a?(Hash)
51
+        payload = ActiveSupport::HashWithIndifferentAccess.new(payload)
49 52
         MAIN_KEYS.each do |key|
50
-          if item.has_key?(key)
51
-            return "#{item[key]}" + ((item.length > 1 && item.length < 5) ? " (#{present_hash item, key})" : "")
52
-          elsif item.has_key?(key.to_s)
53
-            return "#{item[key.to_s]}" + ((item.length > 1 && item.length < 5) ? " (#{present_hash item, key.to_s})" : "")
54
-          end
53
+          return { :title => payload[key].to_s, :entries => present_hash(payload, key) } if payload.has_key?(key)
55 54
         end
56
-        present_hash item
55
+
56
+        { :title => "Event", :entries => present_hash(payload) }
57 57
       else
58
-        item.to_s
58
+        { :title => payload.to_s, :entries => [] }
59 59
       end
60 60
     end
61 61
 
62 62
     def present_hash(hash, skip_key = nil)
63
-      hash.to_a.sort_by {|a| a.first.to_s }.map { |k, v| "#{k}: #{v}" unless [skip_key].include?(k) }.compact.to_sentence
63
+      hash.to_a.sort_by {|a| a.first.to_s }.map { |k, v| "#{k}: #{v}" unless k.to_s == skip_key.to_s }.compact
64 64
     end
65 65
   end
66
-end
66
+end

+ 32 - 0
app/models/agents/manual_event_agent.rb

@@ -0,0 +1,32 @@
1
+module Agents
2
+  class ManualEventAgent < Agent
3
+    cannot_be_scheduled!
4
+    cannot_receive_events!
5
+
6
+    description <<-MD
7
+      Use this Agent to manually create Events for testing or other purposes.
8
+    MD
9
+
10
+    event_description "User determined"
11
+
12
+    def default_options
13
+      { "no options" => "are needed" }
14
+    end
15
+
16
+    def handle_details_post(params)
17
+      if params[:payload]
18
+        create_event(:payload => params[:payload])
19
+        { :success => true }
20
+      else
21
+        { :success => false, :error => "You must provide a JSON payload" }
22
+      end
23
+    end
24
+
25
+    def working?
26
+      true
27
+    end
28
+
29
+    def validate_options
30
+    end
31
+  end
32
+end

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

@@ -1,6 +1,7 @@
1 1
 module Agents
2 2
   class PostAgent < Agent
3 3
     cannot_be_scheduled!
4
+    cannot_create_events!
4 5
 
5 6
     description <<-MD
6 7
        Post Agent receives events from other agents and send those events as the contents of a post request to a specified url. `post_url` field must specify where you would like to receive post requests and do not forget to include URI scheme (`http` or `https`)

+ 1 - 0
app/models/agents/twilio_agent.rb

@@ -4,6 +4,7 @@ require 'securerandom'
4 4
 module Agents
5 5
   class TwilioAgent < Agent
6 6
     cannot_be_scheduled!
7
+    cannot_create_events!
7 8
 
8 9
     description <<-MD
9 10
       The TwilioAgent receives and collects events and sends them via text message or gives you a call when scheduled.

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

@@ -47,8 +47,9 @@
47 47
   <div class="control-group">
48 48
     <%= f.label :sources, :class => 'control-label' %>
49 49
     <div class="controls link-region" data-can-receive-events="<%= @agent.can_receive_events? %>">
50
+      <% eventSources = (current_user.agents - [@agent]).find_all { |a| a.can_create_events? } %>
50 51
       <%= f.select(:source_ids,
51
-                   options_for_select((current_user.agents - [@agent]).map {|s| [s.name, s.id] },
52
+                   options_for_select(eventSources.map {|s| [s.name, s.id] },
52 53
                                       @agent.source_ids),
53 54
                    {}, { :multiple => true, :size => 5, :class => 'span4 select2' }) %>
54 55
       <span class='cannot-receive-events text-info'>This type of Agent cannot receive events.</span>

+ 42 - 0
app/views/agents/agent_views/manual_event_agent/_show.html.erb

@@ -0,0 +1,42 @@
1
+<h3>Manually Create Events</h3>
2
+
3
+<h4 id='event-creation-status'></h4>
4
+
5
+<%= form_tag handle_details_post_agent_path(@agent), :id => "create-event-form" do %>
6
+  <div class="control-group">
7
+    <textarea rows="10" id="payload" name="payload" class="span8 payload-editor" data-height="200">
8
+      {}
9
+    </textarea>
10
+  </div>
11
+
12
+  <div class='form-actions' style='clear: both'>
13
+    <%= submit_tag "Submit", :class => "btn btn-primary" %>
14
+  </div>
15
+<% end %>
16
+
17
+<script>
18
+  $(function () {
19
+    var payloadJsonEditor = window.setupJsonEditor($(".payload-editor"));
20
+    $("#create-event-form").submit(function (e) {
21
+      e.preventDefault();
22
+      var $form = $("#create-event-form");
23
+      var $status = $("#event-creation-status");
24
+      $.ajax({
25
+        url: $form.attr('action'),
26
+        method: "post",
27
+        data: { payload: JSON.parse($form.find("textarea").val()) },
28
+        dataType: "JSON",
29
+        success: function(json) {
30
+          if (json.success) {
31
+            $status.text("Success!");
32
+          } else {
33
+            $status.text("An error occurred: " + json.error);
34
+          }
35
+        },
36
+        error: function(response) {
37
+          $status.text("An error occurred: " + response.responseText)
38
+        }
39
+      });
40
+    });
41
+  });
42
+</script>

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

@@ -4,7 +4,7 @@
4 4
       <div class="page-header">
5 5
         <h2>
6 6
           Editing your <%= @agent.short_type %>
7
-          <%= image_tag "spinner-arrows.gif", :class => "spinner" %>
7
+          <%= image_tag "spinner-arrows.gif", :class => "spinner", :id => 'agent-spinner' %>
8 8
         </h2>
9 9
       </div>
10 10
 

+ 55 - 37
app/views/agents/index.html.erb

@@ -8,53 +8,71 @@
8 8
       <table class='table table-striped'>
9 9
         <tr>
10 10
           <th>Name</th>
11
+          <th>Schedule</th>
11 12
           <th>Last Check</th>
12 13
           <th>Last Event Out</th>
13 14
           <th>Last Event In</th>
14
-          <th>Events</th>
15
-          <th>Schedule</th>
15
+          <th>Events Created</th>
16 16
           <th>Working?</th>
17 17
           <th></th>
18 18
         </tr>
19 19
 
20 20
         <% @agents.each do |agent| %>
21
-            <tr>
22
-              <td>
23
-                <%= agent.name %>
24
-                <br/>
25
-                <span class='muted'><%= agent.short_type.titleize %></span>
26
-              </td>
27
-              <td>
28
-                <% if agent.cannot_be_scheduled? %>
29
-                    N/A
30
-                <% else %>
31
-                    <%= agent.last_check_at ? time_ago_in_words(agent.last_check_at) + " ago" : "never" %>
32
-                <% end %>
33
-              </td>
34
-              <td><%= agent.last_event_at ? time_ago_in_words(agent.last_event_at) + " ago" : "never" %></td>
35
-              <td>
36
-                <% if agent.cannot_receive_events? %>
37
-                    N/A
21
+          <tr>
22
+            <td>
23
+              <%= agent.name %>
24
+              <br/>
25
+              <span class='muted'><%= agent.short_type.titleize %></span>
26
+            </td>
27
+            <td>
28
+              <% if agent.can_be_scheduled? %>
29
+                <%= agent.schedule.to_s.humanize.titleize %>
30
+              <% else %>
31
+                <span class='not-applicable'></span>
32
+              <% end %>
33
+            </td>
34
+            <td>
35
+              <% if agent.can_be_scheduled? %>
36
+                <%= agent.last_check_at ? time_ago_in_words(agent.last_check_at) + " ago" : "never" %>
37
+              <% else %>
38
+                <span class='not-applicable'></span>
39
+              <% end %>
40
+            </td>
41
+            <td>
42
+              <% if agent.can_create_events? %>
43
+                <%= agent.last_event_at ? time_ago_in_words(agent.last_event_at) + " ago" : "never" %>
44
+              <% else %>
45
+                <span class='not-applicable'></span>
46
+              <% end %>
47
+            </td>
48
+            <td>
49
+              <% if agent.can_receive_events? %>
50
+                <%= agent.last_receive_at ? time_ago_in_words(agent.last_receive_at) + " ago" : "never" %>
51
+              <% else %>
52
+                <span class='not-applicable'></span>
53
+              <% end %>
54
+            </td>
55
+            <td>
56
+              <% if agent.can_create_events? %>
57
+                <%= link_to(agent.events_count || 0, events_path(:agent => agent.to_param)) %>
58
+              <% else %>
59
+                <span class='not-applicable'></span>
60
+              <% end %>
61
+            </td>
62
+            <td><%= working(agent) %></td>
63
+            <td>
64
+              <div class="btn-group">
65
+                <%= link_to 'Show', agent_path(agent), class: "btn btn-mini" %>
66
+                <%= link_to 'Edit', edit_agent_path(agent), class: "btn btn-mini" %>
67
+                <%= link_to 'Delete', agent_path(agent), method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-mini" %>
68
+                <% if agent.can_be_scheduled? %>
69
+                  <%= link_to 'Run', run_agent_path(agent, :return => "index"), method: :post, class: "btn btn-mini" %>
38 70
                 <% else %>
39
-                    <%= agent.last_receive_at ? time_ago_in_words(agent.last_receive_at) + " ago" : "never" %>
71
+                  <%= link_to 'Run', "#", class: "btn btn-mini disabled" %>
40 72
                 <% end %>
41
-              </td>
42
-              <td><%= link_to(agent.events_count || 0, events_path(:agent => agent.to_param)) %></td>
43
-              <td><%= (agent.schedule || "n/a").to_s.humanize.titleize %></td>
44
-              <td><%= working(agent) %></td>
45
-              <td>
46
-                <div class="btn-group">
47
-                  <%= link_to 'Show', agent_path(agent), class: "btn btn-mini" %>
48
-                  <%= link_to 'Edit', edit_agent_path(agent), class: "btn btn-mini" %>
49
-                  <%= link_to 'Delete', agent_path(agent), method: :delete, data: {confirm: 'Are you sure?'}, class: "btn btn-mini" %>
50
-                  <% if agent.can_be_scheduled? %>
51
-                      <%= link_to 'Run', run_agent_path(agent, :return => "index"), method: :post, class: "btn btn-mini" %>
52
-                  <% else %>
53
-                      <%= link_to 'Run', "#", class: "btn btn-mini disabled" %>
54
-                  <% end %>
55
-                </div>
56
-              </td>
57
-            </tr>
73
+              </div>
74
+            </td>
75
+          </tr>
58 76
         <% end %>
59 77
       </table>
60 78
 

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

@@ -4,7 +4,7 @@
4 4
       <div class="page-header">
5 5
         <h2>
6 6
           Create a new Agent
7
-          <%= image_tag "spinner-arrows.gif", :class => "spinner" %>
7
+          <%= image_tag "spinner-arrows.gif", :class => "spinner", :id => 'agent-spinner' %>
8 8
         </h2>
9 9
       </div>
10 10
 

+ 52 - 30
app/views/agents/show.html.erb

@@ -13,7 +13,7 @@
13 13
           <% end %>
14 14
           <li><a href="#logs" data-toggle="tab" data-agent-id="<%= @agent.id %>"><i class='icon-list-alt'></i> Logs</a></li>
15 15
 
16
-          <% if @agent.events.count > 0 %>
16
+          <% if @agent.can_create_events? && @agent.events.count > 0 %>
17 17
             <li><%= link_to '<i class="icon-random"></i> Events'.html_safe, events_path(:agent => @agent.to_param) %></li>
18 18
           <% end %>
19 19
           <li><%= link_to '<i class="icon-chevron-left"></i> Back'.html_safe, agents_path %></li>
@@ -29,7 +29,7 @@
29 29
                   </li>
30 30
                 <% end %>
31 31
 
32
-                <% if @agent.events.count > 0 %>
32
+                <% if @agent.can_create_events? && @agent.events.count > 0 %>
33 33
                   <li>
34 34
                     <%= link_to '<i class="icon-trash"></i> Delete all events'.html_safe, remove_events_agent_path(@agent), method: :delete, data: {confirm: 'Are you sure you want to delete ALL events for this Agent?'}, :tabindex => "-1" %>
35 35
                   </li>
@@ -70,38 +70,60 @@
70 70
               <%= @agent.short_type.titleize %>
71 71
             </p>
72 72
 
73
-            <p>
74
-              <b>Schedule:</b>
75
-              <%= (@agent.schedule || "n/a").humanize.titleize %>
76
-            </p>
73
+            <% if @agent.can_be_scheduled? %>
74
+              <p>
75
+                <b>Schedule:</b>
76
+                <%= (@agent.schedule || "n/a").humanize.titleize %>
77
+              </p>
77 78
 
78
-            <p>
79
-              <b>Last checked:</b>
80
-              <% if @agent.cannot_be_scheduled? %>
81
-                N/A
82
-              <% else %>
79
+              <p>
80
+                <b>Last checked:</b>
83 81
                 <%= @agent.last_check_at ? time_ago_in_words(@agent.last_check_at) + " ago" : "never" %>
84
-              <% end %>
85
-            </p>
82
+              </p>
83
+            <% end %>
86 84
 
87
-            <p>
88
-              <b>Last event created:</b>
89
-              <%= @agent.last_event_at ? time_ago_in_words(@agent.last_event_at) + " ago" : "never" %>
90
-            </p>
85
+            <% if @agent.can_create_events? %>
86
+              <p>
87
+                <b>Last event created:</b>
88
+                <%= @agent.last_event_at ? time_ago_in_words(@agent.last_event_at) + " ago" : "never" %>
89
+              </p>
90
+            <% end %>
91 91
 
92
-            <p>
93
-              <b>Last received event:</b>
94
-              <% if @agent.cannot_receive_events? %>
95
-                N/A
96
-              <% else %>
92
+            <% if @agent.can_receive_events? %>
93
+              <p>
94
+                <b>Last received event:</b>
97 95
                 <%= @agent.last_receive_at ? time_ago_in_words(@agent.last_receive_at) + " ago" : "never" %>
98
-              <% end %>
99
-            </p>
96
+              </p>
97
+            <% end %>
100 98
 
101
-            <p>
102
-              <b>Event count:</b>
103
-              <%= link_to @agent.events.count, events_path(:agent => @agent.to_param) %>
104
-            </p>
99
+            <% if @agent.can_create_events? %>
100
+              <p>
101
+                <b>Events created:</b>
102
+                <%= link_to @agent.events.count, events_path(:agent => @agent.to_param) %>
103
+              </p>
104
+            <% end %>
105
+
106
+            <% if @agent.can_receive_events? %>
107
+              <p>
108
+                <b>Event sources:</b>
109
+                <% if @agent.sources.length %>
110
+                  <%= @agent.sources.map { |source_agent| link_to(source_agent.name, agent_path(source_agent)) }.to_sentence.html_safe %>
111
+                <% else %>
112
+                  None
113
+                <% end %>
114
+              </p>
115
+            <% end %>
116
+
117
+            <% if @agent.can_create_events? %>
118
+              <p>
119
+                <b>Event receivers:</b>
120
+                <% if @agent.receivers.length %>
121
+                  <%= @agent.receivers.map { |receiver_agent| link_to(receiver_agent.name, agent_path(receiver_agent)) }.to_sentence.html_safe %>
122
+                <% else %>
123
+                  None
124
+                <% end %>
125
+              </p>
126
+            <% end %>
105 127
 
106 128
             <p>
107 129
               <b>Working:</b>
@@ -110,12 +132,12 @@
110 132
 
111 133
             <p>
112 134
               <b>Options:</b>
113
-              <pre><%= JSON.pretty_generate @agent.options %></pre>
135
+              <pre><%= JSON.pretty_generate @agent.options || {} %></pre>
114 136
             </p>
115 137
 
116 138
             <p>
117 139
               <b>Memory:</b>
118
-              <pre><%= JSON.pretty_generate @agent.memory %></pre>
140
+              <pre><%= JSON.pretty_generate @agent.memory || {} %></pre>
119 141
             </p>
120 142
           </div>
121 143
         </div>

+ 1 - 0
app/views/events/index.html.erb

@@ -16,6 +16,7 @@
16 16
         </tr>
17 17
 
18 18
       <% @events.each do |event| %>
19
+        <% next unless event.agent %>
19 20
         <tr>
20 21
           <td><%= link_to event.agent.name, agent_path(event.agent) %></td>
21 22
           <td><%= time_ago_in_words event.created_at %> ago</td>

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

@@ -7,7 +7,7 @@
7 7
 
8 8
       <p>
9 9
         <b>Payload:</b>
10
-        <pre><%= JSON.pretty_generate @event.payload %></pre>
10
+        <pre><%= JSON.pretty_generate @event.payload || {} %></pre>
11 11
       </p>
12 12
 
13 13
       <% if @event.lat && @event.lng %>

+ 12 - 0
app/views/layouts/_navigation.html.erb

@@ -9,11 +9,18 @@
9 9
 
10 10
 <ul class="nav pull-right">
11 11
   <% if user_signed_in? %>
12
+
13
+    <form class="navbar-search pull-left">
14
+      <input type="text" class="search-query" id='agent-navigate' autocomplete="off" placeholder="Search">
15
+      <%= image_tag "spinner-arrows.gif", :class => "spinner" %>
16
+    </form>
17
+
12 18
     <li id='job-indicator'>
13 19
       <a href="/delayed_job">
14 20
         <span class="badge"><i class="icon-refresh icon-white"></i> <span class='number'>0</span></span>
15 21
       </a>
16 22
     </li>
23
+
17 24
     <li id='event-indicator'>
18 25
       <a href="#">
19 26
         <span class="badge"><i class="icon-random icon-white"></i> <span class='number'>0</span> new events</span>
@@ -36,6 +43,10 @@
36 43
       </li>
37 44
 
38 45
       <li>
46
+        <%= link_to 'About', 'https://github.com/cantino/huginn', :tabindex => "-1" %>
47
+      </li>
48
+
49
+      <li>
39 50
         <% if user_signed_in? %>
40 51
           <%= link_to 'Logout', destroy_user_session_path, :method => :delete, :tabindex => "-1" %>
41 52
         <% else %>
@@ -45,3 +56,4 @@
45 56
     </ul>
46 57
   </li>
47 58
 </ul>
59
+

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

@@ -32,5 +32,21 @@
32 32
         </div>
33 33
       </div>
34 34
     </div>
35
+
36
+    <script>
37
+      var agentPaths = {};
38
+      <% if current_user -%>
39
+        var myAgents = <%= Utils.jsonify(current_user.agents.inject({}) {|m, a| m[a.name] = agent_path(a) unless a.new_record?; m }) %>;
40
+        $.extend(agentPaths, myAgents);
41
+      <% end -%>
42
+      agentPaths["All Agents Index"] = <%= Utils.jsonify agents_path %>;
43
+      agentPaths["New Agent"] = <%= Utils.jsonify new_agent_path %>;
44
+      agentPaths["Account"] = <%= Utils.jsonify edit_user_registration_path %>;
45
+      agentPaths["Events Index"] = <%= Utils.jsonify events_path %>;
46
+      agentPaths["View Agent Diagram"] = <%= Utils.jsonify diagram_agents_path %>;
47
+      agentPaths["Run Event Propagation"] = { url: <%= Utils.jsonify propagate_agents_path %>, method: 'POST' };
48
+      var agentNames = [];
49
+      $.each(agentPaths, function(name, v) { agentNames.push(name); });
50
+    </script>
35 51
   </body>
36
-</html>
52
+</html>

+ 9 - 4
app/views/system_mailer/send_message.html.erb

@@ -7,10 +7,15 @@
7 7
     <% if @headline %>
8 8
       <h1><%= @headline %></h1>
9 9
     <% end %>
10
-    <% @lines.each do |line| %>
11
-      <p>
12
-        <%= line %>
13
-      </p>
10
+    <% @groups.each do |group| %>
11
+      <div style='margin-bottom: 10px;'>
12
+        <div><%= group[:title] %></div>
13
+        <% group[:entries].each do |entry| %>
14
+          <div style='margin-left: 10px;'>
15
+            <%= entry %>
16
+          </div>
17
+        <% end %>
18
+      </div>
14 19
     <% end %>
15 20
   </body>
16 21
 </html>

+ 4 - 2
app/views/system_mailer/send_message.text.erb

@@ -1,5 +1,7 @@
1 1
 <% if @headline %><%= @headline %>
2 2
 
3
-<% end %><% @lines.each do |line| %><%= line %>
4
-
3
+<% end %><% @groups.each do |group| %><%= group[:title] %>
4
+<% group[:entries].each do |entry| %>  <%= entry %>
5 5
 <% end %>
6
+
7
+<% end %>

+ 1 - 0
config/routes.rb

@@ -2,6 +2,7 @@ Huginn::Application.routes.draw do
2 2
   resources :agents do
3 3
     member do
4 4
       post :run
5
+      post :handle_details_post
5 6
       delete :remove_events
6 7
     end
7 8
 

+ 4 - 0
lib/utils.rb

@@ -70,4 +70,8 @@ module Utils
70 70
       result
71 71
     end
72 72
   end
73
+
74
+  def self.jsonify(thing)
75
+    thing.to_json.gsub('</', '<\/').html_safe
76
+  end
73 77
 end

+ 16 - 0
spec/controllers/agents_controller_spec.rb

@@ -18,6 +18,22 @@ describe AgentsController do
18 18
     end
19 19
   end
20 20
 
21
+  describe "POST handle_details_post" do
22
+    it "passes control to handle_details_post on the agent" do
23
+      sign_in users(:bob)
24
+      post :handle_details_post, :id => agents(:bob_manual_event_agent).to_param, :payload => { :foo => "bar" }
25
+      JSON.parse(response.body).should == { "success" => true }
26
+      agents(:bob_manual_event_agent).events.last.payload.should == { :foo => "bar" }
27
+    end
28
+
29
+    it "can only be accessed by the Agent's owner" do
30
+      sign_in users(:jane)
31
+      lambda {
32
+        post :handle_details_post, :id => agents(:bob_manual_event_agent).to_param, :payload => { :foo => :bar }
33
+      }.should raise_error(ActiveRecord::RecordNotFound)
34
+    end
35
+  end
36
+
21 37
   describe "GET show" do
22 38
     it "only shows Agents for the current user" do
23 39
       sign_in users(:bob)

+ 5 - 0
spec/fixtures/agents.yml

@@ -92,3 +92,8 @@ bob_twitter_user_agent:
92 92
       :oauth_token => "---",
93 93
       :oauth_token_secret => "---"
94 94
     }.to_yaml.inspect %>
95
+
96
+bob_manual_event_agent:
97
+  type: Agents::ManualEventAgent
98
+  user: bob
99
+  name: "Bob's event testing agent"

+ 15 - 5
spec/models/agent_spec.rb

@@ -114,13 +114,23 @@ describe Agent do
114 114
     end
115 115
 
116 116
     describe "#create_event" do
117
+      before do
118
+        @checker = Agents::SomethingSource.new(:name => "something")
119
+        @checker.user = users(:bob)
120
+        @checker.save!
121
+      end
122
+
117 123
       it "should use the checker's user" do
118
-        checker = Agents::SomethingSource.new(:name => "something")
119
-        checker.user = users(:bob)
120
-        checker.save!
124
+        @checker.check
125
+        Event.last.user.should == @checker.user
126
+      end
121 127
 
122
-        checker.check
123
-        Event.last.user.should == checker.user
128
+      it "should log an error if the Agent has been marked with 'cannot_create_events!'" do
129
+        mock(@checker).can_create_events? { false }
130
+        lambda {
131
+          @checker.check
132
+        }.should_not change { Event.count }
133
+        @checker.logs.first.message.should =~ /cannot create events/i
124 134
       end
125 135
     end
126 136
 

+ 31 - 3
spec/models/agents/digest_email_agent_spec.rb

@@ -11,6 +11,10 @@ describe Agents::DigestEmailAgent do
11 11
     @checker.save!
12 12
   end
13 13
 
14
+  after do
15
+    ActionMailer::Base.deliveries = []
16
+  end
17
+
14 18
   describe "#receive" do
15 19
     it "queues any payloads it receives" do
16 20
       event1 = Event.new
@@ -33,14 +37,38 @@ describe Agents::DigestEmailAgent do
33 37
       Agents::DigestEmailAgent.async_check(@checker.id)
34 38
       ActionMailer::Base.deliveries.should == []
35 39
 
36
-      @checker.memory[:queue] = ["Something you should know about", { :title => "Foo", :url => "http://google.com", :bar => 2 }, { "message" => "hi", :woah => "there" }]
40
+      @checker.memory[:queue] = ["Something you should know about",
41
+                                 { :title => "Foo", :url => "http://google.com", :bar => 2 },
42
+                                 { "message" => "hi", :woah => "there" },
43
+                                 { "test" => 2 }]
37 44
       @checker.save!
38 45
 
39 46
       Agents::DigestEmailAgent.async_check(@checker.id)
40 47
       ActionMailer::Base.deliveries.last.to.should == ["bob@example.com"]
41 48
       ActionMailer::Base.deliveries.last.subject.should == "something interesting"
42
-      get_message_part(ActionMailer::Base.deliveries.last, /plain/).strip.should == "Something you should know about\n\nFoo (bar: 2 and url: http://google.com)\n\nhi (woah: there)"
43
-      @checker.reload.memory[:queue].should == []
49
+      get_message_part(ActionMailer::Base.deliveries.last, /plain/).strip.should == "Something you should know about\n\nFoo\n  bar: 2\n  url: http://google.com\n\nhi\n  woah: there\n\nEvent\n  test: 2"
50
+      @checker.reload.memory[:queue].should be_empty
51
+    end
52
+
53
+    it "can receive complex events and send them on" do
54
+      stub_request(:any, /wunderground/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/weather.json")), :status => 200)
55
+      stub.any_instance_of(Agents::WeatherAgent).is_tomorrow?(anything) { true }
56
+      @checker.sources << agents(:bob_weather_agent)
57
+
58
+      Agent.async_check(agents(:bob_weather_agent).id)
59
+
60
+      Agent.receive!
61
+      @checker.reload.memory[:queue].should_not be_empty
62
+
63
+      Agents::DigestEmailAgent.async_check(@checker.id)
64
+
65
+      plain_email_text = get_message_part(ActionMailer::Base.deliveries.last, /plain/).strip
66
+      html_email_text = get_message_part(ActionMailer::Base.deliveries.last, /html/).strip
67
+
68
+      plain_email_text.should =~ /avehumidity/
69
+      html_email_text.should =~ /avehumidity/
70
+
71
+      @checker.reload.memory[:queue].should be_empty
44 72
     end
45 73
   end
46 74
 end

+ 1 - 5
spec/models/agents/peak_detector_agent_spec.rb

@@ -69,10 +69,6 @@ describe Agents::PeakDetectorAgent do
69 69
                                   :pattern => { :filter => "something" })
70 70
       @agent.memory[:peaks][:something].length.should == 2
71 71
     end
72
-
73
-    it "works on real world data" do
74
-      pending "need examples"
75
-    end
76 72
   end
77 73
 
78 74
   describe "validation" do
@@ -95,4 +91,4 @@ describe Agents::PeakDetectorAgent do
95 91
       @agent.should_not be_valid
96 92
     end
97 93
   end
98
-end
94
+end

+ 5 - 1
vendor/assets/stylesheets/jquery.json-editor.css.scss

@@ -52,8 +52,12 @@
52 52
     display: block;
53 53
     float: right;
54 54
     text-decoration: none;
55
-    padding-left: 5px;
55
+    padding: 0 5px;
56 56
     border: 0 !important;
57 57
     color: blue;
58
+
59
+    &:hover {
60
+      background-color: #bbb;
61
+    }
58 62
   }
59 63
 }