the import process now allows you to merge your agents with the incoming ones; next step is better UI

This commit is contained in:
Andrew Cantino 2014-06-08 23:37:15 -07:00
parent bdc5638755
commit 827b62356a
13 changed files with 608 additions and 243 deletions

View file

@ -9,14 +9,17 @@
#= require ./worker-checker
#= require_self
window.setupJsonEditor = ($editor = $(".live-json-editor")) ->
window.setupJsonEditor = ($editors = $(".live-json-editor")) ->
JSONEditor.prototype.ADD_IMG = '<%= image_path 'json-editor/add.png' %>'
JSONEditor.prototype.DELETE_IMG = '<%= image_path 'json-editor/delete.png' %>'
if $editor.length
editors = []
$editors.each ->
$editor = $(this)
jsonEditor = new JSONEditor($editor, $editor.data('width') || 400, $editor.data('height') || 500)
jsonEditor.doTruncation true
jsonEditor.showFunctionButtons()
return jsonEditor
editors.push jsonEditor
return editors
hideSchedule = ->
$(".schedule-region select").hide()
@ -55,7 +58,7 @@ showEventDescriptions = ->
$(document).ready ->
# JSON Editor
window.jsonEditor = setupJsonEditor()
window.jsonEditor = setupJsonEditor()[0]
# Flash
if $(".flash").length

View file

