@@ -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() |
@@ -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 |
+} |
@@ -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,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 |
@@ -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) |
@@ -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 |
@@ -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 |
@@ -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) |
@@ -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 --> |
@@ -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' %> |
@@ -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 |
|
@@ -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> |
@@ -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,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">×</button> |
6 | 6 |
<%= content_tag :div, msg, :id => "flash_#{name}" if msg.is_a?(String) %> |
7 | 7 |
</div> |
@@ -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> |
@@ -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 %> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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> |
@@ -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" |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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) |
@@ -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 |