add basic scaffold of scenarios, including the ability to add agents to scenarios, edit scenarios, and view scenarios. next step: import and export

Andrew Cantino 10 年之前
父节点
当前提交
aaa4b7a155

+ 4 - 1
app/assets/javascripts/application.js.coffee.erb

@@ -61,6 +61,9 @@ $(document).ready ->
61 61
   if $(".flash").length
62 62
     setTimeout((-> $(".flash").slideUp(-> $(".flash").remove())), 5000)
63 63
 
64
+  # Help popovers
65
+  $('.hover-help').popover(trigger: 'hover')
66
+
64 67
   # Agent Navigation
65 68
   $agentNavigate = $('#agent-navigate')
66 69
 
@@ -99,7 +102,7 @@ $(document).ready ->
99 102
         e.preventDefault()
100 103
         $agentNavigate.focus()
101 104
 
102
-# Agent Show
105
+  # Agent Show
103 106
   fetchLogs = (e) ->
104 107
     agentId = $(e.target).closest("[data-agent-id]").data("agent-id")
105 108
     e.preventDefault()

+ 11 - 0
app/assets/stylesheets/application.css.scss.erb

@@ -147,3 +147,14 @@ tr.agent-disabled {
147 147
 .json-editor blockquote {
148 148
   font-size: 14px;
149 149
 }
150
+
151
+// Position tweeks
152
+
153
+.hover-help {
154
+  top: 2px;
155
+}
156
+
157
+h2 .scenario {
158
+  position: relative;
159
+  top: -2px;
160
+}

+ 87 - 0
app/controllers/scenarios_controller.rb

@@ -0,0 +1,87 @@
1
+class ScenariosController < ApplicationController
2
+  def index
3
+    @scenarios = current_user.scenarios.page(params[:page])
4
+
5
+    respond_to do |format|
6
+      format.html
7
+      format.json { render json: @scenarios }
8
+    end
9
+  end
10
+
11
+  def new
12
+    @scenario = current_user.scenarios.build
13
+
14
+    respond_to do |format|
15
+      format.html
16
+      format.json { render json: @scenario }
17
+    end
18
+  end
19
+
20
+  def show
21
+    @scenario = current_user.scenarios.find(params[:id])
22
+    @agents = @scenario.agents.preload(:scenarios).page(params[:page])
23
+
24
+    respond_to do |format|
25
+      format.html
26
+      format.json { render json: @scenario }
27
+    end
28
+  end
29
+
30
+  # Share is a work in progress!
31
+  def share
32
+    @scenario = current_user.scenarios.find(params[:id])
33
+    @agents = @scenario.agents.preload(:scenarios).page(params[:page])
34
+
35
+    respond_to do |format|
36
+      format.html
37
+      format.json { render json: @scenario }
38
+    end
39
+  end
40
+
41
+  def edit
42
+    @scenario = current_user.scenarios.find(params[:id])
43
+
44
+    respond_to do |format|
45
+      format.html
46
+      format.json { render json: @scenario }
47
+    end
48
+  end
49
+
50
+  def create
51
+    @scenario = current_user.scenarios.build(params[:scenario])
52
+
53
+    respond_to do |format|
54
+      if @scenario.save
55
+        format.html { redirect_to @scenario, notice: 'This Scenario was successfully created.' }
56
+        format.json { render json: @scenario, status: :created, location: @scenario }
57
+      else
58
+        format.html { render action: "new" }
59
+        format.json { render json: @scenario.errors, status: :unprocessable_entity }
60
+      end
61
+    end
62
+  end
63
+
64
+  def update
65
+    @scenario = current_user.scenarios.find(params[:id])
66
+
67
+    respond_to do |format|
68
+      if @scenario.update_attributes(params[:scenario])
69
+        format.html { redirect_to @scenario, notice: 'This Scenario was successfully updated.' }
70
+        format.json { head :no_content }
71
+      else
72
+        format.html { render action: "edit" }
73
+        format.json { render json: @scenario.errors, status: :unprocessable_entity }
74
+      end
75
+    end
76
+  end
77
+
78
+  def destroy
79
+    @scenario = current_user.scenarios.find(params[:id])
80
+    @scenario.destroy
81
+
82
+    respond_to do |format|
83
+      format.html { redirect_to scenarios_path }
84
+      format.json { head :no_content }
85
+    end
86
+  end
87
+end

+ 6 - 0
app/helpers/agent_helper.rb

@@ -6,6 +6,12 @@ module AgentHelper
6 6
     end
7 7
   end
8 8
 
9
+  def scenario_links(agent)
10
+    agent.scenarios.map { |scenario|
11
+      link_to(scenario.name, scenario, class: "label label-info")
12
+    }.to_sentence
13
+  end
14
+
9 15
   def agent_show_class(agent)
10 16
     agent.short_type.underscore.dasherize
11 17
   end

+ 8 - 1
app/models/agent.rb

@@ -22,13 +22,14 @@ class Agent < ActiveRecord::Base
22 22
 
23 23
   EVENT_RETENTION_SCHEDULES = [["Forever", 0], ["1 day", 1], *([2, 3, 4, 5, 7, 14, 21, 30, 45, 90, 180, 365].map {|n| ["#{n} days", n] })]
24 24
 
25
-  attr_accessible :options, :memory, :name, :type, :schedule, :disabled, :source_ids, :keep_events_for, :propagate_immediately
25
+  attr_accessible :options, :memory, :name, :type, :schedule, :disabled, :source_ids, :scenario_ids, :keep_events_for, :propagate_immediately
26 26
 
27 27
   json_serialize :options, :memory
28 28
 
29 29
   validates_presence_of :name, :user
30 30
   validates_inclusion_of :keep_events_for, :in => EVENT_RETENTION_SCHEDULES.map(&:last)
31 31
   validate :sources_are_owned
32
+  validate :scenarios_are_owned
32 33
   validate :validate_schedule
33 34
   validate :validate_options
34 35
 
@@ -48,6 +49,8 @@ class Agent < ActiveRecord::Base
48 49
   has_many :links_as_receiver, :dependent => :delete_all, :foreign_key => "receiver_id", :class_name => "Link", :inverse_of => :receiver
49 50
   has_many :sources, :through => :links_as_receiver, :class_name => "Agent", :inverse_of => :receivers
50 51
   has_many :receivers, :through => :links_as_source, :class_name => "Agent", :inverse_of => :sources
52
+  has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :agent
53
+  has_many :scenarios, :through => :scenario_memberships, :inverse_of => :agents
51 54
 
52 55
   scope :of_type, lambda { |type|
53 56
     type = case type
@@ -210,6 +213,10 @@ class Agent < ActiveRecord::Base
210 213
     errors.add(:sources, "must be owned by you") unless sources.all? {|s| s.user == user }
211 214
   end
212 215
   
216
+  def scenarios_are_owned
217
+    errors.add(:scenarios, "must be owned by you") unless scenarios.all? {|s| s.user == user }
218
+  end
219
+
213 220
   def validate_schedule
214 221
     unless cannot_be_scheduled?
215 222
       errors.add(:schedule, "is not a valid schedule") unless SCHEDULES.include?(schedule.to_s)

+ 17 - 0
app/models/scenario.rb

@@ -0,0 +1,17 @@
1
+class Scenario < ActiveRecord::Base
2
+  attr_accessible :name, :agent_ids
3
+
4
+  belongs_to :user, :counter_cache => :scenario_count, :inverse_of => :scenarios
5
+  has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :scenario
6
+  has_many :agents, :through => :scenario_memberships, :inverse_of => :scenarios
7
+
8
+  validates_presence_of :name, :user
9
+
10
+  validate :agents_are_owned
11
+
12
+  protected
13
+
14
+  def agents_are_owned
15
+    errors.add(:agents, "must be owned by you") unless agents.all? {|s| s.user == user }
16
+  end
17
+end

+ 4 - 0
app/models/scenario_membership.rb

@@ -0,0 +1,4 @@
1
+class ScenarioMembership < ActiveRecord::Base
2
+  belongs_to :agent, :inverse_of => :scenario_memberships
3
+  belongs_to :scenario, :inverse_of => :scenario_memberships
4
+end

+ 1 - 0
app/models/user.rb

@@ -26,6 +26,7 @@ class User < ActiveRecord::Base
26 26
   has_many :events, -> { order("events.created_at desc") }, :dependent => :delete_all, :inverse_of => :user
27 27
   has_many :agents, -> { order("agents.created_at desc") }, :dependent => :destroy, :inverse_of => :user
28 28
   has_many :logs, :through => :agents, :class_name => "AgentLog"
29
+  has_many :scenarios, :inverse_of => :user, :dependent => :destroy
29 30
 
30 31
   # Allow users to login via either email or username.
31 32
   def self.find_first_by_auth_conditions(warden_conditions)

+ 12 - 0
app/views/agents/_form.html.erb

@@ -41,6 +41,7 @@
41 41
           <div class='event-related-region' data-can-create-events="<%= @agent.can_create_events? %>">
42 42
             <div class="form-group">
43 43
               <%= f.label :keep_events_for, "Keep events" %>
44
+              <span class="glyphicon glyphicon-question-sign hover-help" data-content="In order to conserve disk space, you can choose to have events created by this Agent expire after a certain period of time.  Make sure you keep them long enough to allow any subsequent Agents to make use of them."></span>
44 45
               <%= f.select :keep_events_for, options_for_select(Agent::EVENT_RETENTION_SCHEDULES, @agent.keep_events_for), {}, :class => 'form-control' %>
45 46
             </div>
46 47
           </div>
@@ -59,6 +60,17 @@
59 60
               <% end %>
60 61
             </div>
61 62
           </div>
63
+
64
+          <% if current_user.scenario_count > 0 %>
65
+            <div class="form-group">
66
+              <%= f.label :scenarios %>
67
+              <span class="glyphicon glyphicon-question-sign hover-help" data-content="Use Scenarios to group sets of Agents, both for organization, and to make them easy to export and share."></span>
68
+              <%= f.select(:scenario_ids,
69
+                           options_for_select(current_user.scenarios.pluck(:name, :id), @agent.scenario_ids),
70
+                           {}, { :multiple => true, :size => 5, :class => 'select2 form-control' }) %>
71
+            </div>
72
+          <% end %>
73
+
62 74
         </div>
63 75
 
64 76
         <!-- Form controls full width -->

+ 79 - 0
app/views/agents/_table.html.erb

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

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

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

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

@@ -45,7 +45,7 @@
45 45
               </li>
46 46
 
47 47
               <li>
48
-                <%= link_to '<span class="glyphicon glyphicon-remove"></span> Delete'.html_safe, agent_path(@agent), method: :delete, data: { confirm: 'Are you sure?' }, :tabindex => "-1" %>
48
+                <%= link_to '<span class="glyphicon glyphicon-remove"></span> Delete'.html_safe, agent_path(@agent), method: :delete, data: { confirm: 'Are you sure you wish to permenantly delete this Agent?' }, :tabindex => "-1" %>
49 49
               </li>
50 50
             </ul>
51 51
           </li>

+ 2 - 2
app/views/events/index.html.erb

@@ -20,13 +20,13 @@
20 20
           <% next unless event.agent %>
21 21
           <tr>
22 22
             <td><%= link_to event.agent.name, agent_path(event.agent) %></td>
23
-            <td><%= time_ago_in_words event.created_at %> ago</td>
23
+            <td title='<%= event.created_at %>'><%= time_ago_in_words event.created_at %> ago</td>
24 24
             <td class='payload'><%= truncate event.payload.to_json, :length => 90, :omission => "" %></td>
25 25
             <td>
26 26
               <div class="btn-group btn-group-xs">
27 27
                 <%= link_to 'Show', event_path(event), class: "btn btn-default" %>
28 28
                 <%= link_to 'Re-emit', reemit_event_path(event), method: :post, data: { confirm: 'Are you sure you want to duplicate this event and emit the new one now?' }, class: "btn btn-default" %>
29
-                <%= link_to 'Delete', event_path(event), method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-default btn-danger" %>
29
+                <%= link_to 'Delete', event_path(event), method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-default" %>
30 30
               </div>
31 31
             </td>
32 32
           </tr>

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

@@ -1,7 +1,7 @@
1 1
 <% if flash.keys.length > 0 %>
2 2
   <div class="flash">
3 3
     <% flash.each do |name, msg| %>
4
-      <div class="alert alert-<%= name.to_sym == :notice ? "success" : "error" %> alert-dismissable">
4
+      <div class="alert alert-<%= name.to_sym == :notice ? "success" : "danger" %> alert-dismissable">
5 5
         <button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
6 6
         <%= content_tag :div, msg, :id => "flash_#{name}" if msg.is_a?(String) %>
7 7
       </div>

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

@@ -13,6 +13,7 @@
13 13
   <% if user_signed_in? %>
14 14
     <ul class='nav navbar-nav'>
15 15
       <%= nav_link "Agents", agents_path %>
16
+      <%= nav_link "Scenarios", scenarios_path %>
16 17
       <%= nav_link "Events", events_path %>
17 18
       <%= nav_link "Credentials", user_credentials_path %>
18 19
     </ul>

+ 40 - 0
app/views/scenarios/_form.html.erb

@@ -0,0 +1,40 @@
1
+<%= form_for(@scenario, :method => @scenario.new_record? ? "POST" : "PUT") do |f| %>
2
+  <% if @scenario.errors.any? %>
3
+    <div class="row well">
4
+      <h2><%= pluralize(@scenario.errors.count, "error") %> prohibited this Scenario from being saved:</h2>
5
+      <% @scenario.errors.full_messages.each do |msg| %>
6
+        <p class='text-warning'><%= msg %></p>
7
+      <% end %>
8
+    </div>
9
+  <% end %>
10
+
11
+  <div class="row">
12
+    <div class="col-md-4">
13
+      <div class="form-group">
14
+        <%= f.label :name %>
15
+        <%= f.text_field :name, :class => 'form-control' %>
16
+      </div>
17
+    </div>
18
+  </div>
19
+
20
+  <div class="row">
21
+    <div class="col-md-4">
22
+      <div class="form-group">
23
+        <div>
24
+          <%= f.label :agents %>
25
+          <%= f.select(:agent_ids,
26
+                       options_for_select(current_user.agents.pluck(:name, :id), @scenario.agent_ids),
27
+                       {}, { :multiple => true, :size => 5, :class => 'select2 form-control' }) %>
28
+        </div>
29
+      </div>
30
+    </div>
31
+  </div>
32
+
33
+  <div class="row">
34
+    <div class="col-md-12">
35
+      <div class='form-actions' style='clear: both'>
36
+        <%= f.submit "Save Scenario", :class => "btn btn-primary" %>
37
+      </div>
38
+    </div>
39
+  </div>
40
+<% end %>

+ 21 - 0
app/views/scenarios/edit.html.erb

@@ -0,0 +1,21 @@
1
+<div class='container'>
2
+  <div class='row'>
3
+    <div class='col-md-12'>
4
+      <div class="page-header">
5
+        <h2>
6
+          Edit Scenario
7
+        </h2>
8
+      </div>
9
+
10
+      <%= render 'form' %>
11
+
12
+      <hr>
13
+
14
+      <div class="row">
15
+        <div class="col-md-12">
16
+          <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %>
17
+        </div>
18
+      </div>
19
+    </div>
20
+  </div>
21
+</div>

+ 46 - 0
app/views/scenarios/index.html.erb

@@ -0,0 +1,46 @@
1
+<div class='container'>
2
+  <div class='row'>
3
+    <div class='col-md-12'>
4
+      <div class="page-header">
5
+        <h2>
6
+          Your Scenarios
7
+        </h2>
8
+      </div>
9
+
10
+      <blockquote>
11
+        Scenarios are named groups of Agents.  Scenarios allow you to organize your agents, and to export sets of Agents for sharing.
12
+      </blockquote>
13
+
14
+      <table class='table table-striped'>
15
+        <tr>
16
+          <th>Name</th>
17
+          <th>Agents</th>
18
+          <th></th>
19
+        </tr>
20
+
21
+        <% @scenarios.each do |scenario| %>
22
+          <tr>
23
+            <td><span class='label label-info'><%= scenario.name %></span></td>
24
+            <td><%= link_to pluralize(scenario.agents.count, "agent"), scenario %></td>
25
+            <td>
26
+              <div class="btn-group btn-group-xs" style="float: right">
27
+                <%= link_to 'Show', scenario, class: "btn btn-default" %>
28
+                <%= link_to 'Edit', edit_scenario_path(scenario), class: "btn btn-default" %>
29
+                <%= link_to 'Share', share_scenario_path(scenario), class: "btn btn-default" %>
30
+                <%= link_to 'Delete', scenario_path(scenario), method: :delete, data: {confirm: 'Are you sure?'}, class: "btn btn-default" %>
31
+              </div>
32
+            </td>
33
+          </tr>
34
+        <% end %>
35
+      </table>
36
+
37
+      <%= paginate @scenarios, :theme => 'twitter-bootstrap' %>
38
+
39
+      <br/>
40
+
41
+      <div class="btn-group">
42
+        <%= link_to '<span class="glyphicon glyphicon-plus"></span> New Scenario'.html_safe, new_scenario_path, class: "btn btn-default" %>
43
+      </div>
44
+    </div>
45
+  </div>
46
+</div>

+ 21 - 0
app/views/scenarios/new.html.erb

@@ -0,0 +1,21 @@
1
+<div class='container'>
2
+  <div class='row'>
3
+    <div class='col-md-12'>
4
+      <div class="page-header">
5
+        <h2>
6
+          Create a new Scenario
7
+        </h2>
8
+      </div>
9
+
10
+      <%= render 'form' %>
11
+
12
+      <hr>
13
+
14
+      <div class="row">
15
+        <div class="col-md-12">
16
+          <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %>
17
+        </div>
18
+      </div>
19
+    </div>
20
+  </div>
21
+</div>

+ 17 - 0
app/views/scenarios/share.html.erb

@@ -0,0 +1,17 @@
1
+<div class='container'>
2
+  <div class='row'>
3
+    <div class='col-md-12'>
4
+      <div class="page-header">
5
+        <h2>Share <span class='label label-info scenario'><%= @scenario.name %></span> with the world</h2>
6
+      </div>
7
+
8
+      <hr>
9
+
10
+      <div class="row">
11
+        <div class="col-md-12">
12
+          <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %>
13
+        </div>
14
+      </div>
15
+    </div>
16
+  </div>
17
+</div>

+ 21 - 0
app/views/scenarios/show.html.erb

@@ -0,0 +1,21 @@
1
+<div class='container'>
2
+  <div class='row'>
3
+    <div class='col-md-12'>
4
+      <div class="page-header">
5
+        <h2>Scenario <span class='label label-info scenario'><%= @scenario.name %></span></h2>
6
+      </div>
7
+
8
+      <div class="btn-group">
9
+        <%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %>
10
+        <%= link_to '<span class="glyphicon glyphicon-edit"></span> Edit'.html_safe, edit_scenario_path(@scenario), class: "btn btn-default" %>
11
+        <%= link_to '<span class="glyphicon glyphicon-share-alt"></span> Share'.html_safe, share_scenario_path(@scenario), class: "btn btn-default" %>
12
+      </div>
13
+
14
+      <div class="page-header">
15
+        <h3>Agents</h3>
16
+      </div>
17
+
18
+      <%= render :partial => 'agents/table' %>
19
+    </div>
20
+  </div>
21
+</div>

+ 6 - 0
config/routes.rb

@@ -26,6 +26,12 @@ Huginn::Application.routes.draw do
26 26
     end
27 27
   end
28 28
 
29
+  resources :scenarios do
30
+    member do
31
+      get :share
32
+    end
33
+  end
34
+
29 35
   resources :user_credentials, :except => :show
30 36
 
31 37
   get "/worker_status" => "worker_status#show"

+ 12 - 0
db/migrate/20140509170420_create_scenarios.rb

@@ -0,0 +1,12 @@
1
+class CreateScenarios < ActiveRecord::Migration
2
+  def change
3
+    create_table :scenarios do |t|
4
+      t.string :name, :null => false
5
+      t.integer :user_id, :null => false
6
+
7
+      t.timestamps
8
+    end
9
+
10
+    add_column :users, :scenario_count, :integer, :null => false, :default => 0
11
+  end
12
+end

+ 10 - 0
db/migrate/20140509170443_create_scenario_memberships.rb

@@ -0,0 +1,10 @@
1
+class CreateScenarioMemberships < ActiveRecord::Migration
2
+  def change
3
+    create_table :scenario_memberships do |t|
4
+      t.integer :agent_id, :null => false
5
+      t.integer :scenario_id, :null => false
6
+
7
+      t.timestamps
8
+    end
9
+  end
10
+end

+ 15 - 0
db/schema.rb

@@ -101,6 +101,20 @@ ActiveRecord::Schema.define(:version => 20140408150825) do
101 101
 
102 102
   add_index "user_credentials", ["user_id", "credential_name"], :name => "index_user_credentials_on_user_id_and_credential_name", :unique => true
103 103
 
104
+  create_table "scenario_memberships", force: true do |t|
105
+    t.integer  "agent_id",    null: false
106
+    t.integer  "scenario_id", null: false
107
+    t.datetime "created_at"
108
+    t.datetime "updated_at"
109
+  end
110
+
111
+  create_table "scenarios", force: true do |t|
112
+    t.string   "name",       null: false
113
+    t.integer  "user_id",    null: false
114
+    t.datetime "created_at"
115
+    t.datetime "updated_at"
116
+  end
117
+
104 118
   create_table "users", :force => true do |t|
105 119
     t.string   "email",                  :default => "",    :null => false
106 120
     t.string   "encrypted_password",     :default => "",    :null => false
@@ -120,6 +134,7 @@ ActiveRecord::Schema.define(:version => 20140408150825) do
120 134
     t.datetime "locked_at"
121 135
     t.string   "username",                                  :null => false
122 136
     t.string   "invitation_code",                           :null => false
137
+    t.integer  "scenario_count",         default: 0,     null: false
123 138
   end
124 139
 
125 140
   add_index "users", ["email"], :name => "index_users_on_email", :unique => true

+ 98 - 0
spec/controllers/scenarios_controller_spec.rb

@@ -0,0 +1,98 @@
1
+require 'spec_helper'
2
+
3
+describe ScenariosController do
4
+  def valid_attributes(options = {})
5
+    { :name => "some_name" }.merge(options)
6
+  end
7
+
8
+  before do
9
+    sign_in users(:bob)
10
+  end
11
+
12
+  describe "GET index" do
13
+    it "only returns Scenarios for the current user" do
14
+      get :index
15
+      assigns(:scenarios).all? {|i| i.user.should == users(:bob) }.should be_true
16
+    end
17
+  end
18
+
19
+  describe "GET show" do
20
+    it "only shows Scenarios for the current user" do
21
+      get :show, :id => scenarios(:bob_weather).to_param
22
+      assigns(:scenario).should eq(scenarios(:bob_weather))
23
+
24
+      lambda {
25
+        get :show, :id => scenarios(:jane_weather).to_param
26
+      }.should raise_error(ActiveRecord::RecordNotFound)
27
+    end
28
+
29
+    it "loads Agents for the requested Scenario" do
30
+      get :show, :id => scenarios(:bob_weather).to_param
31
+      assigns(:agents).pluck(:id).should eq(scenarios(:bob_weather).agents.pluck(:id))
32
+    end
33
+  end
34
+
35
+  describe "GET edit" do
36
+    it "only shows Scenarios for the current user" do
37
+      get :edit, :id => scenarios(:bob_weather).to_param
38
+      assigns(:scenario).should eq(scenarios(:bob_weather))
39
+
40
+      lambda {
41
+        get :edit, :id => scenarios(:jane_weather).to_param
42
+      }.should raise_error(ActiveRecord::RecordNotFound)
43
+    end
44
+  end
45
+
46
+  describe "POST create" do
47
+    it "creates Scenarios for the current user" do
48
+      expect {
49
+        post :create, :scenario => valid_attributes
50
+      }.to change { users(:bob).scenarios.count }.by(1)
51
+    end
52
+
53
+    it "shows errors" do
54
+      expect {
55
+        post :create, :scenario => valid_attributes(:name => "")
56
+      }.not_to change { users(:bob).scenarios.count }
57
+      assigns(:scenario).should have(1).errors_on(:name)
58
+      response.should render_template("new")
59
+    end
60
+
61
+    it "will not create Scenarios for other users" do
62
+      expect {
63
+        post :create, :scenario => valid_attributes(:user_id => users(:jane).id)
64
+      }.to raise_error(ActiveModel::MassAssignmentSecurity::Error)
65
+    end
66
+  end
67
+
68
+  describe "PUT update" do
69
+    it "updates attributes on Scenarios for the current user" do
70
+      post :update, :id => scenarios(:bob_weather).to_param, :scenario => { :name => "new_name" }
71
+      response.should redirect_to(scenario_path(scenarios(:bob_weather)))
72
+      scenarios(:bob_weather).reload.name.should == "new_name"
73
+
74
+      lambda {
75
+        post :update, :id => scenarios(:jane_weather).to_param, :scenario => { :name => "new_name" }
76
+      }.should raise_error(ActiveRecord::RecordNotFound)
77
+      scenarios(:jane_weather).reload.name.should_not == "new_name"
78
+    end
79
+
80
+    it "shows errors" do
81
+      post :update, :id => scenarios(:bob_weather).to_param, :scenario => { :name => "" }
82
+      assigns(:scenario).should have(1).errors_on(:name)
83
+      response.should render_template("edit")
84
+    end
85
+  end
86
+
87
+  describe "DELETE destroy" do
88
+    it "destroys only Scenarios owned by the current user" do
89
+      expect {
90
+        delete :destroy, :id => scenarios(:bob_weather).to_param
91
+      }.to change(Scenario, :count).by(-1)
92
+
93
+      lambda {
94
+        delete :destroy, :id => scenarios(:jane_weather).to_param
95
+      }.should raise_error(ActiveRecord::RecordNotFound)
96
+    end
97
+  end
98
+end

+ 15 - 0
spec/fixtures/scenario_memberships.yml

@@ -0,0 +1,15 @@
1
+jane_weather_agent_scenario_membership:
2
+  agent: jane_weather_agent
3
+  scenario: jane_weather
4
+
5
+jane_rain_notifier_agent_scenario_membership:
6
+  agent: jane_rain_notifier_agent
7
+  scenario: jane_weather
8
+
9
+bob_weather_agent_scenario_membership:
10
+  agent: bob_weather_agent
11
+  scenario: bob_weather
12
+
13
+bob_rain_notifier_agent_scenario_membership:
14
+  agent: bob_rain_notifier_agent
15
+  scenario: bob_weather

+ 7 - 0
spec/fixtures/scenarios.yml

@@ -0,0 +1,7 @@
1
+jane_weather:
2
+  name: Jane's weather alert Scenario
3
+  user: jane
4
+
5
+bob_weather:
6
+  name: Bob's weather alert Scenario
7
+  user: bob

+ 3 - 1
spec/fixtures/users.yml

@@ -4,8 +4,10 @@ bob:
4 4
   email: "bob@example.com"
5 5
   username: bob
6 6
   invitation_code: <%= User::INVITATION_CODES.last %>
7
+  scenario_count: 1
7 8
 
8 9
 jane:
9 10
   email: "jane@example.com"
10 11
   username: jane
11
-  invitation_code: <%= User::INVITATION_CODES.last %>
12
+  invitation_code: <%= User::INVITATION_CODES.last %>
13
+  scenario_count: 1

+ 17 - 0
spec/models/agent_spec.rb

@@ -480,6 +480,23 @@ describe Agent do
480 480
         agent.should have(0).errors_on(:sources)
481 481
       end
482 482
 
483
+      it "should not allow scenarios owned by other people" do
484
+        agent = Agents::SomethingSource.new(:name => "something")
485
+        agent.user = users(:bob)
486
+
487
+        agent.scenario_ids = [scenarios(:bob_weather).id]
488
+        agent.should have(0).errors_on(:scenarios)
489
+
490
+        agent.scenario_ids = [scenarios(:bob_weather).id, scenarios(:jane_weather).id]
491
+        agent.should have(1).errors_on(:scenarios)
492
+
493
+        agent.scenario_ids = [scenarios(:jane_weather).id]
494
+        agent.should have(1).errors_on(:scenarios)
495
+
496
+        agent.user = users(:jane)
497
+        agent.should have(0).errors_on(:scenarios)
498
+      end
499
+
483 500
       it "validates keep_events_for" do
484 501
         agent = Agents::SomethingSource.new(:name => "something")
485 502
         agent.user = users(:bob)

+ 44 - 0
spec/models/scenario_spec.rb

@@ -0,0 +1,44 @@
1
+require 'spec_helper'
2
+
3
+describe Scenario do
4
+  describe "validations" do
5
+    before do
6
+      @scenario = users(:bob).scenarios.new(:name => "some scenario")
7
+      @scenario.should be_valid
8
+    end
9
+
10
+    it "validates the presence of name" do
11
+      @scenario.name = ''
12
+      @scenario.should_not be_valid
13
+    end
14
+
15
+    it "validates the presence of user" do
16
+      @scenario.user = nil
17
+      @scenario.should_not be_valid
18
+    end
19
+
20
+    it "only allows Agents owned by user" do
21
+      @scenario.agent_ids = [agents(:bob_website_agent).id]
22
+      @scenario.should be_valid
23
+
24
+      @scenario.agent_ids = [agents(:jane_website_agent).id]
25
+      @scenario.should_not be_valid
26
+    end
27
+  end
28
+
29
+  describe "counters" do
30
+    before do
31
+      @scenario = users(:bob).scenarios.new(:name => "some scenario")
32
+    end
33
+
34
+    it "maintains a counter cache on user" do
35
+      lambda {
36
+        @scenario.save!
37
+      }.should change { users(:bob).reload.scenario_count }.by(1)
38
+
39
+      lambda {
40
+        @scenario.destroy
41
+      }.should change { users(:bob).reload.scenario_count }.by(-1)
42
+    end
43
+  end
44
+end