@ -140,7 +140,11 @@ span.not-applicable:after {
opacity: 0.5;
}
// Fix JSON Editor
// JSON Editor
.live-json-editor {
font-family: "Courier New", Courier, monospace;
}
.json-editor blockquote {
font-size: 14px;

View file

@ -1,9 +1,2 @@
.scenario-import {
.danger {
color: red;
font-weight: strong;
border: 1px solid red;
padding: 10px;
margin: 10px 0;
}
}

View file

@ -11,13 +11,8 @@ class ScenarioImportsController < ApplicationController
render :text => 'Sorry, you cannot import a Scenario by URL from your own Huginn server.' and return
end
if @scenario_import.valid?
if @scenario_import.do_import?
@scenario_import.import!
redirect_to @scenario_import.scenario, notice: "Import successful!"
else
render action: "new"
end
if @scenario_import.valid? && @scenario_import.should_import? && @scenario_import.import
redirect_to @scenario_import.scenario, notice: "Import successful!"
else
render action: "new"
end

View file

@ -1,4 +1,7 @@
# This is a helper class for managing Scenario imports.
require 'ostruct'
# This is a helper class for managing Scenario imports, used by the ScenarioImportsController. This class behaves much
# like a normal ActiveRecord object, with validations and callbacks. However, it is never persisted to the database.
class ScenarioImport
include ActiveModel::Model
include ActiveModel::Callbacks
@ -7,7 +10,7 @@ class ScenarioImport
DANGEROUS_AGENT_TYPES = %w[Agents::ShellCommandAgent]
URL_REGEX = /\Ahttps?:\/\//i
attr_accessor :file, :url, :data, :do_import
attr_accessor :file, :url, :data, :do_import, :merges
attr_reader :user
@ -17,13 +20,14 @@ class ScenarioImport
validate :validate_presence_of_file_url_or_data
validates_format_of :url, :with => URL_REGEX, :allow_nil => true, :allow_blank => true, :message => "appears to be invalid"
validate :validate_data
validate :generate_diff
def step_one?
data.blank?
end
def step_two?
valid?
data.present?
end
def set_user(user)
@ -31,7 +35,7 @@ class ScenarioImport
end
def existing_scenario
@existing_scenario ||= user.scenarios.find_by_guid(parsed_data["guid"])
@existing_scenario ||= user.scenarios.find_by(:guid => parsed_data["guid"])
end
def dangerous?
@ -39,18 +43,22 @@ class ScenarioImport
end
def parsed_data
@parsed_data ||= data && JSON.parse(data) rescue {}
@parsed_data ||= (data && JSON.parse(data) rescue {}) || {}
end
def do_import?
def agent_diffs
@agent_diffs || generate_diff
end
def should_import?
do_import == "1"
end
def import!(options = {})
def import(options = {})
success = true
guid = parsed_data['guid']
description = parsed_data['description']
name = parsed_data['name']
agents = parsed_data['agents']
links = parsed_data['links']
source_url = parsed_data['source_url'].presence || nil
@scenario = user.scenarios.where(:guid => guid).first_or_initialize
@ -58,17 +66,20 @@ class ScenarioImport
:source_url => source_url, :public => false)
unless options[:skip_agents]
created_agents = agents.map do |agent_data|
agent = @scenario.agents.find_by(:guid => agent_data['guid']) || Agent.build_for_type(agent_data['type'], user)
agent.guid = agent_data['guid']
agent.attributes = { :name => agent_data['name'],
:schedule => agent_data['schedule'],
:keep_events_for => agent_data['keep_events_for'],
:propagate_immediately => agent_data['propagate_immediately'],
:disabled => agent_data['disabled'],
:options => agent_data['options'],
created_agents = agent_diffs.map do |agent_diff|
agent = agent_diff.agent || Agent.build_for_type("Agents::" + agent_diff.type.incoming, user)
agent.guid = agent_diff.guid.incoming
agent.attributes = { :name => agent_diff.name.updated,
:disabled => agent_diff.disabled.updated, # == "true"
:options => agent_diff.options.updated,
:scenario_ids => [@scenario.id] }
agent.save!
agent.schedule = agent_diff.schedule.updated if agent_diff.schedule.present?
agent.keep_events_for = agent_diff.keep_events_for.updated if agent_diff.keep_events_for.present?
agent.propagate_immediately = agent_diff.propagate_immediately.updated if agent_diff.propagate_immediately.present? # == "true"
unless agent.save
success = false
errors.add(:base, "Errors when saving '#{agent_diff.name.incoming}': #{agent.errors.full_messages.to_sentence}")
end
agent
end
@ -78,6 +89,8 @@ class ScenarioImport
receiver.sources << source unless receiver.sources.include?(source)
end
end
success
end
def scenario
@ -119,4 +132,110 @@ class ScenarioImport
errors.add(:base, "Please provide either a Scenario JSON File or a Public Scenario URL.")
end
end
end
def generate_diff
@agent_diffs = (parsed_data['agents'] || []).map.with_index do |agent_data, index|
# AgentDiff is defined at the end of this file.
agent_diff = AgentDiff.new(agent_data)
if existing_scenario
# If this Agent exists already, update the AgentDiff with the local version's information.
agent_diff.diff_with! existing_scenario.agents.find_by(:guid => agent_data['guid'])
begin
# Update the AgentDiff with any hand-merged changes coming from the UI. This only happens when this
# Agent already exists locally and has conflicting changes.
agent_diff.update_from! merges[index.to_s] if merges
rescue JSON::ParserError
errors.add(:base, "Your updated options for '#{agent_data['name']}' were unparsable.")
end
end
agent_diff
end
end
# AgentDiff is a helper object that encapsulates an incoming Agent. All fields will be returned as an array
# of either one or two values. The first value is the incoming value, the second is the existing value, if
# it differs from the incoming value.
class AgentDiff < OpenStruct
class FieldDiff
attr_accessor :incoming, :current, :updated
def initialize(incoming)
@incoming = incoming
@updated = incoming
end
def set_current(current)
@current = current
@requires_merge = (incoming != current)
end
def requires_merge?
@requires_merge
end
end
def initialize(agent_data)
super()
@requires_merge = false
self.agent = nil
store! agent_data
end
BASE_FIELDS = %w[name schedule keep_events_for propagate_immediately disabled guid]
def agent_exists?
!!agent
end
def requires_merge?
@requires_merge
end
def store!(agent_data)
self.type = FieldDiff.new(agent_data["type"].split("::").pop)
self.options = FieldDiff.new(agent_data['options'] || {})
BASE_FIELDS.each do |option|
self[option] = FieldDiff.new(agent_data[option]) if agent_data.has_key?(option)
end
end
def diff_with!(agent)
return unless agent.present?
self.agent = agent
type.set_current(agent.short_type)
options.set_current(agent.options || {})
@requires_merge ||= type.requires_merge?
@requires_merge ||= options.requires_merge?
BASE_FIELDS.each do |field|
next unless self[field].present?
self[field].set_current(agent.send(field))
@requires_merge ||= self[field].requires_merge?
end
end
def update_from!(merges)
each_field do |field, value, selection_options|
value.updated = merges[field]
end
if options.requires_merge?
options.updated = JSON.parse(merges['options'])
end
end
def each_field
boolean = [["True", "true"], ["False", "false"]]
yield 'name', name if name.requires_merge?
yield 'schedule', schedule, Agent::SCHEDULES.map {|s| [s.humanize.titleize, s] } if self['schedule'].present? && schedule.requires_merge?
yield 'keep_events_for', keep_events_for, Agent::EVENT_RETENTION_SCHEDULES if self['keep_events_for'].present? && keep_events_for.requires_merge?
yield 'propagate_immediately', propagate_immediately, boolean if self['propagate_immediately'].present? && propagate_immediately.requires_merge?
yield 'disabled', disabled, boolean if disabled.requires_merge?
end
end
end

View file

@ -14,7 +14,7 @@
<script>
$(function () {
var payloadJsonEditor = window.setupJsonEditor($(".payload-editor"));
var payloadJsonEditor = window.setupJsonEditor($(".payload-editor"))[0];
$("#create-event-form").submit(function (e) {
e.preventDefault();
var $form = $("#create-event-form");

View file

@ -1,23 +1,32 @@
<div class="page-header">
<h2>
Import a Public Scenario
</h2>
</div>
<blockquote>You can import Scenarios, either from a <code>.json</code> file, or via a public Scenario URL. When you import a Scenario, Huginn will keep track of where it came from and later let you update it.</blockquote>
<div class="col-md-4">
<div class="form-group">
<%= f.label :url, 'Option 1: Provide a Public Scenario URL' %>
<%= f.text_field :url, :class => 'form-control', :placeholder => "Public Scenario URL" %>
</div>
<div class="form-group">
<%= f.label :file, 'Option 2: Upload a Scenario JSON File' %>
<%= f.file_field :file, :class => 'form-control' %>
</div>
<div class='form-actions'>
<%= f.submit "Start Import", :class => "btn btn-primary" %>
<div class="row">
<div class="page-header">
<h2>
Import a Public Scenario
</h2>
</div>
</div>
<div class='row'>
<blockquote>
You can import Scenarios, either from a <code>.json</code> file, or via a public Scenario URL. When you
import a Scenario, Huginn will keep track of where it came from and later let you update it.
</blockquote>
</div>
<div class='row'>
<div class="col-md-4">
<div class="form-group">
<%= f.label :url, 'Option 1: Provide a Public Scenario URL' %>
<%= f.text_field :url, :class => 'form-control', :placeholder => "Public Scenario URL" %>
</div>
<div class="form-group">
<%= f.label :file, 'Option 2: Upload a Scenario JSON File' %>
<%= f.file_field :file, :class => 'form-control' %>
</div>
<div class='form-actions'>
<%= f.submit "Start Import", :class => "btn btn-primary" %>
</div>
</div>
</div>

View file

@ -1,60 +1,152 @@
<div class="col-md-12">
<div class="page-header">
<h2><%= @scenario_import.parsed_data["name"] %> (exported <%= time_ago_in_words Time.parse(@scenario_import.parsed_data["exported_at"]) %> ago)</h2>
</div>
<% if @scenario_import.parsed_data["description"].present? %>
<blockquote><%= @scenario_import.parsed_data["description"] %></blockquote>
<% end %>
<p>
This import contains <%= pluralize @scenario_import.parsed_data["agents"].length, "Agent" %>:
</p>
<ul class='agent-import-list'>
<% @scenario_import.parsed_data["agents"].each do |agent_data| %>
<li>
<%= link_to agent_data['name'], '#', :class => 'options-toggle' %>
<span class='text-muted'>
(<%= agent_data["type"].split("::").pop.titleize %>)
</span>
<pre class='options' style='display: none;'><%= Utils.pretty_jsonify agent_data["options"] || {} %></pre>
</li>
<div class="row">
<div class="col-md-12">
<% if @scenario_import.dangerous? %>
<div class="alert alert-danger">
<span class='glyphicon glyphicon-warning-sign'></span>
This Scenario contains one or more potentially dangerous Agents.
These may be able to run local commands or execute code.
Please be sure that you understand the Agent configurations before importing!
</div>
<% end %>
</ul>
<script>
$(function() {
$('.agent-import-list .options-toggle').on('click', function(e) {
e.preventDefault();
$(this).siblings('.options').fadeToggle();
});
});
</script>
<% if @scenario_import.dangerous? %>
<div class="danger">
This Scenario contains one or more potentially dangerous Agents.
These may be able to run local commands or execute code.
Please be sure that you understand the above Agent configurations before importing!
</div>
<% end %>
<% if @scenario_import.existing_scenario.present? %>
<div class="danger">
This Scenario already exists in your system.
If you continue, the import will overwrite your existing
<span class='label label-info scenario'><%= @scenario_import.existing_scenario.name %></span> Scenario and the Agents in it.
</div>
<% end %>
<div class="checkbox">
<%= f.label :do_import do %>
<%= f.check_box :do_import %> I confirm that I want to import these Agents.
<% if @scenario_import.existing_scenario.present? %>
<div class="alert alert-warning">
<span class='glyphicon glyphicon-warning-sign'></span>
This Scenario already exists in your system. The import will update your existing
<span class='label label-info scenario'><%= @scenario_import.existing_scenario.name %></span> Scenario's title
and
description. Below you can customize how the individual agents get updated.
</div>
<% end %>
<div class="page-header">
<h2>
<%= @scenario_import.parsed_data["name"] %>
<span class='text-muted'>(<%= pluralize @scenario_import.parsed_data["agents"].length, "Agent" %>
; exported <%= time_ago_in_words Time.parse(@scenario_import.parsed_data["exported_at"]) %> ago)</span>
</h2>
</div>
<% if @scenario_import.parsed_data["description"].present? %>
<blockquote><%= @scenario_import.parsed_data["description"] %></blockquote>
<% end %>
</div>
<div class='form-actions'>
<%= f.submit "Finish Import", :class => "btn btn-primary" %>
</div>
</div>
<div class='agent-import-list'>
<% @scenario_import.agent_diffs.each.with_index do |agent_diff, index| %>
<div class='agent-import' data-index='<%= index %>'>
<div class='row'>
<div class='col-md-12'>
<h3>
<a href='#' data-toggle="modal" data-target="#agent_options_<%= index %>"><%= agent_diff.name.incoming %></a>
<span class='text-muted'>
(<%= agent_diff.type.incoming %><% " -- WARNING: this Agent's type has been changed. This import will likely fail!" if agent_diff.type.requires_merge? %>)
</span>
</h3>
<% if agent_diff.agent_exists? %>
<div>
This Agent exists in your Huginn system.
<% if agent_diff.requires_merge? %>
Here are the differences between the incoming version and the one you have now. For each field, please
select which value you'd like to keep.
<% else %>
It's already up-to-date.
<% end %>
</div>
<% end %>
</div>
</div>
<div class="modal fade" id="agent_options_<%= index %>" tabindex="-1" role="dialog" aria-labelledby="modalLabel<%= index %>" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="modalLabel<%= index %>">Options for '<%= agent_diff.name.updated %>'</h4>
</div>
<div class="modal-body">
<pre class='options'><%= Utils.pretty_jsonify agent_diff.options.incoming %></pre>
</div>
</div>
</div>
</div>
<% agent_diff.each_field do |field, value, selection_options| %>
<div class='row'>
<div class='col-md-4'>
<div class="form-group">
<%= label_tag "scenario_import[merges][#{index}][#{field}]", field.titleize %>
<% if selection_options.present? %>
<div>
Your current Agent's value is:
<span class='current'><%= selection_options.find { |s| s.last.to_s == value.current.to_s }.first %></span>
</div>
<%= select_tag "scenario_import[merges][#{index}][#{field}]", options_for_select(selection_options, value.updated), :class => 'form-control' %>
<% else %>
<div>
Your current Agent's value is: <span class='current'><%= value.current.to_s %></span>
</div>
<%= text_field_tag "scenario_import[merges][#{index}][#{field}]", value.updated, :class => 'form-control' %>
<% end %>
</div>
</div>
</div>
<% end %>
<div class='row'>
<% if agent_diff.options.requires_merge? %>
<div class='col-md-12'>
<label>Options</label>
</div>
<div class='col-md-6'>
<textarea name="scenario_import[merges][<%= index %>][options]" rows='10' class="form-control live-json-editor">
<%= Utils.pretty_jsonify(agent_diff.options.updated) %>
</textarea>
</div>
<div class='col-md-6'>
<div>
Your current options:
</div>
<pre class='options'><%= Utils.pretty_jsonify agent_diff.options.current %></pre>
</div>
<% end %>
</div>
</div>
<% end %>
</div>
<div class='row'>
<div class='col-md-12'>
<div class="checkbox">
<%= f.label :do_import do %>
<%= f.check_box :do_import %> I confirm that I want to import these Agents.
<% end %>
</div>
<div class='form-actions'>
<%= f.submit "Finish Import", :class => "btn btn-primary" %>
</div>
</div>
</div>
<script>
// $(function() {
// $('.agent-import-list .options-toggle').on('click', function (e) {
// e.preventDefault();
// $(this).siblings('.options').slideToggle()
// if ($(this).text() == "Show Options") {
// $(this).text("Hide Options");
// } else {
// $(this).text("Show Options");
// }
// });
// });
</script>

View file

@ -1,6 +1,6 @@
<div class='container scenario-import'>
<div class='row'>
<div class='col-md-12'>
<div class="row">
<div class="col-md-12">
<% if @scenario_import.errors.any? %>
<div class="row well">
<h2><%= pluralize(@scenario_import.errors.count, "error") %> prohibited this Scenario from being imported:</h2>
@ -9,26 +9,24 @@
<% end %>
</div>
<% end %>
</div>
</div>
<%= form_for @scenario_import, :multipart => true do |f| %>
<%= f.hidden_field :data %>
<%= form_for @scenario_import, :multipart => true do |f| %>
<%= f.hidden_field :data %>
<div class="row">
<% if @scenario_import.step_one? %>
<%= render 'step_one', :f => f %>
<% elsif @scenario_import.step_two? %>
<%= render 'step_two', :f => f %>
<% end %>
</div>
<% end %>
<% if @scenario_import.step_one? %>
<%= render 'step_one', :f => f %>
<% elsif @scenario_import.step_two? %>
<%= render 'step_two', :f => f %>
<% end %>
<% end %>
<hr />
<hr />
<div class="row">
<div class="col-md-12">
<%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %>
</div>
</div>
<div class="row">
<div class="col-md-12">
<%= link_to '<span class="glyphicon glyphicon-chevron-left"></span> Back'.html_safe, scenarios_path, class: "btn btn-default" %>
</div>
</div>
</div>

View file

@ -42,12 +42,13 @@ class AgentsExporter
{
:type => agent.type,
:name => agent.name,
:schedule => agent.schedule,
:keep_events_for => agent.keep_events_for,
:propagate_immediately => agent.propagate_immediately,
:disabled => agent.disabled,
:guid => agent.guid,
:options => agent.options
}
}.tap do |options|
options[:schedule] = agent.schedule if agent.can_be_scheduled?
options[:keep_events_for] = agent.keep_events_for if agent.can_create_events?
options[:propagate_immediately] = agent.propagate_immediately if agent.can_receive_events?
end
end
end

View file

@ -1,10 +1,6 @@
require 'spec_helper'
describe ScenarioImportsController do
def valid_attributes(options = {})
{ :name => "some_name" }.merge(options)
end
before do
sign_in users(:bob)
end
@ -22,6 +18,7 @@ describe ScenarioImportsController do
post :create, :scenario_import => { :url => "bad url" }
assigns(:scenario_import).user.should == users(:bob)
assigns(:scenario_import).url.should == "bad url"
assigns(:scenario_import).should_not be_valid
response.should render_template(:new)
end
end

View file

@ -21,9 +21,12 @@ describe AgentsExporter do
data[:links].should == [{ :source => 0, :receiver => 1 }]
data[:agents].should == agent_list.map { |agent| exporter.agent_as_json(agent) }
data[:agents].all? { |agent_json| agent_json[:guid].present? && agent_json[:type].present? && agent_json[:name].present? }.should be_true
data[:agents][0].should_not have_key(:propagate_immediately) # can't receive events
data[:agents][1].should_not have_key(:schedule) # can't be scheduled
end
it "does not output links to other agents" do
it "does not output links to other agents outside of the incoming set" do
Link.create!(:source_id => agents(:jane_weather_agent).id, :receiver_id => agents(:jane_website_agent).id)
Link.create!(:source_id => agents(:jane_website_agent).id, :receiver_id => agents(:jane_rain_notifier_agent).id)

View file

@ -1,6 +1,7 @@
require 'spec_helper'
describe ScenarioImport do
let(:user) { users(:bob) }
let(:guid) { "somescenarioguid" }
let(:description) { "This is a cool Huginn Scenario that does something useful!" }
let(:name) { "A useful Scenario" }
@ -22,6 +23,28 @@ describe ScenarioImport do
'message' => "Looks like rain!"
}
}
let(:valid_parsed_weather_agent_data) do
{
:type => "Agents::WeatherAgent",
:name => "a weather agent",
:schedule => "5pm",
:keep_events_for => 14,
:disabled => true,
:guid => "a-weather-agent",
:options => weather_agent_options
}
end
let(:valid_parsed_trigger_agent_data) do
{
:type => "Agents::TriggerAgent",
:name => "listen for weather",
:keep_events_for => 0,
:propagate_immediately => true,
:disabled => false,
:guid => "a-trigger-agent",
:options => trigger_agent_options
}
end
let(:valid_parsed_data) do
{
:name => name,
@ -30,26 +53,8 @@ describe ScenarioImport do
:source_url => source_url,
:exported_at => 2.days.ago.utc.iso8601,
:agents => [
{
:type => "Agents::WeatherAgent",
:name => "a weather agent",
:schedule => "5pm",
:keep_events_for => 14,
:propagate_immediately => false,
:disabled => false,
:guid => "a-weather-agent",
:options => weather_agent_options
},
{
:type => "Agents::TriggerAgent",
:name => "listen for weather",
:schedule => nil,
:keep_events_for => 0,
:propagate_immediately => true,
:disabled => true,
:guid => "a-trigger-agent",
:options => trigger_agent_options
}
valid_parsed_weather_agent_data,
valid_parsed_trigger_agent_data
],
:links => [
{ :source => 0, :receiver => 1 }
@ -66,7 +71,11 @@ describe ScenarioImport do
end
describe "validations" do
subject { ScenarioImport.new }
subject do
_import = ScenarioImport.new
_import.set_user(user)
_import
end
it "is not valid when none of file, url, or data are present" do
subject.should_not be_valid
@ -145,7 +154,7 @@ describe ScenarioImport do
end
end
describe "#import!" do
describe "#import and #generate_diff" do
let(:scenario_import) do
_import = ScenarioImport.new(:data => valid_data)
_import.set_user users(:bob)
@ -153,107 +162,249 @@ describe ScenarioImport do
end
context "when this scenario has never been seen before" do
it "makes a new scenario" do
lambda {
scenario_import.import!(:skip_agents => true)
}.should change { users(:bob).scenarios.count }.by(1)
describe "#import" do
it "makes a new scenario" do
lambda {
scenario_import.import(:skip_agents => true)
}.should change { users(:bob).scenarios.count }.by(1)
scenario_import.scenario.name.should == name
scenario_import.scenario.description.should == description
scenario_import.scenario.guid.should == guid
scenario_import.scenario.source_url.should == source_url
scenario_import.scenario.public.should be_false
scenario_import.scenario.name.should == name
scenario_import.scenario.description.should == description
scenario_import.scenario.guid.should == guid
scenario_import.scenario.source_url.should == source_url
scenario_import.scenario.public.should be_false
end
it "creates the Agents" do
lambda {
scenario_import.import
}.should change { users(:bob).agents.count }.by(2)
weather_agent = scenario_import.scenario.agents.find_by(:guid => "a-weather-agent")
trigger_agent = scenario_import.scenario.agents.find_by(:guid => "a-trigger-agent")
weather_agent.name.should == "a weather agent"
weather_agent.schedule.should == "5pm"
weather_agent.keep_events_for.should == 14
weather_agent.propagate_immediately.should be_false
weather_agent.should be_disabled
weather_agent.memory.should be_empty
weather_agent.options.should == weather_agent_options
trigger_agent.name.should == "listen for weather"
trigger_agent.sources.should == [weather_agent]
trigger_agent.schedule.should be_nil
trigger_agent.keep_events_for.should == 0
trigger_agent.propagate_immediately.should be_true
trigger_agent.should_not be_disabled
trigger_agent.memory.should be_empty
trigger_agent.options.should == trigger_agent_options
end
it "creates new Agents, even if one already exists with the given guid (so that we don't overwrite a user's work outside of the scenario)" do
agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent"
lambda {
scenario_import.import
}.should change { users(:bob).agents.count }.by(2)
end
end
it "creates the Agents" do
lambda {
scenario_import.import!
}.should change { users(:bob).agents.count }.by(2)
describe "#generate_diff" do
it "returns AgentDiff objects for the incoming Agents" do
scenario_import.should be_valid
weather_agent = scenario_import.scenario.agents.find_by(:guid => "a-weather-agent")
trigger_agent = scenario_import.scenario.agents.find_by(:guid => "a-trigger-agent")
agent_diffs = scenario_import.agent_diffs
weather_agent.name.should == "a weather agent"
weather_agent.schedule.should == "5pm"
weather_agent.keep_events_for.should == 14
weather_agent.propagate_immediately.should be_false
weather_agent.should_not be_disabled
weather_agent.memory.should be_empty
weather_agent.options.should == weather_agent_options
weather_agent_diff = agent_diffs[0]
trigger_agent_diff = agent_diffs[1]
trigger_agent.name.should == "listen for weather"
trigger_agent.sources.should == [weather_agent]
trigger_agent.schedule.should be_nil
trigger_agent.keep_events_for.should == 0
trigger_agent.propagate_immediately.should be_true
trigger_agent.should be_disabled
trigger_agent.memory.should be_empty
trigger_agent.options.should == trigger_agent_options
end
valid_parsed_weather_agent_data.each do |key, value|
if key == :type
value = value.split("::").last
end
weather_agent_diff.should respond_to(key)
field = weather_agent_diff.send(key)
field.should be_a(ScenarioImport::AgentDiff::FieldDiff)
field.incoming.should == value
field.updated.should == value
field.current.should be_nil
end
weather_agent_diff.should_not respond_to(:propagate_immediately)
it "creates new Agents, even if one already exists with the given guid (so that we don't overwrite a user's work outside of the scenario)" do
agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent"
lambda {
scenario_import.import!
}.should change { users(:bob).agents.count }.by(2)
valid_parsed_trigger_agent_data.each do |key, value|
if key == :type
value = value.split("::").last
end
trigger_agent_diff.should respond_to(key)
field = trigger_agent_diff.send(key)
field.should be_a(ScenarioImport::AgentDiff::FieldDiff)
field.incoming.should == value
field.updated.should == value
field.current.should be_nil
end
trigger_agent_diff.should_not respond_to(:schedule)
end
end
end
context "when an a scenario already exists with the given guid" do
let!(:existing_scenario) {
_existing_scenerio = users(:bob).scenarios.build(:name => "an existing scenario")
let!(:existing_scenario) do
_existing_scenerio = users(:bob).scenarios.build(:name => "an existing scenario", :description => "something")
_existing_scenerio.guid = guid
_existing_scenerio.save!
agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent"
agents(:bob_weather_agent).scenarios << _existing_scenerio
_existing_scenerio
}
it "uses the existing scenario, updating it's data" do
lambda {
scenario_import.import!(:skip_agents => true)
scenario_import.scenario.should == existing_scenario
}.should_not change { users(:bob).scenarios.count }
existing_scenario.reload
existing_scenario.guid.should == guid
existing_scenario.description.should == description
existing_scenario.name.should == name
existing_scenario.source_url.should == source_url
existing_scenario.public.should be_false
end
it "updates any existing agents in the scenario, and makes new ones as needed" do
agents(:bob_weather_agent).update_attribute :guid, "a-weather-agent"
agents(:bob_weather_agent).scenarios << existing_scenario
describe "#import" do
it "uses the existing scenario, updating its data" do
lambda {
scenario_import.import(:skip_agents => true)
scenario_import.scenario.should == existing_scenario
}.should_not change { users(:bob).scenarios.count }
lambda {
# Shouldn't matter how many times we do it!
scenario_import.import!
scenario_import.import!
scenario_import.import!
}.should change { users(:bob).agents.count }.by(1)
existing_scenario.reload
existing_scenario.guid.should == guid
existing_scenario.description.should == description
existing_scenario.name.should == name
existing_scenario.source_url.should == source_url
existing_scenario.public.should be_false
end
weather_agent = existing_scenario.agents.find_by(:guid => "a-weather-agent")
trigger_agent = existing_scenario.agents.find_by(:guid => "a-trigger-agent")
it "updates any existing agents in the scenario, and makes new ones as needed" do
scenario_import.should be_valid
weather_agent.should == agents(:bob_weather_agent)
lambda {
scenario_import.import
}.should change { users(:bob).agents.count }.by(1) # One, because the weather agent already existed.
weather_agent.name.should == "a weather agent"
weather_agent.schedule.should == "5pm"
weather_agent.keep_events_for.should == 14
weather_agent.propagate_immediately.should be_false
weather_agent.should_not be_disabled
weather_agent.memory.should be_empty
weather_agent.options.should == weather_agent_options
weather_agent = existing_scenario.agents.find_by(:guid => "a-weather-agent")
trigger_agent = existing_scenario.agents.find_by(:guid => "a-trigger-agent")
trigger_agent.name.should == "listen for weather"
trigger_agent.sources.should == [weather_agent]
trigger_agent.schedule.should be_nil
trigger_agent.keep_events_for.should == 0
trigger_agent.propagate_immediately.should be_true
trigger_agent.should be_disabled
trigger_agent.memory.should be_empty
trigger_agent.options.should == trigger_agent_options
weather_agent.should == agents(:bob_weather_agent)
weather_agent.name.should == "a weather agent"
weather_agent.schedule.should == "5pm"
weather_agent.keep_events_for.should == 14
weather_agent.propagate_immediately.should be_false
weather_agent.should be_disabled
weather_agent.memory.should be_empty
weather_agent.options.should == weather_agent_options
trigger_agent.name.should == "listen for weather"
trigger_agent.sources.should == [weather_agent]
trigger_agent.schedule.should be_nil
trigger_agent.keep_events_for.should == 0
trigger_agent.propagate_immediately.should be_true
trigger_agent.should_not be_disabled
trigger_agent.memory.should be_empty
trigger_agent.options.should == trigger_agent_options
end
it "honors updates coming from the UI" do
scenario_import.merges = {
"0" => {
"name" => "updated name",
"schedule" => "6pm",
"keep_events_for" => "2",
"disabled" => "false",
"options" => weather_agent_options.merge("api_key" => "foo").to_json
}
}
scenario_import.should be_valid
scenario_import.import.should be_true
weather_agent = existing_scenario.agents.find_by(:guid => "a-weather-agent")
weather_agent.name.should == "updated name"
weather_agent.schedule.should == "6pm"
weather_agent.keep_events_for.should == 2
weather_agent.should_not be_disabled
weather_agent.options.should == weather_agent_options.merge("api_key" => "foo")
end
it "adds errors when updated agents are invalid" do
scenario_import.merges = {
"0" => {
"name" => "",
"schedule" => "foo",
"keep_events_for" => "2",
"options" => weather_agent_options.merge("api_key" => "").to_json
}
}
scenario_import.import.should be_false
errors = scenario_import.errors.full_messages.to_sentence
errors.should =~ /Name can't be blank/
errors.should =~ /api_key is required/
errors.should =~ /Schedule is not a valid schedule/
end
end
describe "#generate_diff" do
it "returns AgentDiff objects that include 'current' values from any agents that already exist" do
agent_diffs = scenario_import.agent_diffs
weather_agent_diff = agent_diffs[0]
trigger_agent_diff = agent_diffs[1]
# Already exists
weather_agent_diff.agent.should == agents(:bob_weather_agent)
valid_parsed_weather_agent_data.each do |key, value|
next if key == :type
weather_agent_diff.send(key).current.should == agents(:bob_weather_agent).send(key)
end
# Doesn't exist yet
valid_parsed_trigger_agent_data.each do |key, value|
trigger_agent_diff.send(key).current.should be_nil
end
end
it "sets the 'updated' FieldDiff values based on any feedback from the user" do
scenario_import.merges = {
"0" => {
"name" => "a new name",
"schedule" => "6pm",
"keep_events_for" => "2",
"disabled" => "true",
"options" => weather_agent_options.merge("api_key" => "foo").to_json
},
"1" => {
"name" => "another new name"
}
}
scenario_import.should be_valid
agent_diffs = scenario_import.agent_diffs
weather_agent_diff = agent_diffs[0]
trigger_agent_diff = agent_diffs[1]
weather_agent_diff.name.current.should == agents(:bob_weather_agent).name
weather_agent_diff.name.incoming.should == valid_parsed_weather_agent_data[:name]
weather_agent_diff.name.updated.should == "a new name"
weather_agent_diff.schedule.updated.should == "6pm"
weather_agent_diff.keep_events_for.updated.should == "2"
weather_agent_diff.disabled.updated.should == "true"
weather_agent_diff.options.updated.should == weather_agent_options.merge("api_key" => "foo")
end
it "adds errors on validation when updated options are unparsable" do
scenario_import.merges = {
"0" => {
"options" => '{'
}
}
scenario_import.should_not be_valid
scenario_import.should have(1).error_on(:base)
end
end
end
end