diff --git a/Gemfile b/Gemfile index f556084a..bece4ea5 100644 --- a/Gemfile +++ b/Gemfile @@ -59,6 +59,7 @@ gem 'faraday', '~> 0.9.0' gem 'faraday_middleware' gem 'typhoeus', '~> 0.6.3' gem 'nokogiri', '~> 1.6.1' +gem 'net-ftp-list', '~> 3.2.8' gem 'wunderground', '~> 1.2.0' gem 'forecast_io', '~> 2.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index 5795b313..569a6ec7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -188,6 +188,7 @@ GEM multipart-post (2.0.0) mysql2 (0.3.16) naught (1.0.0) + net-ftp-list (3.2.8) nokogiri (1.6.3.1) mini_portile (= 0.6.0) oauth (0.4.7) @@ -418,6 +419,7 @@ DEPENDENCIES liquid (~> 2.6.1) mqtt mysql2 (~> 0.3.16) + net-ftp-list (~> 3.2.8) nokogiri (~> 1.6.1) omniauth omniauth-37signals diff --git a/README.md b/README.md index cdeb7b66..15491104 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ Follow [@tectonic](https://twitter.com/tectonic) for updates as Huginn evolves, Want to help with Huginn? All contributions are encouraged! You could make UI improvements, add new Agents, write documentation and tutorials, or try tackling [issues tagged with #help-wanted](https://github.com/cantino/huginn/issues?direction=desc&labels=help-wanted&page=1&sort=created&state=open). +Really want an issue fixed/feature implemented? Or maybe you just want to solve some community issues and earn some extra coffee money? Then you should take a look at the [current bounties on Bountysource](https://www.bountysource.com/trackers/282580-huginn). + Have an awesome an idea but not feeling quite up to contributing yet? Head over to our [Official 'suggest an agent' thread ](https://github.com/cantino/huginn/issues/353) and tell us about your cool idea! ## Examples @@ -105,5 +107,5 @@ Huginn is a work in progress and is just getting started. Please get involved! Please fork, add specs, and send pull requests! -[![Build Status](https://travis-ci.org/cantino/huginn.png)](https://travis-ci.org/cantino/huginn) [![Coverage Status](https://coveralls.io/repos/cantino/huginn/badge.png)](https://coveralls.io/r/cantino/huginn) [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/cantino/huginn/trend.png)](https://bitdeli.com/free "Bitdeli Badge") [![Dependency Status](https://gemnasium.com/cantino/huginn.svg)](https://gemnasium.com/cantino/huginn) +[![Build Status](https://travis-ci.org/cantino/huginn.png)](https://travis-ci.org/cantino/huginn) [![Coverage Status](https://coveralls.io/repos/cantino/huginn/badge.png)](https://coveralls.io/r/cantino/huginn) [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/cantino/huginn/trend.png)](https://bitdeli.com/free "Bitdeli Badge") [![Dependency Status](https://gemnasium.com/cantino/huginn.svg)](https://gemnasium.com/cantino/huginn) [![Bountysource](https://www.bountysource.com/badge/tracker?tracker_id=282580)](https://www.bountysource.com/trackers/282580-huginn?utm_source=282580&utm_medium=shield&utm_campaign=TRACKER_BADGE) diff --git a/app/assets/javascripts/diagram.js.coffee b/app/assets/javascripts/diagram.js.coffee new file mode 100644 index 00000000..1782ff72 --- /dev/null +++ b/app/assets/javascripts/diagram.js.coffee @@ -0,0 +1,20 @@ +$ -> + svg = document.querySelector('.agent-diagram svg.diagram') + overlay = document.querySelector('.agent-diagram .overlay') + getTopLeft = (node) -> + bbox = node.getBBox() + point = svg.createSVGPoint() + point.x = bbox.x + bbox.width + point.y = bbox.y + point.matrixTransform(node.getCTM()) + $(svg).find('g.node[data-badge-id]').each -> + tl = getTopLeft(this) + $('#' + this.getAttribute('data-badge-id'), overlay).each -> + badge = $(this) + badge.css + left: tl.x - badge.outerWidth() * (2/3) + top: tl.y - badge.outerHeight() * (1/3) + 'background-color': badge.find('.label').css('background-color') + .show() + return + return diff --git a/app/assets/stylesheets/diagram.css.scss b/app/assets/stylesheets/diagram.css.scss new file mode 100644 index 00000000..c068ca62 --- /dev/null +++ b/app/assets/stylesheets/diagram.css.scss @@ -0,0 +1,30 @@ +.agent-diagram { + position: relative; + z-index: auto; + + svg.diagram { + position: absolute; + z-index: 1; + } + + .overlay-container { + position: absolute; + top: 0; + left: 0; + z-index: auto; + + .overlay { + position: relative; + z-index: auto; + width: 100%; + height: 100%; + + .badge { + position: absolute; + display: none; + color: white !important; + z-index: 2; + } + } + } +} diff --git a/app/controllers/agents_controller.rb b/app/controllers/agents_controller.rb index b72686ab..e276cb1a 100644 --- a/app/controllers/agents_controller.rb +++ b/app/controllers/agents_controller.rb @@ -98,14 +98,6 @@ class AgentsController < ApplicationController @agent = current_user.agents.find(params[:id]) end - def diagram - @agents = if params[:scenario_id].present? - current_user.scenarios.find(params[:scenario_id]).agents.includes(:receivers) - else - current_user.agents.includes(:receivers) - end - end - def create @agent = Agent.build_for_type(params[:agent].delete(:type), current_user, diff --git a/app/controllers/diagrams_controller.rb b/app/controllers/diagrams_controller.rb new file mode 100644 index 00000000..6772ea41 --- /dev/null +++ b/app/controllers/diagrams_controller.rb @@ -0,0 +1,9 @@ +class DiagramsController < ApplicationController + def show + @agents = if params[:scenario_id].present? + current_user.scenarios.find(params[:scenario_id]).agents.includes(:receivers) + else + current_user.agents.includes(:receivers) + end + end +end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 20635378..a2fdcd79 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -2,8 +2,8 @@ class EventsController < ApplicationController before_filter :load_event, :except => :index def index - if params[:agent] - @agent = current_user.agents.find(params[:agent]) + if params[:agent_id] + @agent = current_user.agents.find(params[:agent_id]) @events = @agent.events.page(params[:page]) else @events = current_user.events.preload(:agent).page(params[:page]) diff --git a/app/helpers/dot_helper.rb b/app/helpers/dot_helper.rb index 5a5361d7..0b9ae942 100644 --- a/app/helpers/dot_helper.rb +++ b/app/helpers/dot_helper.rb @@ -6,7 +6,7 @@ module DotHelper dot.close_write dot.read } rescue false) - svg.html_safe + decorate_svg(svg, agents).html_safe else tag('img', src: URI('https://chart.googleapis.com/chart').tap { |uri| uri.query = URI.encode_www_form(cht: 'gv', chl: agents_dot(agents)) @@ -57,6 +57,13 @@ module DotHelper end end + def ids(values) + values.each_with_index { |id, i| + raw ' ' if i > 0 + id id + } + end + def attr_list(attrs = nil) return if attrs.nil? attrs = attrs.select { |key, value| value.present? } @@ -86,16 +93,13 @@ module DotHelper end def statement(ids, attrs = nil) - Array(ids).each_with_index { |id, i| - raw ' ' if i > 0 - id id - } + ids Array(ids) attr_list attrs raw ';' end - def block(title, &block) - raw title + def block(*ids, &block) + ids ids raw '{' block.call raw '}' @@ -112,11 +116,7 @@ module DotHelper draw(agents: agents, agent_id: ->agent { 'a%d' % agent.id }, agent_label: ->agent { - if agent.disabled? - '%s (Disabled)' % agent.name - else - agent.name - end.gsub(/(.{20}\S*)\s+/) { + agent.name.gsub(/(.{20}\S*)\s+/) { # Fold after every 20+ characters $1 + "\n" } @@ -128,6 +128,7 @@ module DotHelper def agent_node(agent) node(agent_id[agent], label: agent_label[agent], + tooltip: (agent.short_type.titleize if rich), URL: (agent_url[agent] if rich), style: ('rounded,dashed' if agent.disabled?), color: (@disabled if agent.disabled?), @@ -141,7 +142,7 @@ module DotHelper color: (@disabled if agent.disabled? || receiver.disabled?)) end - block('digraph foo') { + block('digraph', 'Agent Event Flow') { # statement 'graph', rankdir: 'LR' statement 'node', shape: 'box', @@ -160,4 +161,60 @@ module DotHelper } } end + + def decorate_svg(xml, agents) + svg = Nokogiri::XML(xml).at('svg') + + Nokogiri::HTML::Document.new.tap { |doc| + doc << root = Nokogiri::XML::Node.new('div', doc) { |div| + div['class'] = 'agent-diagram' + } + + svg['class'] = 'diagram' + + root << svg + root << overlay_container = Nokogiri::XML::Node.new('div', doc) { |div| + div['class'] = 'overlay-container' + div['style'] = "width: #{svg['width']}; height: #{svg['height']}" + } + overlay_container << overlay = Nokogiri::XML::Node.new('div', doc) { |div| + div['class'] = 'overlay' + } + + svg.xpath('//xmlns:g[@class="node"]', svg.namespaces).each { |node| + agent_id = (node.xpath('./xmlns:title/text()', svg.namespaces).to_s[/\d+/] or next).to_i + agent = agents.find { |a| a.id == agent_id } + + count = agent.events_count + next unless count && count > 0 + + overlay << Nokogiri::XML::Node.new('a', doc) { |badge| + badge['id'] = id = 'b%d' % agent_id + badge['class'] = 'badge' + badge['href'] = events_path(agent: agent) + badge['target'] = '_blank' + badge['title'] = "#{count} events created" + badge.content = count.to_s + + node['data-badge-id'] = id + + badge << Nokogiri::XML::Node.new('span', doc) { |label| + # a dummy label only to obtain the background color + label['class'] = [ + 'label', + if agent.disabled? + 'label-warning' + elsif agent.working? + 'label-success' + else + 'label-danger' + end + ].join(' ') + label['style'] = 'display: none'; + } + } + } + # See also: app/assets/diagram.js.coffee + }.at('div.agent-diagram').to_s + end end diff --git a/app/models/agents/event_formatting_agent.rb b/app/models/agents/event_formatting_agent.rb index 3d04ab80..dbf21f15 100644 --- a/app/models/agents/event_formatting_agent.rb +++ b/app/models/agents/event_formatting_agent.rb @@ -25,9 +25,14 @@ module Agents "instructions": { "message": "Today's conditions look like {{conditions}} with a high temperature of {{high.celsius}} degrees Celsius.", - "subject": "{{data}}" + "subject": "{{data}}", + "created_at": "{{created_at}}" } + Names here like `conditions`, `high` and `data` refer to the corresponding values in the Event hash. + + The special key `created_at` refers to the timestamp of the Event, which can be reformatted by the `date` filter, like `{{created_at | date:"at %I:%M %p" }}`. + The upstream agent of each received event is accessible via the key `agent`, which has the following attributes: #{''.tap { |s| s << AgentDrop.instance_methods(false).map { |m| "`#{m}`" }.join(', ') }}. Have a look at the [Wiki](https://github.com/cantino/huginn/wiki/Formatting-Events-using-Liquid) to learn more about liquid templating. @@ -68,8 +73,6 @@ module Agents If you want to retain original contents of events and only add new keys, then set `mode` to `merge`, otherwise set it to `clean`. - By default, the output event will have a `created_at` field added as well, reflecting the original Event creation time. You can skip this output by setting `skip_created_at` to `true`. - To CGI escape output (for example when creating a link), use the Liquid `uri_escape` filter, like so: { @@ -82,7 +85,7 @@ module Agents after_save :clear_matchers def validate_options - errors.add(:base, "instructions, mode, and skip_created_at all need to be present.") unless options['instructions'].present? && options['mode'].present? && options['skip_created_at'].present? + errors.add(:base, "instructions and mode need to be present.") unless options['instructions'].present? && options['mode'].present? validate_matchers end @@ -96,7 +99,6 @@ module Agents }, 'matchers' => [], 'mode' => "clean", - 'skip_created_at' => "false" } end @@ -110,7 +112,6 @@ module Agents opts = interpolated(event.to_liquid(payload)) formatted_event = opts['mode'].to_s == "merge" ? event.payload.dup : {} formatted_event.merge! opts['instructions'] - formatted_event['created_at'] = event.created_at unless opts['skip_created_at'].to_s == "true" create_event :payload => formatted_event end end diff --git a/app/models/agents/ftpsite_agent.rb b/app/models/agents/ftpsite_agent.rb index a3ae2755..7cb27e81 100644 --- a/app/models/agents/ftpsite_agent.rb +++ b/app/models/agents/ftpsite_agent.rb @@ -1,4 +1,5 @@ require 'net/ftp' +require 'net/ftp/list' require 'uri' require 'time' @@ -105,34 +106,15 @@ module Agents # commands during iteration. list = ftp.list('-a') - month2year = {} - list.each do |line| - mon, day, smtn, rest = line.split(' ', 9)[5..-1] - - # Remove symlink target part if any - filename = rest[/\A(.+?)(?:\s+->\s|\z)/, 1] - + entry = Net::FTP::List.parse line + filename = entry.basename + mtime = Time.parse(entry.mtime.to_s).utc + patterns.any? { |pattern| File.fnmatch?(pattern, filename) } or next - case smtn - when /:/ - if year = month2year[mon] - mtime = Time.parse("#{mon} #{day} #{year} #{smtn} GMT") - else - log "Getting mtime of #{filename}" - mtime = ftp.mtime(filename) - month2year[mon] = mtime.year - end - else - # Do not bother calling MDTM for old files. Losing the - # time part only makes a timestamp go backwards, meaning - # that it will trigger no new event. - mtime = Time.parse("#{mon} #{day} #{smtn} GMT") - end - after < mtime or next yield filename, mtime @@ -193,7 +175,7 @@ module Agents found_entries[filename] }.each { |filename| create_event :payload => { - 'url' => (base_uri + filename).to_s, + 'url' => "#{base_uri}#{filename}", 'filename' => filename, 'timestamp' => found_entries[filename], } diff --git a/app/models/agents/hipchat_agent.rb b/app/models/agents/hipchat_agent.rb index fcd16c64..475ab275 100644 --- a/app/models/agents/hipchat_agent.rb +++ b/app/models/agents/hipchat_agent.rb @@ -31,7 +31,7 @@ module Agents end def validate_options - errors.add(:base, "you need to specify a hipchat auth_token") unless options['auth_token'].present? + errors.add(:base, "you need to specify a hipchat auth_token or provide a credential named hipchat_auth_token") unless options['auth_token'].present? || credential('hipchat_auth_token').present? errors.add(:base, "you need to specify a room_name or a room_name_path") if options['room_name'].blank? && options['room_name_path'].blank? end @@ -40,10 +40,10 @@ module Agents end def receive(incoming_events) - client = HipChat::Client.new(interpolated[:auth_token]) + client = HipChat::Client.new(interpolated[:auth_token] || credential('hipchat_auth_token')) incoming_events.each do |event| mo = interpolated(event) - client[mo[:room_name]].send(mo[:username], mo[:message], :notify => mo[:notify].to_s == 'true' ? 1 : 0, :color => mo[:color]) + client[mo[:room_name]].send(mo[:username], mo[:message], :notify => boolify(mo[:notify]) ? 1 : 0, :color => mo[:color]) end end end diff --git a/app/models/agents/mqtt_agent.rb b/app/models/agents/mqtt_agent.rb index 85c8f811..35c4005e 100644 --- a/app/models/agents/mqtt_agent.rb +++ b/app/models/agents/mqtt_agent.rb @@ -17,7 +17,7 @@ module Agents Simply choose a topic (think email subject line) to publish/listen to, and configure your service. - It's easy to setup your own [broker](http://jpmens.net/2013/09/01/installing-mosquitto-on-a-raspberry-pi/) or connect to a [cloud service](www.cloudmqtt.com) + It's easy to setup your own [broker](http://jpmens.net/2013/09/01/installing-mosquitto-on-a-raspberry-pi/) or connect to a [cloud service](http://www.cloudmqtt.com) Hints: Many services run mqtts (mqtt over SSL) often with a custom certificate. diff --git a/app/models/agents/post_agent.rb b/app/models/agents/post_agent.rb index d239ec2b..f30c7c76 100644 --- a/app/models/agents/post_agent.rb +++ b/app/models/agents/post_agent.rb @@ -69,7 +69,7 @@ module Agents def receive(incoming_events) incoming_events.each do |event| outgoing = interpolated(event)['payload'].presence || {} - if interpolated['no_merge'].to_s == 'true' + if boolify(interpolated['no_merge']) handle outgoing, event.payload else handle outgoing.merge(event.payload), event.payload diff --git a/app/models/agents/trigger_agent.rb b/app/models/agents/trigger_agent.rb index 4c294513..a0a06090 100644 --- a/app/models/agents/trigger_agent.rb +++ b/app/models/agents/trigger_agent.rb @@ -102,7 +102,7 @@ module Agents end def keep_event? - interpolated['keep_event'] == 'true' + boolify(interpolated['keep_event']) end end end diff --git a/app/models/agents/twilio_agent.rb b/app/models/agents/twilio_agent.rb index 81380fb5..1641097a 100644 --- a/app/models/agents/twilio_agent.rb +++ b/app/models/agents/twilio_agent.rb @@ -44,13 +44,13 @@ module Agents incoming_events.each do |event| message = (event.payload['message'].presence || event.payload['text'].presence || event.payload['sms'].presence).to_s if message.present? - if interpolated(event)['receive_call'].to_s == 'true' + if boolify(interpolated(event)['receive_call']) secret = SecureRandom.hex 3 memory['pending_calls'][secret] = message make_call secret end - if interpolated(event)['receive_text'].to_s == 'true' + if boolify(interpolated(event)['receive_text']) message = message.slice 0..160 send_message message end diff --git a/app/models/event.rb b/app/models/event.rb index c877e70b..97bd0ef9 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -56,6 +56,8 @@ class EventDrop case key when 'agent' @object.agent + when 'created_at' + @object.created_at end end end diff --git a/app/views/agents/_table.html.erb b/app/views/agents/_table.html.erb index ddb2171b..1f6c72f4 100644 --- a/app/views/agents/_table.html.erb +++ b/app/views/agents/_table.html.erb @@ -53,7 +53,7 @@ <% if agent.can_create_events? %> - <%= link_to(agent.events_count || 0, events_path(:agent => agent.to_param)) %> + <%= link_to(agent.events_count || 0, agent_events_path(agent)) %> <% else %> <% end %> diff --git a/app/views/agents/index.html.erb b/app/views/agents/index.html.erb index b5c48321..96cfc010 100644 --- a/app/views/agents/index.html.erb +++ b/app/views/agents/index.html.erb @@ -12,9 +12,8 @@
<%= link_to ' New Agent'.html_safe, new_agent_path, class: "btn btn-default" %> <%= link_to ' Run event propagation'.html_safe, propagate_agents_path, method: 'post', class: "btn btn-default" %> - <%= link_to ' View diagram'.html_safe, diagram_agents_path, class: "btn btn-default" %> + <%= link_to ' View diagram'.html_safe, diagram_path, class: "btn btn-default" %>
- diff --git a/app/views/agents/show.html.erb b/app/views/agents/show.html.erb index 231f517a..af220661 100644 --- a/app/views/agents/show.html.erb +++ b/app/views/agents/show.html.erb @@ -15,7 +15,7 @@
  • '> Logs
  • <% if @agent.can_create_events? && @agent.events.count > 0 %> -
  • <%= link_to ' Events'.html_safe, events_path(:agent => @agent.to_param) %>
  • +
  • <%= link_to ' Events'.html_safe, agent_events_path(@agent) %>
  • <% else %>
  • Events
  • <% end %> @@ -103,7 +103,7 @@ <% if @agent.can_create_events? %>

    Events created: - <%= link_to @agent.events.count, events_path(:agent => @agent.to_param) %> + <%= link_to @agent.events.count, agent_events_path(@agent) %>

    <% end %> diff --git a/app/views/agents/diagram.html.erb b/app/views/diagrams/show.html.erb similarity index 86% rename from app/views/agents/diagram.html.erb rename to app/views/diagrams/show.html.erb index be2bb3d6..5a1de393 100644 --- a/app/views/agents/diagram.html.erb +++ b/app/views/diagrams/show.html.erb @@ -1,3 +1,7 @@ +<% content_for :head do %> + <%= javascript_include_tag "diagram" %> +<% end %> +
    diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index be9ab2e5..44631ced 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -41,7 +41,7 @@ agentPaths["New Agent"] = <%= Utils.jsonify new_agent_path %>; agentPaths["Account"] = <%= Utils.jsonify edit_user_registration_path %>; agentPaths["Events Index"] = <%= Utils.jsonify events_path %>; - agentPaths["View Agent Diagram"] = <%= Utils.jsonify diagram_agents_path %>; + agentPaths["View Agent Diagram"] = <%= Utils.jsonify diagram_path %>; agentPaths["Run Event Propagation"] = { url: <%= Utils.jsonify propagate_agents_path %>, method: 'POST' }; diff --git a/app/views/scenarios/show.html.erb b/app/views/scenarios/show.html.erb index 292e6c12..c76cbba2 100644 --- a/app/views/scenarios/show.html.erb +++ b/app/views/scenarios/show.html.erb @@ -15,7 +15,7 @@
    <%= link_to ' Back'.html_safe, scenarios_path, class: "btn btn-default" %> - <%= link_to ' View Diagram'.html_safe, diagram_agents_path(:scenario_id => @scenario.to_param), class: "btn btn-default" %> + <%= link_to ' View Diagram'.html_safe, scenario_diagram_path(@scenario), class: "btn btn-default" %> <%= link_to ' Edit'.html_safe, edit_scenario_path(@scenario), class: "btn btn-default" %> <% if @scenario.source_url.present? %> <%= link_to ' Update'.html_safe, new_scenario_imports_path(:url => @scenario.source_url), class: "btn btn-default" %> diff --git a/config/environments/production.rb b/config/environments/production.rb index f5b8b045..a43a0775 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -61,7 +61,7 @@ Huginn::Application.configure do end # Precompile additional assets (application.js.coffee.erb, application.css, and all non-JS/CSS are already added) - config.assets.precompile += %w( graphing.js user_credentials.js ) + config.assets.precompile += %w( diagram.js graphing.js user_credentials.js ) # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. diff --git a/config/routes.rb b/config/routes.rb index 9df45713..37c3b46e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -11,7 +11,6 @@ Huginn::Application.routes.draw do post :propagate get :type_details get :event_descriptions - get :diagram end resources :logs, :only => [:index] do @@ -19,8 +18,12 @@ Huginn::Application.routes.draw do delete :clear end end + + resources :events, :only => [:index] end + resource :diagram, :only => [:show] + resources :events, :only => [:index, :show, :destroy] do member do post :reemit @@ -36,6 +39,8 @@ Huginn::Application.routes.draw do get :share get :export end + + resource :diagram, :only => [:show] end resources :user_credentials, :except => :show diff --git a/db/migrate/20140730005210_convert_efa_skip_created_at.rb b/db/migrate/20140730005210_convert_efa_skip_created_at.rb new file mode 100644 index 00000000..8029fcf6 --- /dev/null +++ b/db/migrate/20140730005210_convert_efa_skip_created_at.rb @@ -0,0 +1,21 @@ +class ConvertEfaSkipCreatedAt < ActiveRecord::Migration + def up + Agent.where(type: 'Agents::EventFormattingAgent').each do |agent| + agent.options_will_change! + unless agent.options.delete('skip_created_at').to_s == 'true' + agent.options['instructions'] = { + 'created_at' => '{{created_at}}' + }.update(agent.options['instructions'] || {}) + end + agent.save! + end + end + + def down + Agent.where(type: 'Agents::EventFormattingAgent').each do |agent| + agent.options_will_change! + agent.options['skip_created_at'] = (agent.options['instructions'] || {})['created_at'] == '{{created_at}}' + agent.save! + end + end +end diff --git a/db/seeds.rb b/db/seeds.rb index c0afb5df..901bb34f 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -64,7 +64,7 @@ unless user.agents.where(:name => "Rain Notifier").exists? 'value' => "rain|storm", 'path' => "conditions" }], - 'message' => "Just so you know, it looks like '' tomorrow in " + 'message' => "Just so you know, it looks like '{{conditions}}' tomorrow in {{location}}" }).save! end diff --git a/spec/controllers/events_controller_spec.rb b/spec/controllers/events_controller_spec.rb index 8acbae2f..8e9b97ca 100644 --- a/spec/controllers/events_controller_spec.rb +++ b/spec/controllers/events_controller_spec.rb @@ -15,12 +15,12 @@ describe EventsController do it "can filter by Agent" do sign_in users(:bob) - get :index, :agent => agents(:bob_website_agent) + get :index, :agent_id => agents(:bob_website_agent) assigns(:events).length.should == agents(:bob_website_agent).events.length assigns(:events).all? {|i| i.agent.should == agents(:bob_website_agent) }.should be_true lambda { - get :index, :agent => agents(:jane_website_agent) + get :index, :agent_id => agents(:jane_website_agent) }.should raise_error(ActiveRecord::RecordNotFound) end end diff --git a/spec/env.test b/spec/env.test new file mode 100644 index 00000000..c086797c --- /dev/null +++ b/spec/env.test @@ -0,0 +1,5 @@ +APP_SECRET_TOKEN=notarealappsecrettoken +TWITTER_OAUTH_KEY=twitteroauthkey +TWITTER_OAUTH_SECRET=twitteroauthsecret +THIRTY_SEVEN_SIGNALS_OAUTH_KEY=TESTKEY +THIRTY_SEVEN_SIGNALS_OAUTH_SECRET=TESTSECRET diff --git a/spec/fixtures/agents.yml b/spec/fixtures/agents.yml index 6cddb364..d33f2080 100644 --- a/spec/fixtures/agents.yml +++ b/spec/fixtures/agents.yml @@ -72,7 +72,7 @@ jane_rain_notifier_agent: :value => "rain", :path => "conditions" }], - :message => "Just so you know, it looks like '' tomorrow in " + :message => "Just so you know, it looks like '{{conditions}}' tomorrow in {{location}}" }.to_json.inspect %> bob_rain_notifier_agent: @@ -87,7 +87,7 @@ bob_rain_notifier_agent: :value => "rain", :path => "conditions" }], - :message => "Just so you know, it looks like '' tomorrow in " + :message => "Just so you know, it looks like '{{conditions}}' tomorrow in {{location}}" }.to_json.inspect %> bob_twitter_user_agent: diff --git a/spec/helpers/dot_helper_spec.rb b/spec/helpers/dot_helper_spec.rb index e39ea6a4..60f63f2d 100644 --- a/spec/helpers/dot_helper_spec.rb +++ b/spec/helpers/dot_helper_spec.rb @@ -56,13 +56,13 @@ describe DotHelper do it "generates a DOT script" do agents_dot(@agents).should =~ %r{ \A - digraph \s foo \{ + digraph \x20 "Agent \x20 Event \x20 Flow" \{ node \[ [^\]]+ \]; (?\w+) \[label=foo\]; \k -> (?\w+) \[style=dashed\]; \k -> (?\w+) \[color="\#999999"\]; \k \[label=bar1\]; - \k \[label="bar2 \s \(Disabled\)",style="rounded,dashed",color="\#999999",fontcolor="\#999999"\]; + \k \[label=bar2,style="rounded,dashed",color="\#999999",fontcolor="\#999999"\]; \k -> (?\w+) \[style=dashed,color="\#999999"\]; \k \[label=bar3\]; \} @@ -73,15 +73,15 @@ describe DotHelper do it "generates a richer DOT script" do agents_dot(@agents, true).should =~ %r{ \A - digraph \s foo \{ + digraph \x20 "Agent \x20 Event \x20 Flow" \{ node \[ [^\]]+ \]; - (?\w+) \[label=foo,URL="#{Regexp.quote(agent_path(@foo))}"\]; + (?\w+) \[label=foo,tooltip="Dot \x20 Foo",URL="#{Regexp.quote(agent_path(@foo))}"\]; \k -> (?\w+) \[style=dashed\]; \k -> (?\w+) \[color="\#999999"\]; - \k \[label=bar1,URL="#{Regexp.quote(agent_path(@bar1))}"\]; - \k \[label="bar2 \s \(Disabled\)",URL="#{Regexp.quote(agent_path(@bar2))}",style="rounded,dashed",color="\#999999",fontcolor="\#999999"\]; + \k \[label=bar1,tooltip="Dot \x20 Bar",URL="#{Regexp.quote(agent_path(@bar1))}"\]; + \k \[label=bar2,tooltip="Dot \x20 Bar",URL="#{Regexp.quote(agent_path(@bar2))}",style="rounded,dashed",color="\#999999",fontcolor="\#999999"\]; \k -> (?\w+) \[style=dashed,color="\#999999"\]; - \k \[label=bar3,URL="#{Regexp.quote(agent_path(@bar3))}"\]; + \k \[label=bar3,tooltip="Dot \x20 Bar",URL="#{Regexp.quote(agent_path(@bar3))}"\]; \} \z }x diff --git a/spec/models/agents/event_formatting_agent_spec.rb b/spec/models/agents/event_formatting_agent_spec.rb index a13e5d1c..919c23ef 100644 --- a/spec/models/agents/event_formatting_agent_spec.rb +++ b/spec/models/agents/event_formatting_agent_spec.rb @@ -9,6 +9,8 @@ describe Agents::EventFormattingAgent do :message => "Received {{content.text}} from {{content.name}} .", :subject => "Weather looks like {{conditions}} according to the forecast at {{pretty_date.time}}", :agent => "{{agent.type}}", + :created_at => "{{created_at}}", + :created_at_iso => "{{created_at | date:'%FT%T%:z'}}", }, :mode => "clean", :matchers => [ @@ -18,7 +20,6 @@ describe Agents::EventFormattingAgent do :to => "pretty_date", }, ], - :skip_created_at => "false" } } @checker = Agents::EventFormattingAgent.new(@valid_params) @@ -53,18 +54,12 @@ describe Agents::EventFormattingAgent do Event.last.payload[:content].should_not == nil end - it "should accept skip_created_at" do - @checker.receive([@event]) - Event.last.payload[:created_at].should_not == nil - @checker.options[:skip_created_at] = "true" - @checker.receive([@event]) - Event.last.payload[:created_at].should == nil - end - it "should handle Liquid templating in instructions" do @checker.receive([@event]) Event.last.payload[:message].should == "Received Some Lorem Ipsum from somevalue ." Event.last.payload[:agent].should == "WeatherAgent" + Event.last.payload[:created_at].should == @event.created_at.to_s + Event.last.payload[:created_at_iso].should == @event.created_at.iso8601 end it "should handle matchers and Liquid templating in instructions" do @@ -144,10 +139,5 @@ describe Agents::EventFormattingAgent do @checker.options[:mode] = "" @checker.should_not be_valid end - - it "should validate presence of skip_created_at" do - @checker.options[:skip_created_at] = "" - @checker.should_not be_valid - end end end diff --git a/spec/models/agents/ftpsite_agent_spec.rb b/spec/models/agents/ftpsite_agent_spec.rb index 9c216258..5fd411ec 100644 --- a/spec/models/agents/ftpsite_agent_spec.rb +++ b/spec/models/agents/ftpsite_agent_spec.rb @@ -7,19 +7,23 @@ describe Agents::FtpsiteAgent do @site = { 'expected_update_period_in_days' => 1, 'url' => "ftp://ftp.example.org/pub/releases/", - 'patterns' => ["example-*.tar.gz"], + 'patterns' => ["example*.tar.gz"], } @checker = Agents::FtpsiteAgent.new(:name => "Example", :options => @site, :keep_events_for => 2) @checker.user = users(:bob) @checker.save! - stub(@checker).each_entry.returns { |block| - block.call("example-latest.tar.gz", Time.parse("2014-04-01T10:00:01Z")) - block.call("example-1.0.tar.gz", Time.parse("2013-10-01T10:00:00Z")) - block.call("example-1.1.tar.gz", Time.parse("2014-04-01T10:00:00Z")) - } end describe "#check" do + + before do + stub(@checker).each_entry.returns { |block| + block.call("example latest.tar.gz", Time.parse("2014-04-01T10:00:01Z")) + block.call("example-1.0.tar.gz", Time.parse("2013-10-01T10:00:00Z")) + block.call("example-1.1.tar.gz", Time.parse("2014-04-01T10:00:00Z")) + } + end + it "should validate the integer fields" do @checker.options['expected_update_period_in_days'] = "nonsense" lambda { @checker.save! }.should raise_error; @@ -33,7 +37,7 @@ describe Agents::FtpsiteAgent do known_entries.sort_by(&:last).should == [ ["example-1.0.tar.gz", "2013-10-01T10:00:00Z"], ["example-1.1.tar.gz", "2014-04-01T10:00:00Z"], - ["example-latest.tar.gz", "2014-04-01T10:00:01Z"], + ["example latest.tar.gz", "2014-04-01T10:00:01Z"], ] } @@ -46,7 +50,7 @@ describe Agents::FtpsiteAgent do lambda { @checker.check }.should_not change { Event.count } stub(@checker).each_entry.returns { |block| - block.call("example-latest.tar.gz", Time.parse("2014-04-02T10:00:01Z")) + block.call("example latest.tar.gz", Time.parse("2014-04-02T10:00:01Z")) # In the long list format the timestamp may look going # backwards after six months: Oct 01 10:00 -> Oct 01 2013 @@ -62,7 +66,7 @@ describe Agents::FtpsiteAgent do ["example-1.0.tar.gz", "2013-10-01T00:00:00Z"], ["example-1.1.tar.gz", "2014-04-01T10:00:00Z"], ["example-1.2.tar.gz", "2014-04-02T10:00:00Z"], - ["example-latest.tar.gz", "2014-04-02T10:00:01Z"], + ["example latest.tar.gz", "2014-04-02T10:00:01Z"], ] } @@ -75,5 +79,33 @@ describe Agents::FtpsiteAgent do lambda { @checker.check }.should_not change { Event.count } end end + + describe "#each_entry" do + before do + stub.any_instance_of(Net::FTP).list.returns [ # Windows format + "04-02-14 10:01AM 288720748 example latest.tar.gz", + "04-01-14 10:05AM 288720710 no-match-example.tar.gz" + ] + stub(@checker).open_ftp.yields Net::FTP.new + end + + it "filters out files that don't match the given format" do + entries = [] + @checker.each_entry { |a, b| entries.push [a, b] } + + entries.size.should == 1 + filename, mtime = entries.first + filename.should == 'example latest.tar.gz' + mtime.should == '2014-04-02T10:01:00Z' + end + + it "filters out files that are older than the given date" do + @checker.options['after'] = '2015-10-21' + entries = [] + @checker.each_entry { |a, b| entries.push [a, b] } + entries.size.should == 0 + end + end + end end diff --git a/spec/models/agents/hipchat_agent_spec.rb b/spec/models/agents/hipchat_agent_spec.rb index f40b2d5a..8540bf4f 100644 --- a/spec/models/agents/hipchat_agent_spec.rb +++ b/spec/models/agents/hipchat_agent_spec.rb @@ -42,6 +42,12 @@ describe Agents::HipchatAgent do @checker.should be_valid end + it "should also allow a credential" do + @checker.options['auth_token'] = nil + @checker.should_not be_valid + @checker.user.user_credentials.create :credential_name => 'hipchat_auth_token', :credential_value => 'something' + @checker.reload.should be_valid + end end describe "#receive" do diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index eb3818ea..a9170fdf 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -85,6 +85,7 @@ describe EventDrop do before do @event = Event.new @event.agent = agents(:jane_weather_agent) + @event.created_at = Time.at(1400000000) @event.payload = { 'title' => 'some title', 'url' => 'http://some.site.example.org/', @@ -111,4 +112,9 @@ describe EventDrop do t = '{{agent.name}}' interpolate(t, @event).should eq('SF Weather') end + + it 'should have created_at' do + t = '{{created_at | date:"%FT%T%z" }}' + interpolate(t, @event).should eq('2014-05-13T09:53:20-0700') + end end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 1f8dddc0..32387b58 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -59,8 +59,6 @@ describe Service do stub_request(:post, "https://launchpad.37signals.com/authorization/token?client_id=TESTKEY&client_secret=TESTSECRET&refresh_token=refreshtokentest&type=refresh"). to_return(:status => 200, :body => '{"expires_in":1209600,"access_token": "NEWTOKEN"}', :headers => {}) @service.provider = '37signals' - ENV['THIRTY_SEVEN_SIGNALS_OAUTH_KEY'] = 'TESTKEY' - ENV['THIRTY_SEVEN_SIGNALS_OAUTH_SECRET'] = 'TESTSECRET' @service.refresh_token = 'refreshtokentest' @service.refresh_token! @service.token.should == 'NEWTOKEN' diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f4fa126e..27ef9286 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,3 @@ -# This file is copied to spec/ when you run 'rails generate rspec:install' ENV["RAILS_ENV"] ||= 'test' if ENV['COVERAGE'] @@ -9,6 +8,10 @@ else Coveralls.wear!('rails') end +# Required ENV variables that are normally set in .env are setup here for the test environment. +require 'dotenv' +Dotenv.load File.join(File.dirname(__FILE__), "env.test") + require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' require 'rspec/autorun' @@ -19,7 +22,7 @@ WebMock.disable_net_connect! # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. -Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} +Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } ActiveRecord::Migration.maintain_test_schema!