@@ -70,9 +70,11 @@ EMAIL_FROM_ADDRESS=from_address@gmail.com |
||
70 | 70 |
# Number of lines of log messages to keep per Agent |
71 | 71 |
AGENT_LOG_LENGTH=200 |
72 | 72 |
|
73 |
-############################# |
|
74 |
-# OAuth Configuration # |
|
75 |
-############################# |
|
73 |
+######################################################################################################## |
|
74 |
+# OAuth Configuration # |
|
75 |
+# More information at the wiki: https://github.com/cantino/huginn/wiki/Configuring-OAuth-applications # |
|
76 |
+######################################################################################################## |
|
77 |
+ |
|
76 | 78 |
TWITTER_OAUTH_KEY= |
77 | 79 |
TWITTER_OAUTH_SECRET= |
78 | 80 |
|
@@ -2,13 +2,13 @@ language: ruby |
||
2 | 2 |
cache: bundler |
3 | 3 |
bundler_args: --without development production |
4 | 4 |
env: |
5 |
- - APP_SECRET_TOKEN=b2724973fd81c2f4ac0f92ac48eb3f0152c4a11824c122bcf783419a4c51d8b9bba81c8ba6a66c7de599677c7f486242cf819775c433908e77c739c5c8ae118d TWITTER_OAUTH_KEY=twitteroauthkey TWITTER_OAUTH_SECRET=twitteroauthsecret |
|
5 |
+ - APP_SECRET_TOKEN=b2724973fd81c2f4ac0f92ac48eb3f0152c4a11824c122bcf783419a4c51d8b9bba81c8ba6a66c7de599677c7f486242cf819775c433908e77c739c5c8ae118d |
|
6 | 6 |
rvm: |
7 | 7 |
- 2.0.0 |
8 | 8 |
- 2.1.1 |
9 | 9 |
- 1.9.3 |
10 | 10 |
before_install: |
11 |
- - travis_retry gem install bundler |
|
11 |
+ - travis_retry gem install bundler |
|
12 | 12 |
before_script: |
13 | 13 |
- mysql -e 'create database huginn_test;' |
14 | 14 |
- bundle exec rake db:migrate db:test:prepare |
@@ -56,6 +56,8 @@ gem 'uglifier', '>= 1.3.0' |
||
56 | 56 |
gem 'select2-rails', '~> 3.5.4' |
57 | 57 |
gem 'jquery-rails', '~> 3.1.0' |
58 | 58 |
gem 'ace-rails-ap', '~> 2.0.1' |
59 |
+gem 'spectrum-rails' |
|
60 |
+ |
|
59 | 61 |
|
60 | 62 |
# geokit-rails doesn't work with geokit 1.8.X but it specifies ~> 1.5 |
61 | 63 |
# in its own Gemfile. |
@@ -128,4 +130,3 @@ else |
||
128 | 130 |
gem 'unicorn', platform: :ruby_18 |
129 | 131 |
gem 'rails_12factor', platform: :ruby_18 |
130 | 132 |
end |
131 |
- |
@@ -315,6 +315,8 @@ GEM |
||
315 | 315 |
simplecov-html (0.8.0) |
316 | 316 |
slack-notifier (0.5.0) |
317 | 317 |
slop (3.6.0) |
318 |
+ spectrum-rails (1.3.4) |
|
319 |
+ railties (>= 3.1) |
|
318 | 320 |
sprockets (2.11.0) |
319 | 321 |
hike (~> 1.2) |
320 | 322 |
multi_json (~> 1.0) |
@@ -444,6 +446,7 @@ DEPENDENCIES |
||
444 | 446 |
select2-rails (~> 3.5.4) |
445 | 447 |
shoulda-matchers |
446 | 448 |
slack-notifier (~> 0.5.0) |
449 |
+ spectrum-rails |
|
447 | 450 |
therubyracer (~> 0.12.1) |
448 | 451 |
twilio-ruby (~> 3.11.5) |
449 | 452 |
twitter (~> 5.8.0) |
@@ -6,6 +6,7 @@ |
||
6 | 6 |
#= require json2 |
7 | 7 |
#= require jquery.json-editor |
8 | 8 |
#= require latlon_and_geo |
9 |
+#= require spectrum |
|
9 | 10 |
#= require ./worker-checker |
10 | 11 |
#= require_self |
11 | 12 |
|
@@ -60,6 +61,10 @@ showEventDescriptions = -> |
||
60 | 61 |
$(".event-descriptions").html("").hide() |
61 | 62 |
|
62 | 63 |
$(document).ready -> |
64 |
+ $('.navbar .dropdown.dropdown-hover').hover \ |
|
65 |
+ -> $(this).addClass('open'), |
|
66 |
+ -> $(this).removeClass('open') |
|
67 |
+ |
|
63 | 68 |
# JSON Editor |
64 | 69 |
window.jsonEditor = setupJsonEditor()[0] |
65 | 70 |
|
@@ -164,7 +169,7 @@ $(document).ready -> |
||
164 | 169 |
|
165 | 170 |
$(".description").html(json.description_html) if json.description_html? |
166 | 171 |
|
167 |
- $('.oauthable-form').html($(json.form).find('.oauthable-form').html()) if json.form? |
|
172 |
+ $('.oauthable-form').html(json.form) if json.form? |
|
168 | 173 |
|
169 | 174 |
if $("#agent_options").hasClass("showing-default") || $("#agent_options").val().match(/\A\s*(\{\s*\}|)\s*\Z/g) |
170 | 175 |
window.jsonEditor.json = json.options |
@@ -12,6 +12,7 @@ |
||
12 | 12 |
*= require select2-bootstrap |
13 | 13 |
*= require jquery.json-editor |
14 | 14 |
*= require rickshaw |
15 |
+ *= require spectrum |
|
15 | 16 |
*= require_tree . |
16 | 17 |
*= require_self |
17 | 18 |
*/ |
@@ -186,3 +187,17 @@ h2 .scenario, a span.label.scenario { |
||
186 | 187 |
.color-success { |
187 | 188 |
color: #5cb85c; |
188 | 189 |
} |
190 |
+ |
|
191 |
+.form-group { |
|
192 |
+ .sp-replacer { |
|
193 |
+ @extend .form-control; |
|
194 |
+ } |
|
195 |
+ |
|
196 |
+ .sp-preview { |
|
197 |
+ width: 100%; |
|
198 |
+ } |
|
199 |
+ |
|
200 |
+ .sp-dd { |
|
201 |
+ display: none; |
|
202 |
+ } |
|
203 |
+} |
@@ -1,9 +1,50 @@ |
||
1 | 1 |
module LiquidDroppable |
2 | 2 |
extend ActiveSupport::Concern |
3 | 3 |
|
4 |
+ # In subclasses of this base class, "locals" take precedence over |
|
5 |
+ # methods. |
|
4 | 6 |
class Drop < Liquid::Drop |
5 |
- def initialize(object) |
|
7 |
+ class << self |
|
8 |
+ def inherited(subclass) |
|
9 |
+ class << subclass |
|
10 |
+ attr_reader :drop_methods |
|
11 |
+ |
|
12 |
+ # Make all public methods private so that #before_method |
|
13 |
+ # catches everything. |
|
14 |
+ def drop_methods! |
|
15 |
+ return if @drop_methods |
|
16 |
+ |
|
17 |
+ @drop_methods = Set.new |
|
18 |
+ |
|
19 |
+ (public_instance_methods - Drop.public_instance_methods).each { |name| |
|
20 |
+ @drop_methods << name.to_s |
|
21 |
+ private name |
|
22 |
+ } |
|
23 |
+ end |
|
24 |
+ end |
|
25 |
+ end |
|
26 |
+ end |
|
27 |
+ |
|
28 |
+ def initialize(object, locals = nil) |
|
29 |
+ self.class.drop_methods! |
|
30 |
+ |
|
6 | 31 |
@object = object |
32 |
+ @locals = locals || {} |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ def before_method(name) |
|
36 |
+ if @locals.include?(name) |
|
37 |
+ @locals[name] |
|
38 |
+ elsif self.class.drop_methods.include?(name) |
|
39 |
+ __send__(name) |
|
40 |
+ end |
|
41 |
+ end |
|
42 |
+ |
|
43 |
+ def each |
|
44 |
+ return to_enum(__method__) unless block_given? |
|
45 |
+ self.class.drop_methods.each { |name| |
|
46 |
+ yield [name, __send__(name)] |
|
47 |
+ } |
|
7 | 48 |
end |
8 | 49 |
end |
9 | 50 |
|
@@ -11,11 +11,11 @@ module Oauthable |
||
11 | 11 |
true |
12 | 12 |
end |
13 | 13 |
|
14 |
- def valid_services(current_user) |
|
14 |
+ def valid_services_for(user) |
|
15 | 15 |
if valid_oauth_providers == :all |
16 |
- current_user.available_services |
|
16 |
+ user.available_services |
|
17 | 17 |
else |
18 |
- current_user.available_services.where(provider: valid_oauth_providers) |
|
18 |
+ user.available_services.where(provider: valid_oauth_providers) |
|
19 | 19 |
end |
20 | 20 |
end |
21 | 21 |
|
@@ -25,11 +25,11 @@ module TwitterConcern |
||
25 | 25 |
end |
26 | 26 |
|
27 | 27 |
def twitter_oauth_token |
28 |
- self.service.token |
|
28 |
+ service.token |
|
29 | 29 |
end |
30 | 30 |
|
31 | 31 |
def twitter_oauth_token_secret |
32 |
- self.service.secret |
|
32 |
+ service.secret |
|
33 | 33 |
end |
34 | 34 |
|
35 | 35 |
def twitter |
@@ -1,3 +1,6 @@ |
||
1 |
+require 'faraday' |
|
2 |
+require 'faraday_middleware' |
|
3 |
+ |
|
1 | 4 |
module WebRequestConcern |
2 | 5 |
extend ActiveSupport::Concern |
3 | 6 |
|
@@ -31,14 +31,15 @@ class AgentsController < ApplicationController |
||
31 | 31 |
end |
32 | 32 |
|
33 | 33 |
def type_details |
34 |
- agent = Agent.build_for_type(params[:type], current_user, {}) |
|
34 |
+ @agent = Agent.build_for_type(params[:type], current_user, {}) |
|
35 | 35 |
render :json => { |
36 |
- :can_be_scheduled => agent.can_be_scheduled?, |
|
37 |
- :default_schedule => agent.default_schedule, |
|
38 |
- :can_receive_events => agent.can_receive_events?, |
|
39 |
- :can_create_events => agent.can_create_events?, |
|
40 |
- :options => agent.default_options, |
|
41 |
- :description_html => agent.html_description |
|
36 |
+ :can_be_scheduled => @agent.can_be_scheduled?, |
|
37 |
+ :default_schedule => @agent.default_schedule, |
|
38 |
+ :can_receive_events => @agent.can_receive_events?, |
|
39 |
+ :can_create_events => @agent.can_create_events?, |
|
40 |
+ :options => @agent.default_options, |
|
41 |
+ :description_html => @agent.html_description, |
|
42 |
+ :form => render_to_string(partial: 'oauth_dropdown') |
|
42 | 43 |
} |
43 | 44 |
end |
44 | 45 |
|
@@ -13,4 +13,27 @@ class ApplicationController < ActionController::Base |
||
13 | 13 |
devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:login, :username, :email, :password, :remember_me) } |
14 | 14 |
devise_parameter_sanitizer.for(:account_update) { |u| u.permit(:username, :email, :password, :password_confirmation, :current_password) } |
15 | 15 |
end |
16 |
+ |
|
17 |
+ def upgrade_warning |
|
18 |
+ return unless current_user |
|
19 |
+ twitter_oauth_check |
|
20 |
+ basecamp_auth_check |
|
21 |
+ end |
|
22 |
+ |
|
23 |
+ private |
|
24 |
+ def twitter_oauth_check |
|
25 |
+ if ENV['TWITTER_OAUTH_KEY'].blank? || ENV['TWITTER_OAUTH_SECRET'].blank? |
|
26 |
+ if @twitter_agent = current_user.agents.where("type like 'Agents::Twitter%'").first |
|
27 |
+ @twitter_oauth_key = @twitter_agent.options['consumer_key'].presence || @twitter_agent.credential('twitter_consumer_key') |
|
28 |
+ @twitter_oauth_secret = @twitter_agent.options['consumer_secret'].presence || @twitter_agent.credential('twitter_consumer_secret') |
|
29 |
+ end |
|
30 |
+ end |
|
31 |
+ end |
|
32 |
+ |
|
33 |
+ def basecamp_auth_check |
|
34 |
+ if ENV['THIRTY_SEVEN_SIGNALS_OAUTH_KEY'].blank? || ENV['THIRTY_SEVEN_SIGNALS_OAUTH_SECRET'].blank? |
|
35 |
+ @basecamp_agent = current_user.agents.where(type: 'Agents::BasecampAgent').first |
|
36 |
+ end |
|
37 |
+ end |
|
38 |
+ |
|
16 | 39 |
end |
@@ -1,6 +1,8 @@ |
||
1 | 1 |
class HomeController < ApplicationController |
2 | 2 |
skip_before_filter :authenticate_user! |
3 | 3 |
|
4 |
+ before_filter :upgrade_warning, only: :index |
|
5 |
+ |
|
4 | 6 |
def index |
5 | 7 |
end |
6 | 8 |
|
@@ -45,6 +45,8 @@ class ScenariosController < ApplicationController |
||
45 | 45 |
@exporter = AgentsExporter.new(:name => @scenario.name, |
46 | 46 |
:description => @scenario.description, |
47 | 47 |
:guid => @scenario.guid, |
48 |
+ :tag_fg_color => @scenario.tag_fg_color, |
|
49 |
+ :tag_bg_color => @scenario.tag_bg_color, |
|
48 | 50 |
:source_url => @scenario.public? && export_scenario_url(@scenario), |
49 | 51 |
:agents => @scenario.agents) |
50 | 52 |
response.headers['Content-Disposition'] = 'attachment; filename="' + @exporter.filename + '"' |
@@ -1,4 +1,5 @@ |
||
1 | 1 |
class ServicesController < ApplicationController |
2 |
+ before_filter :upgrade_warning, only: :index |
|
2 | 3 |
|
3 | 4 |
def index |
4 | 5 |
@services = current_user.services.page(params[:page]) |
@@ -8,11 +8,11 @@ module AgentHelper |
||
8 | 8 |
|
9 | 9 |
def scenario_links(agent) |
10 | 10 |
agent.scenarios.map { |scenario| |
11 |
- link_to(scenario.name, scenario, class: "label label-info") |
|
11 |
+ link_to(scenario.name, scenario, class: "label", style: style_colors(scenario)) |
|
12 | 12 |
}.join(" ").html_safe |
13 | 13 |
end |
14 | 14 |
|
15 | 15 |
def agent_show_class(agent) |
16 | 16 |
agent.short_type.underscore.dasherize |
17 | 17 |
end |
18 |
-end |
|
18 |
+end |
@@ -0,0 +1,7 @@ |
||
1 |
+module MarkdownHelper |
|
2 |
+ |
|
3 |
+ def markdown(text) |
|
4 |
+ Kramdown::Document.new(text, :auto_ids => false).to_html.html_safe |
|
5 |
+ end |
|
6 |
+ |
|
7 |
+end |
@@ -0,0 +1,23 @@ |
||
1 |
+module ScenarioHelper |
|
2 |
+ |
|
3 |
+ def style_colors(scenario) |
|
4 |
+ colors = { |
|
5 |
+ color: scenario.tag_fg_color || default_scenario_fg_color, |
|
6 |
+ background_color: scenario.tag_bg_color || default_scenario_bg_color |
|
7 |
+ }.map { |key, value| "#{key.to_s.dasherize}:#{value}" }.join(';') |
|
8 |
+ end |
|
9 |
+ |
|
10 |
+ def scenario_label(scenario, text = nil) |
|
11 |
+ text ||= scenario.name |
|
12 |
+ content_tag :span, text, class: 'label scenario', style: style_colors(scenario) |
|
13 |
+ end |
|
14 |
+ |
|
15 |
+ def default_scenario_bg_color |
|
16 |
+ '#5BC0DE' |
|
17 |
+ end |
|
18 |
+ |
|
19 |
+ def default_scenario_fg_color |
|
20 |
+ '#FFFFFF' |
|
21 |
+ end |
|
22 |
+ |
|
23 |
+end |
@@ -0,0 +1,5 @@ |
||
1 |
+module ServiceHelper |
|
2 |
+ def has_oauth_configuration_for(provider) |
|
3 |
+ ENV["#{provider.upcase}_OAUTH_KEY"].present? && ENV["#{provider.upcase}_OAUTH_SECRET"].present? |
|
4 |
+ end |
|
5 |
+end |
@@ -44,7 +44,7 @@ class Agent < ActiveRecord::Base |
||
44 | 44 |
after_save :possibly_update_event_expirations |
45 | 45 |
|
46 | 46 |
belongs_to :user, :inverse_of => :agents |
47 |
- belongs_to :service |
|
47 |
+ belongs_to :service, :inverse_of => :agents |
|
48 | 48 |
has_many :events, -> { order("events.id desc") }, :dependent => :delete_all, :inverse_of => :agent |
49 | 49 |
has_one :most_recent_event, :inverse_of => :agent, :class_name => "Event", :order => "events.id desc" |
50 | 50 |
has_many :logs, -> { order("agent_logs.id desc") }, :dependent => :delete_all, :inverse_of => :agent, :class_name => "AgentLog" |
@@ -392,7 +392,7 @@ class AgentDrop |
||
392 | 392 |
@object.short_type |
393 | 393 |
end |
394 | 394 |
|
395 |
- METHODS = [ |
|
395 |
+ [ |
|
396 | 396 |
:name, |
397 | 397 |
:type, |
398 | 398 |
:options, |
@@ -403,19 +403,9 @@ class AgentDrop |
||
403 | 403 |
:disabled, |
404 | 404 |
:keep_events_for, |
405 | 405 |
:propagate_immediately, |
406 |
- ] |
|
407 |
- |
|
408 |
- METHODS.each { |attr| |
|
406 |
+ ].each { |attr| |
|
409 | 407 |
define_method(attr) { |
410 | 408 |
@object.__send__(attr) |
411 | 409 |
} unless method_defined?(attr) |
412 | 410 |
} |
413 |
- |
|
414 |
- def each(&block) |
|
415 |
- return to_enum(__method__) unless block |
|
416 |
- |
|
417 |
- METHODS.each { |attr| |
|
418 |
- yield [attr, __sent__(attr)] |
|
419 |
- } |
|
420 |
- end |
|
421 | 411 |
end |
@@ -21,25 +21,25 @@ module Agents |
||
21 | 21 |
event_description <<-MD |
22 | 22 |
Events are the raw JSON provided by the Basecamp API. Should look something like: |
23 | 23 |
|
24 |
- { |
|
25 |
- "creator": { |
|
26 |
- "fullsize_avatar_url": "https://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/original.gif?r=3", |
|
27 |
- "avatar_url": "http://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/avatar.gif?r=3", |
|
28 |
- "name": "Dominik Sander", |
|
29 |
- "id": 123456 |
|
30 |
- }, |
|
31 |
- "attachments": [], |
|
32 |
- "raw_excerpt": "test test", |
|
33 |
- "excerpt": "test test", |
|
34 |
- "id": 6454342343, |
|
35 |
- "created_at": "2014-04-17T10:25:31.000+02:00", |
|
36 |
- "updated_at": "2014-04-17T10:25:31.000+02:00", |
|
37 |
- "summary": "commented on whaat", |
|
38 |
- "action": "commented on", |
|
39 |
- "target": "whaat", |
|
40 |
- "url": "https://basecamp.com/12456/api/v1/projects/76454545-explore-basecamp/messages/76454545-whaat.json", |
|
41 |
- "html_url": "https://basecamp.com/12456/projects/76454545-explore-basecamp/messages/76454545-whaat#comment_76454545" |
|
42 |
- } |
|
24 |
+ { |
|
25 |
+ "creator": { |
|
26 |
+ "fullsize_avatar_url": "https://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/original.gif?r=3", |
|
27 |
+ "avatar_url": "http://dge9rmgqjs8m1.cloudfront.net/global/dfsdfsdfdsf/avatar.gif?r=3", |
|
28 |
+ "name": "Dominik Sander", |
|
29 |
+ "id": 123456 |
|
30 |
+ }, |
|
31 |
+ "attachments": [], |
|
32 |
+ "raw_excerpt": "test test", |
|
33 |
+ "excerpt": "test test", |
|
34 |
+ "id": 6454342343, |
|
35 |
+ "created_at": "2014-04-17T10:25:31.000+02:00", |
|
36 |
+ "updated_at": "2014-04-17T10:25:31.000+02:00", |
|
37 |
+ "summary": "commented on whaat", |
|
38 |
+ "action": "commented on", |
|
39 |
+ "target": "whaat", |
|
40 |
+ "url": "https://basecamp.com/12456/api/v1/projects/76454545-explore-basecamp/messages/76454545-whaat.json", |
|
41 |
+ "html_url": "https://basecamp.com/12456/projects/76454545-explore-basecamp/messages/76454545-whaat#comment_76454545" |
|
42 |
+ } |
|
43 | 43 |
MD |
44 | 44 |
|
45 | 45 |
default_schedule "every_10m" |
@@ -59,28 +59,29 @@ module Agents |
||
59 | 59 |
end |
60 | 60 |
|
61 | 61 |
def check |
62 |
- self.service.prepare_request |
|
62 |
+ service.prepare_request |
|
63 | 63 |
reponse = HTTParty.get request_url, request_options.merge(query_parameters) |
64 |
- memory[:last_run] = Time.now.utc.iso8601 |
|
65 |
- if last_check_at != nil |
|
66 |
- JSON.parse(reponse.body).each do |event| |
|
64 |
+ events = JSON.parse(reponse.body) |
|
65 |
+ if !memory[:last_event].nil? |
|
66 |
+ events.each do |event| |
|
67 | 67 |
create_event :payload => event |
68 | 68 |
end |
69 | 69 |
end |
70 |
+ memory[:last_event] = events.first['created_at'] if events.length > 0 |
|
70 | 71 |
save! |
71 | 72 |
end |
72 | 73 |
|
73 | 74 |
private |
74 | 75 |
def request_url |
75 |
- "https://basecamp.com/#{URI.encode(self.service.options[:user_id].to_s)}/api/v1/projects/#{URI.encode(interpolated[:project_id].to_s)}/events.json" |
|
76 |
+ "https://basecamp.com/#{URI.encode(service.options[:user_id].to_s)}/api/v1/projects/#{URI.encode(interpolated[:project_id].to_s)}/events.json" |
|
76 | 77 |
end |
77 | 78 |
|
78 | 79 |
def request_options |
79 |
- {:headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)", "Authorization" => "Bearer \"#{self.service.token}\""}} |
|
80 |
+ {:headers => {"User-Agent" => "Huginn (https://github.com/cantino/huginn)", "Authorization" => "Bearer \"#{service.token}\""}} |
|
80 | 81 |
end |
81 | 82 |
|
82 | 83 |
def query_parameters |
83 |
- memory[:last_run].present? ? { :query => {:since => memory[:last_run]} } : {} |
|
84 |
+ memory[:last_event].present? ? { :query => {:since => memory[:last_event]} } : {} |
|
84 | 85 |
end |
85 | 86 |
end |
86 | 87 |
end |
@@ -51,7 +51,7 @@ module Agents |
||
51 | 51 |
{ |
52 | 52 |
"path": "{{date.pretty}}", |
53 | 53 |
"regexp": "\\A(?<time>\\d\\d:\\d\\d [AP]M [A-Z]+)", |
54 |
- "to": "pretty_date", |
|
54 |
+ "to": "pretty_date" |
|
55 | 55 |
} |
56 | 56 |
] |
57 | 57 |
} |
@@ -61,7 +61,7 @@ module Agents |
||
61 | 61 |
"pretty_date": { |
62 | 62 |
"time": "10:00 PM EST", |
63 | 63 |
"0": "10:00 PM EST on January 11, 2013" |
64 |
- "1": "10:00 PM EST", |
|
64 |
+ "1": "10:00 PM EST" |
|
65 | 65 |
} |
66 | 66 |
|
67 | 67 |
So you can use it in `instructions` like this: |
@@ -80,7 +80,19 @@ module Agents |
||
80 | 80 |
} |
81 | 81 |
MD |
82 | 82 |
|
83 |
- event_description "User defined" |
|
83 |
+ event_description do |
|
84 |
+ "Events will have the following fields%s:\n\n %s" % [ |
|
85 |
+ case options['mode'].to_s |
|
86 |
+ when 'merged' |
|
87 |
+ ', merged with the original contents' |
|
88 |
+ when /\{/ |
|
89 |
+ ', conditionally merged with the original contents' |
|
90 |
+ end, |
|
91 |
+ Utils.pretty_print(Hash[options['instructions'].keys.map { |key| |
|
92 |
+ [key, "..."] |
|
93 |
+ }]) |
|
94 |
+ ] |
|
95 |
+ end |
|
84 | 96 |
|
85 | 97 |
after_save :clear_matchers |
86 | 98 |
|
@@ -62,7 +62,7 @@ module Agents |
||
62 | 62 |
.... |
63 | 63 |
}, |
64 | 64 |
'agent_id' => 1234, |
65 |
- 'event_id' => 3432, |
|
65 |
+ 'event_id' => 3432 |
|
66 | 66 |
} |
67 | 67 |
MD |
68 | 68 |
|
@@ -1,6 +1,4 @@ |
||
1 | 1 |
require 'nokogiri' |
2 |
-require 'faraday' |
|
3 |
-require 'faraday_middleware' |
|
4 | 2 |
require 'date' |
5 | 3 |
|
6 | 4 |
module Agents |
@@ -19,7 +17,7 @@ module Agents |
||
19 | 17 |
|
20 | 18 |
`url` can be a single url, or an array of urls (for example, for multiple pages with the exact same structure but different content to scrape) |
21 | 19 |
|
22 |
- The `type` value can be `xml`, `html`, or `json`. |
|
20 |
+ The `type` value can be `xml`, `html`, `json`, or `text`. |
|
23 | 21 |
|
24 | 22 |
To tell the Agent how to parse the content, specify `extract` as a hash with keys naming the extractions and values of hashes. |
25 | 23 |
|
@@ -40,6 +38,28 @@ module Agents |
||
40 | 38 |
"description": { "path": "results.data[*].description" } |
41 | 39 |
} |
42 | 40 |
|
41 |
+ When parsing text, each sub-hash should contain a `regexp` and `index`. Output text is matched against the regular expression repeatedly from the beginning through to the end, collecting a captured group specified by `index` in each match. Each index should be either an integer or a string name which corresponds to `(?<_name_>...)`. For example, to parse lines of `_word_: _definition_`, the following should work: |
|
42 |
+ |
|
43 |
+ "extract": { |
|
44 |
+ "word": { "regexp": "^(.+?): (.+)$", index: 1 }, |
|
45 |
+ "definition": { "regexp": "^(.+?): (.+)$", index: 2 } |
|
46 |
+ } |
|
47 |
+ |
|
48 |
+ Or if you prefer names to numbers for index: |
|
49 |
+ |
|
50 |
+ "extract": { |
|
51 |
+ "word": { "regexp": "^(?<word>.+?): (?<definition>.+)$", index: 'word' }, |
|
52 |
+ "definition": { "regexp": "^(?<word>.+?): (?<definition>.+)$", index: 'definition' } |
|
53 |
+ } |
|
54 |
+ |
|
55 |
+ To extract the whole content as one event: |
|
56 |
+ |
|
57 |
+ "extract": { |
|
58 |
+ "content": { "regexp": "\A(?m:.)*\z", index: 0 } |
|
59 |
+ } |
|
60 |
+ |
|
61 |
+ Beware that `.` does not match the newline character (LF) unless the `m` flag is in effect, and `^`/`$` basically match every line beginning/end. See [this document](http://ruby-doc.org/core-#{RUBY_VERSION}/doc/regexp_rdoc.html) to learn the regular expression variant used in this service. |
|
62 |
+ |
|
43 | 63 |
Note that for all of the formats, whatever you extract MUST have the same number of matches for each extractor. E.g., if you're extracting rows, all extractors must match all rows. For generating CSS selectors, something like [SelectorGadget](http://selectorgadget.com) may be helpful. |
44 | 64 |
|
45 | 65 |
Can be configured to use HTTP basic auth by including the `basic_auth` parameter with `"username:password"`, or `["username", "password"]`. |
@@ -58,7 +78,11 @@ module Agents |
||
58 | 78 |
MD |
59 | 79 |
|
60 | 80 |
event_description do |
61 |
- "Events will have the fields you specified. Your options look like:\n\n #{Utils.pretty_print interpolated['extract']}" |
|
81 |
+ "Events will have the following fields:\n\n %s" % [ |
|
82 |
+ Utils.pretty_print(Hash[options['extract'].keys.map { |key| |
|
83 |
+ [key, "..."] |
|
84 |
+ }]) |
|
85 |
+ ] |
|
62 | 86 |
end |
63 | 87 |
|
64 | 88 |
def working? |
@@ -137,77 +161,60 @@ module Agents |
||
137 | 161 |
log "Storing new result for '#{name}': #{doc.inspect}" |
138 | 162 |
create_event :payload => doc |
139 | 163 |
end |
140 |
- else |
|
141 |
- output = {} |
|
142 |
- interpolated['extract'].each do |name, extraction_details| |
|
143 |
- if extraction_type == "json" |
|
144 |
- result = Utils.values_at(doc, extraction_details['path']) |
|
145 |
- log "Extracting #{extraction_type} at #{extraction_details['path']}: #{result}" |
|
146 |
- else |
|
147 |
- case |
|
148 |
- when css = extraction_details['css'] |
|
149 |
- nodes = doc.css(css) |
|
150 |
- when xpath = extraction_details['xpath'] |
|
151 |
- doc.remove_namespaces! # ignore xmlns, useful when parsing atom feeds |
|
152 |
- nodes = doc.xpath(xpath) |
|
153 |
- else |
|
154 |
- error '"css" or "xpath" is required for HTML or XML extraction' |
|
155 |
- return |
|
156 |
- end |
|
157 |
- case nodes |
|
158 |
- when Nokogiri::XML::NodeSet |
|
159 |
- result = nodes.map { |node| |
|
160 |
- case value = node.xpath(extraction_details['value']) |
|
161 |
- when Float |
|
162 |
- # Node#xpath() returns any numeric value as float; |
|
163 |
- # convert it to integer as appropriate. |
|
164 |
- value = value.to_i if value.to_i == value |
|
165 |
- end |
|
166 |
- value.to_s |
|
167 |
- } |
|
168 |
- else |
|
169 |
- error "The result of HTML/XML extraction was not a NodeSet" |
|
170 |
- return |
|
171 |
- end |
|
172 |
- log "Extracting #{extraction_type} at #{xpath || css}: #{result}" |
|
173 |
- end |
|
174 |
- output[name] = result |
|
164 |
+ next |
|
165 |
+ end |
|
166 |
+ |
|
167 |
+ output = |
|
168 |
+ case extraction_type |
|
169 |
+ when 'json' |
|
170 |
+ extract_json(doc) |
|
171 |
+ when 'text' |
|
172 |
+ extract_text(doc) |
|
173 |
+ else |
|
174 |
+ extract_xml(doc) |
|
175 | 175 |
end |
176 | 176 |
|
177 |
- num_unique_lengths = interpolated['extract'].keys.map { |name| output[name].length }.uniq |
|
177 |
+ num_unique_lengths = interpolated['extract'].keys.map { |name| output[name].length }.uniq |
|
178 | 178 |
|
179 |
- if num_unique_lengths.length != 1 |
|
180 |
- error "Got an uneven number of matches for #{interpolated['name']}: #{interpolated['extract'].inspect}" |
|
181 |
- return |
|
182 |
- end |
|
179 |
+ if num_unique_lengths.length != 1 |
|
180 |
+ raise "Got an uneven number of matches for #{interpolated['name']}: #{interpolated['extract'].inspect}" |
|
181 |
+ end |
|
183 | 182 |
|
184 |
- old_events = previous_payloads num_unique_lengths.first |
|
185 |
- num_unique_lengths.first.times do |index| |
|
186 |
- result = {} |
|
187 |
- interpolated['extract'].keys.each do |name| |
|
188 |
- result[name] = output[name][index] |
|
189 |
- if name.to_s == 'url' |
|
190 |
- result[name] = (response.env[:url] + result[name]).to_s |
|
191 |
- end |
|
183 |
+ old_events = previous_payloads num_unique_lengths.first |
|
184 |
+ num_unique_lengths.first.times do |index| |
|
185 |
+ result = {} |
|
186 |
+ interpolated['extract'].keys.each do |name| |
|
187 |
+ result[name] = output[name][index] |
|
188 |
+ if name.to_s == 'url' |
|
189 |
+ result[name] = (response.env[:url] + result[name]).to_s |
|
192 | 190 |
end |
191 |
+ end |
|
193 | 192 |
|
194 |
- if store_payload!(old_events, result) |
|
195 |
- log "Storing new parsed result for '#{name}': #{result.inspect}" |
|
196 |
- create_event :payload => result |
|
197 |
- end |
|
193 |
+ if store_payload!(old_events, result) |
|
194 |
+ log "Storing new parsed result for '#{name}': #{result.inspect}" |
|
195 |
+ create_event :payload => result |
|
198 | 196 |
end |
199 | 197 |
end |
200 | 198 |
else |
201 |
- error "Failed: #{response.inspect}" |
|
199 |
+ raise "Failed: #{response.inspect}" |
|
202 | 200 |
end |
203 | 201 |
end |
202 |
+ rescue => e |
|
203 |
+ error e.message |
|
204 | 204 |
end |
205 | 205 |
|
206 | 206 |
def receive(incoming_events) |
207 | 207 |
incoming_events.each do |event| |
208 |
+ Thread.current[:current_event] = event |
|
208 | 209 |
url_to_scrape = event.payload['url'] |
209 | 210 |
check_url(url_to_scrape) if url_to_scrape =~ /^https?:\/\//i |
210 | 211 |
end |
212 |
+ ensure |
|
213 |
+ Thread.current[:current_event] = nil |
|
214 |
+ end |
|
215 |
+ |
|
216 |
+ def interpolated(event = Thread.current[:current_event]) |
|
217 |
+ super |
|
211 | 218 |
end |
212 | 219 |
|
213 | 220 |
private |
@@ -216,22 +223,22 @@ module Agents |
||
216 | 223 |
# If mode is set to 'on_change', this method may return false and update an existing |
217 | 224 |
# event to expire further in the future. |
218 | 225 |
def store_payload!(old_events, result) |
219 |
- if !interpolated['mode'].present? |
|
220 |
- return true |
|
221 |
- elsif interpolated['mode'].to_s == "all" |
|
222 |
- return true |
|
223 |
- elsif interpolated['mode'].to_s == "on_change" |
|
226 |
+ case interpolated['mode'].presence |
|
227 |
+ when 'on_change' |
|
224 | 228 |
result_json = result.to_json |
225 | 229 |
old_events.each do |old_event| |
226 | 230 |
if old_event.payload.to_json == result_json |
227 | 231 |
old_event.expires_at = new_event_expiration_date |
228 | 232 |
old_event.save! |
229 | 233 |
return false |
230 |
- end |
|
234 |
+ end |
|
231 | 235 |
end |
232 |
- return true |
|
236 |
+ true |
|
237 |
+ when 'all', '' |
|
238 |
+ true |
|
239 |
+ else |
|
240 |
+ raise "Illegal options[mode]: #{interpolated['mode']}" |
|
233 | 241 |
end |
234 |
- raise "Illegal options[mode]: " + interpolated['mode'].to_s |
|
235 | 242 |
end |
236 | 243 |
|
237 | 244 |
def previous_payloads(num_events) |
@@ -244,7 +251,7 @@ module Agents |
||
244 | 251 |
look_back = UNIQUENESS_LOOK_BACK |
245 | 252 |
end |
246 | 253 |
end |
247 |
- events.order("id desc").limit(look_back) if interpolated['mode'].present? && interpolated['mode'].to_s == "on_change" |
|
254 |
+ events.order("id desc").limit(look_back) if interpolated['mode'] == "on_change" |
|
248 | 255 |
end |
249 | 256 |
|
250 | 257 |
def extract_full_json? |
@@ -253,35 +260,94 @@ module Agents |
||
253 | 260 |
|
254 | 261 |
def extraction_type |
255 | 262 |
(interpolated['type'] || begin |
256 |
- if interpolated['url'] =~ /\.(rss|xml)$/i |
|
263 |
+ case interpolated['url'] |
|
264 |
+ when /\.(rss|xml)$/i |
|
257 | 265 |
"xml" |
258 |
- elsif interpolated['url'] =~ /\.json$/i |
|
266 |
+ when /\.json$/i |
|
259 | 267 |
"json" |
268 |
+ when /\.(txt|text)$/i |
|
269 |
+ "text" |
|
260 | 270 |
else |
261 | 271 |
"html" |
262 | 272 |
end |
263 | 273 |
end).to_s |
264 | 274 |
end |
265 | 275 |
|
276 |
+ def extract_each(doc, &block) |
|
277 |
+ interpolated['extract'].each_with_object({}) { |(name, extraction_details), output| |
|
278 |
+ output[name] = block.call(extraction_details) |
|
279 |
+ } |
|
280 |
+ end |
|
281 |
+ |
|
282 |
+ def extract_json(doc) |
|
283 |
+ extract_each(doc) { |extraction_details| |
|
284 |
+ result = Utils.values_at(doc, extraction_details['path']) |
|
285 |
+ log "Extracting #{extraction_type} at #{extraction_details['path']}: #{result}" |
|
286 |
+ result |
|
287 |
+ } |
|
288 |
+ end |
|
289 |
+ |
|
290 |
+ def extract_text(doc) |
|
291 |
+ extract_each(doc) { |extraction_details| |
|
292 |
+ regexp = Regexp.new(extraction_details['regexp']) |
|
293 |
+ result = [] |
|
294 |
+ doc.scan(regexp) { |
|
295 |
+ result << Regexp.last_match[extraction_details['index']] |
|
296 |
+ } |
|
297 |
+ log "Extracting #{extraction_type} at #{regexp}: #{result}" |
|
298 |
+ result |
|
299 |
+ } |
|
300 |
+ end |
|
301 |
+ |
|
302 |
+ def extract_xml(doc) |
|
303 |
+ extract_each(doc) { |extraction_details| |
|
304 |
+ case |
|
305 |
+ when css = extraction_details['css'] |
|
306 |
+ nodes = doc.css(css) |
|
307 |
+ when xpath = extraction_details['xpath'] |
|
308 |
+ doc.remove_namespaces! # ignore xmlns, useful when parsing atom feeds |
|
309 |
+ nodes = doc.xpath(xpath) |
|
310 |
+ else |
|
311 |
+ raise '"css" or "xpath" is required for HTML or XML extraction' |
|
312 |
+ end |
|
313 |
+ case nodes |
|
314 |
+ when Nokogiri::XML::NodeSet |
|
315 |
+ result = nodes.map { |node| |
|
316 |
+ case value = node.xpath(extraction_details['value']) |
|
317 |
+ when Float |
|
318 |
+ # Node#xpath() returns any numeric value as float; |
|
319 |
+ # convert it to integer as appropriate. |
|
320 |
+ value = value.to_i if value.to_i == value |
|
321 |
+ end |
|
322 |
+ value.to_s |
|
323 |
+ } |
|
324 |
+ else |
|
325 |
+ raise "The result of HTML/XML extraction was not a NodeSet" |
|
326 |
+ end |
|
327 |
+ log "Extracting #{extraction_type} at #{xpath || css}: #{result}" |
|
328 |
+ result |
|
329 |
+ } |
|
330 |
+ end |
|
331 |
+ |
|
266 | 332 |
def parse(data) |
267 | 333 |
case extraction_type |
268 |
- when "xml" |
|
269 |
- Nokogiri::XML(data) |
|
270 |
- when "json" |
|
271 |
- JSON.parse(data) |
|
272 |
- when "html" |
|
273 |
- Nokogiri::HTML(data) |
|
274 |
- else |
|
275 |
- raise "Unknown extraction type #{extraction_type}" |
|
334 |
+ when "xml" |
|
335 |
+ Nokogiri::XML(data) |
|
336 |
+ when "json" |
|
337 |
+ JSON.parse(data) |
|
338 |
+ when "html" |
|
339 |
+ Nokogiri::HTML(data) |
|
340 |
+ when "text" |
|
341 |
+ data |
|
342 |
+ else |
|
343 |
+ raise "Unknown extraction type #{extraction_type}" |
|
276 | 344 |
end |
277 | 345 |
end |
278 | 346 |
|
279 | 347 |
def is_positive_integer?(value) |
280 |
- begin |
|
281 |
- Integer(value) >= 0 |
|
282 |
- rescue |
|
283 |
- false |
|
284 |
- end |
|
348 |
+ Integer(value) >= 0 |
|
349 |
+ rescue |
|
350 |
+ false |
|
285 | 351 |
end |
286 | 352 |
end |
287 | 353 |
end |
@@ -44,26 +44,21 @@ class Event < ActiveRecord::Base |
||
44 | 44 |
end |
45 | 45 |
|
46 | 46 |
class EventDrop |
47 |
- def initialize(event, payload = event.payload) |
|
48 |
- super(event) |
|
49 |
- @payload = payload |
|
50 |
- end |
|
51 |
- |
|
52 |
- def before_method(key) |
|
53 |
- if @payload.key?(key) |
|
54 |
- @payload[key] |
|
55 |
- else |
|
56 |
- case key |
|
57 |
- when 'agent' |
|
58 |
- @object.agent |
|
59 |
- when 'created_at' |
|
60 |
- @object.created_at |
|
61 |
- end |
|
62 |
- end |
|
47 |
+ def initialize(object, locals = nil) |
|
48 |
+ locals ||= object.payload |
|
49 |
+ super |
|
63 | 50 |
end |
64 | 51 |
|
65 | 52 |
def each(&block) |
66 | 53 |
return to_enum(__method__) unless block |
67 |
- @payload.each(&block) |
|
54 |
+ @locals.each(&block) |
|
55 |
+ end |
|
56 |
+ |
|
57 |
+ def agent |
|
58 |
+ @object.agent |
|
59 |
+ end |
|
60 |
+ |
|
61 |
+ def created_at |
|
62 |
+ @object.created_at |
|
68 | 63 |
end |
69 | 64 |
end |
@@ -1,7 +1,7 @@ |
||
1 | 1 |
class Scenario < ActiveRecord::Base |
2 | 2 |
include HasGuid |
3 | 3 |
|
4 |
- attr_accessible :name, :agent_ids, :description, :public, :source_url |
|
4 |
+ attr_accessible :name, :agent_ids, :description, :public, :source_url, :tag_fg_color, :tag_bg_color |
|
5 | 5 |
|
6 | 6 |
belongs_to :user, :counter_cache => :scenario_count, :inverse_of => :scenarios |
7 | 7 |
has_many :scenario_memberships, :dependent => :destroy, :inverse_of => :scenario |
@@ -9,6 +9,11 @@ class Scenario < ActiveRecord::Base |
||
9 | 9 |
|
10 | 10 |
validates_presence_of :name, :user |
11 | 11 |
|
12 |
+ validates_format_of :tag_fg_color, :tag_bg_color, |
|
13 |
+ # Regex adapted from: http://stackoverflow.com/a/1636354/3130625 |
|
14 |
+ :with => /\A#(?:[0-9a-fA-F]{3}){1,2}\z/, :allow_nil => true, |
|
15 |
+ :message => "must be a valid hex color." |
|
16 |
+ |
|
12 | 17 |
validate :agents_are_owned |
13 | 18 |
|
14 | 19 |
protected |
@@ -60,10 +60,14 @@ class ScenarioImport |
||
60 | 60 |
description = parsed_data['description'] |
61 | 61 |
name = parsed_data['name'] |
62 | 62 |
links = parsed_data['links'] |
63 |
+ tag_fg_color = parsed_data['tag_fg_color'] |
|
64 |
+ tag_bg_color = parsed_data['tag_bg_color'] |
|
63 | 65 |
source_url = parsed_data['source_url'].presence || nil |
64 | 66 |
@scenario = user.scenarios.where(:guid => guid).first_or_initialize |
65 | 67 |
@scenario.update_attributes!(:name => name, :description => description, |
66 |
- :source_url => source_url, :public => false) |
|
68 |
+ :source_url => source_url, :public => false, |
|
69 |
+ :tag_fg_color => tag_fg_color, |
|
70 |
+ :tag_bg_color => tag_bg_color) |
|
67 | 71 |
|
68 | 72 |
unless options[:skip_agents] |
69 | 73 |
created_agents = agent_diffs.map do |agent_diff| |
@@ -1,17 +1,22 @@ |
||
1 | 1 |
class Service < ActiveRecord::Base |
2 |
- attr_accessible :provider, :name, :token, :secret, :refresh_token, :expires_at, :global, :options |
|
2 |
+ PROVIDER_TO_ENV_MAP = {'37signals' => 'THIRTY_SEVEN_SIGNALS'} |
|
3 |
+ |
|
4 |
+ attr_accessible :provider, :name, :token, :secret, :refresh_token, :expires_at, :global, :options, :uid |
|
3 | 5 |
|
4 | 6 |
serialize :options, Hash |
5 | 7 |
|
6 |
- belongs_to :user |
|
7 |
- has_many :agents |
|
8 |
+ belongs_to :user, :inverse_of => :services |
|
9 |
+ has_many :agents, :inverse_of => :service |
|
8 | 10 |
|
9 | 11 |
validates_presence_of :user_id, :provider, :name, :token |
10 | 12 |
|
11 | 13 |
before_destroy :disable_agents |
12 | 14 |
|
13 |
- def disable_agents |
|
14 |
- self.agents.each do |agent| |
|
15 |
+ scope :available_to_user, lambda { |user| where("services.user_id = ? or services.global = true", user.id) } |
|
16 |
+ scope :by_name, lambda { |dir = 'desc'| order("services.name #{dir}") } |
|
17 |
+ |
|
18 |
+ def disable_agents(conditions = {}) |
|
19 |
+ agents.where.not(conditions[:where_not] || {}).each do |agent| |
|
15 | 20 |
agent.service_id = nil |
16 | 21 |
agent.disabled = true |
17 | 22 |
agent.save!(validate: false) |
@@ -19,52 +24,66 @@ class Service < ActiveRecord::Base |
||
19 | 24 |
end |
20 | 25 |
|
21 | 26 |
def toggle_availability! |
27 |
+ disable_agents(where_not: {user_id: self.user_id}) if global |
|
22 | 28 |
self.global = !self.global |
23 | 29 |
self.save! |
24 | 30 |
end |
25 | 31 |
|
26 | 32 |
def prepare_request |
27 |
- if self.expires_at && Time.now > self.expires_at |
|
28 |
- self.refresh_token! |
|
33 |
+ if expires_at && Time.now > expires_at |
|
34 |
+ refresh_token! |
|
29 | 35 |
end |
30 | 36 |
end |
31 | 37 |
|
32 | 38 |
def refresh_token! |
33 | 39 |
response = HTTParty.post(endpoint, query: { |
34 | 40 |
type: 'refresh', |
35 |
- client_id: ENV["#{self.provider.upcase}_OAUTH_KEY"], |
|
36 |
- client_secret: ENV["#{self.provider.upcase}_OAUTH_SECRET"], |
|
37 |
- refresh_token: self.refresh_token |
|
41 |
+ client_id: oauth_key, |
|
42 |
+ client_secret: oauth_secret, |
|
43 |
+ refresh_token: refresh_token |
|
38 | 44 |
}) |
39 | 45 |
data = JSON.parse(response.body) |
40 |
- self.update(expires_at: Time.now + data['expires_in'], token: data['access_token'], refresh_token: data['refresh_token'].presence || self.refresh_token) |
|
46 |
+ update(expires_at: Time.now + data['expires_in'], token: data['access_token'], refresh_token: data['refresh_token'].presence || refresh_token) |
|
41 | 47 |
end |
42 | 48 |
|
43 |
- def self.initialize_or_update_via_omniauth(omniauth) |
|
49 |
+ def endpoint |
|
50 |
+ client_options = "OmniAuth::Strategies::#{OmniAuth::Utils.camelize(self.provider)}".constantize.default_options['client_options'] |
|
51 |
+ URI.join(client_options['site'], client_options['token_url']) |
|
52 |
+ end |
|
53 |
+ |
|
54 |
+ def provider_to_env |
|
55 |
+ PROVIDER_TO_ENV_MAP[provider].presence || provider.upcase |
|
56 |
+ end |
|
57 |
+ |
|
58 |
+ def oauth_key |
|
59 |
+ ENV["#{provider_to_env}_OAUTH_KEY"] |
|
60 |
+ end |
|
61 |
+ |
|
62 |
+ def oauth_secret |
|
63 |
+ ENV["#{provider_to_env}_OAUTH_SECRET"] |
|
64 |
+ end |
|
65 |
+ |
|
66 |
+ def self.provider_specific_options(omniauth) |
|
44 | 67 |
case omniauth['provider'] |
45 |
- when 'twitter' |
|
46 |
- find_or_initialize_by(provider: omniauth['provider'], name: omniauth['info']['nickname']).tap do |service| |
|
47 |
- service.assign_attributes(token: omniauth['credentials']['token'], secret: omniauth['credentials']['secret']) |
|
48 |
- end |
|
49 |
- when 'github' |
|
50 |
- find_or_initialize_by(provider: omniauth['provider'], name: omniauth['info']['nickname']).tap do |service| |
|
51 |
- service.assign_attributes(token: omniauth['credentials']['token']) |
|
52 |
- end |
|
53 |
- when '37signals' |
|
54 |
- find_or_initialize_by(provider: omniauth['provider'], name: omniauth['info']['name']).tap do |service| |
|
55 |
- service.assign_attributes(token: omniauth['credentials']['token'], |
|
56 |
- refresh_token: omniauth['credentials']['refresh_token'], |
|
57 |
- expires_at: Time.at(omniauth['credentials']['expires_at']), |
|
58 |
- options: {user_id: omniauth['extra']['accounts'][0]['id']}) |
|
59 |
- end |
|
60 |
- else |
|
61 |
- false |
|
68 |
+ when 'twitter', 'github' |
|
69 |
+ { name: omniauth['info']['nickname'] } |
|
70 |
+ when '37signals' |
|
71 |
+ { user_id: omniauth['extra']['accounts'][0]['id'], name: omniauth['info']['name'] } |
|
72 |
+ else |
|
73 |
+ { name: omniauth['info']['nickname'] } |
|
62 | 74 |
end |
63 | 75 |
end |
64 | 76 |
|
65 |
- private |
|
66 |
- def endpoint |
|
67 |
- client_options = "OmniAuth::Strategies::#{OmniAuth::Utils.camelize(self.provider)}".constantize.default_options['client_options'] |
|
68 |
- URI.join(client_options['site'], client_options['token_url']) |
|
77 |
+ def self.initialize_or_update_via_omniauth(omniauth) |
|
78 |
+ options = provider_specific_options(omniauth) |
|
79 |
+ |
|
80 |
+ find_or_initialize_by(provider: omniauth['provider'], uid: omniauth['uid'].to_s).tap do |service| |
|
81 |
+ service.assign_attributes token: omniauth['credentials']['token'], |
|
82 |
+ secret: omniauth['credentials']['secret'], |
|
83 |
+ name: options[:name], |
|
84 |
+ refresh_token: omniauth['credentials']['refresh_token'], |
|
85 |
+ expires_at: omniauth['credentials']['expires_at'] && Time.at(omniauth['credentials']['expires_at']), |
|
86 |
+ options: options |
|
87 |
+ end |
|
69 | 88 |
end |
70 | 89 |
end |
@@ -27,10 +27,10 @@ class User < ActiveRecord::Base |
||
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 | 29 |
has_many :scenarios, :inverse_of => :user, :dependent => :destroy |
30 |
- has_many :services, -> { order("services.name")}, :dependent => :destroy |
|
30 |
+ has_many :services, -> { by_name('asc') }, :dependent => :destroy |
|
31 | 31 |
|
32 | 32 |
def available_services |
33 |
- Service.where("user_id = ? or global = true", self.id).order("services.name desc") |
|
33 |
+ Service.available_to_user(self).by_name |
|
34 | 34 |
end |
35 | 35 |
|
36 | 36 |
# Allow users to login via either email or username. |
@@ -32,7 +32,7 @@ |
||
32 | 32 |
|
33 | 33 |
<% agent.scenarios.each do |scenario| %> |
34 | 34 |
<li> |
35 |
- <%= link_to "<span class='color-warning glyphicon glyphicon-remove-circle'></span> Remove from <span class='scenario label label-info'>#{h scenario.name}</span>".html_safe, leave_scenario_agent_path(agent, :scenario_id => scenario.to_param, :return => returnTo), method: :put, :tabindex => "-1" %> |
|
35 |
+ <%= link_to "<span class='color-warning glyphicon glyphicon-remove-circle'></span> Remove from #{scenario_label(scenario)}".html_safe, leave_scenario_agent_path(agent, :scenario_id => scenario.to_param, :return => returnTo), method: :put, :tabindex => "-1" %> |
|
36 | 36 |
</li> |
37 | 37 |
<% end %> |
38 | 38 |
<% end %> |
@@ -31,12 +31,7 @@ |
||
31 | 31 |
</div> |
32 | 32 |
|
33 | 33 |
<div class='oauthable-form'> |
34 |
- <% if @agent.try(:oauthable?) %> |
|
35 |
- <div class="form-group type-select"> |
|
36 |
- <%= f.label :service %> |
|
37 |
- <%= f.select :service_id, options_for_select(@agent.valid_services(current_user).collect { |s| ["(#{s.provider}) #{s.name}", s.id]}, @agent.service_id),{}, class: 'form-control' %> |
|
38 |
- </div> |
|
39 |
- <% end %> |
|
34 |
+ <%= render partial: 'oauth_dropdown' %> |
|
40 | 35 |
</div> |
41 | 36 |
|
42 | 37 |
<div class="form-group"> |
@@ -0,0 +1,6 @@ |
||
1 |
+<% if @agent.try(:oauthable?) %> |
|
2 |
+ <div class="form-group type-select"> |
|
3 |
+ <%= label_tag :service %> |
|
4 |
+ <%= select_tag 'agent[service_id]', options_for_select(@agent.valid_services_for(current_user).collect { |s| ["(#{s.provider}) #{s.name}", s.id]}, @agent.service_id), class: 'form-control' %> |
|
5 |
+ </div> |
|
6 |
+<% end %> |
@@ -110,8 +110,8 @@ |
||
110 | 110 |
<% if @agent.can_receive_events? %> |
111 | 111 |
<p> |
112 | 112 |
<b>Event sources:</b> |
113 |
- <% if @agent.sources.length %> |
|
114 |
- <%= @agent.sources.map { |source_agent| link_to(source_agent.name, agent_path(source_agent)) }.to_sentence.html_safe %> |
|
113 |
+ <% if (agents = @agent.sources).length > 0 %> |
|
114 |
+ <%= agents.map { |agent| link_to(agent.name, agent_path(agent)) }.to_sentence.html_safe %> |
|
115 | 115 |
<% else %> |
116 | 116 |
None |
117 | 117 |
<% end %> |
@@ -126,8 +126,8 @@ |
||
126 | 126 |
<% if @agent.can_create_events? %> |
127 | 127 |
<p> |
128 | 128 |
<b>Event receivers:</b> |
129 |
- <% if @agent.receivers.length %> |
|
130 |
- <%= @agent.receivers.map { |receiver_agent| link_to(receiver_agent.name, agent_path(receiver_agent)) }.to_sentence.html_safe %> |
|
129 |
+ <% if (agents = @agent.receivers).length > 0 %> |
|
130 |
+ <%= agents.map { |agent| link_to(agent.name, agent_path(agent)) }.to_sentence.html_safe %> |
|
131 | 131 |
<% else %> |
132 | 132 |
None |
133 | 133 |
<% end %> |
@@ -0,0 +1,26 @@ |
||
1 |
+<% if @twitter_agent || @basecamp_agent %> |
|
2 |
+ <div class="alert alert-danger" role="alert"> |
|
3 |
+ <p> |
|
4 |
+ <b>Warning!</b> You need to update your Huginn configuration, so your agents continue to work with the new OAuth services. |
|
5 |
+ </p> |
|
6 |
+ <br/> |
|
7 |
+ <% if @twitter_agent %> |
|
8 |
+ <p> |
|
9 |
+ To complete the migration of your <b>Twitter</b> agents you need to update your .env file and add the following two lines: |
|
10 |
+ |
|
11 |
+ <pre> |
|
12 |
+TWITTER_OAUTH_KEY=<%= @twitter_oauth_key %> |
|
13 |
+TWITTER_OAUTH_SECRET=<%= @twitter_oauth_secret %> |
|
14 |
+ </pre> |
|
15 |
+ To authenticate new accounts with your twitter OAuth application you need to log in the to <a href="https://apps.twitter.com/" target="_blank">twitter application management page</a> and set the callback URL of your application to "http<%= ENV['FORCE_SSL'] == 'true' ? 's' : '' %>://<%= ENV['DOMAIN'] %>/auth/twitter/callback". |
|
16 |
+ |
|
17 |
+ </p> |
|
18 |
+ <% end %> |
|
19 |
+ <% if @basecamp_agent %> |
|
20 |
+ <p> |
|
21 |
+ Your <b>Basecamp</b> agents could not be migrated automatically. You need to manually register an application with 37signals and authenticate Huginn to use it.<br/> |
|
22 |
+ Have a look at the <%= link_to 'Wiki', 'https://github.com/cantino/huginn/wiki/Configuring-OAuth-applications', target: '_blank' %> if you need help. |
|
23 |
+ </p> |
|
24 |
+ <% end %> |
|
25 |
+ </div> |
|
26 |
+<% end -%> |
@@ -24,7 +24,10 @@ |
||
24 | 24 |
<%= render 'layouts/messages' %> |
25 | 25 |
</div> |
26 | 26 |
</div> |
27 |
- |
|
27 |
+ <% if user_signed_in? %> |
|
28 |
+ <%= render "upgrade_warning" %> |
|
29 |
+ <% end %> |
|
30 |
+ |
|
28 | 31 |
<%= yield %> |
29 | 32 |
|
30 | 33 |
</div> |
@@ -13,9 +13,8 @@ |
||
13 | 13 |
<div class="alert alert-warning"> |
14 | 14 |
<span class='glyphicon glyphicon-warning-sign'></span> |
15 | 15 |
This Scenario already exists in your system. The import will update your existing |
16 |
- <span class='label label-info scenario'><%= @scenario_import.existing_scenario.name %></span> Scenario's title |
|
17 |
- and |
|
18 |
- description. Below you can customize how the individual agents get updated. |
|
16 |
+ <%= scenario_label(@scenario_import.existing_scenario) %> Scenario's title, |
|
17 |
+ description and tag colors. Below you can customize how the individual agents get updated. |
|
19 | 18 |
</div> |
20 | 19 |
<% end %> |
21 | 20 |
|
@@ -30,7 +29,7 @@ |
||
30 | 29 |
</div> |
31 | 30 |
|
32 | 31 |
<% if @scenario_import.parsed_data["description"].present? %> |
33 |
- <blockquote><%= @scenario_import.parsed_data["description"] %></blockquote> |
|
32 |
+ <blockquote><%= markdown(@scenario_import.parsed_data["description"]) %></blockquote> |
|
34 | 33 |
<% end %> |
35 | 34 |
|
36 | 35 |
</div> |
@@ -120,12 +119,13 @@ |
||
120 | 119 |
</div> |
121 | 120 |
<% end %> |
122 | 121 |
</div> |
122 |
+ |
|
123 | 123 |
<% if agent_diff.requires_service? %> |
124 | 124 |
<div class='row'> |
125 | 125 |
<div class='col-md-4'> |
126 | 126 |
<div class="form-group type-select"> |
127 | 127 |
<%= label_tag "scenario_import[merges][#{index}][service_id]", 'Service' %> |
128 |
- <%= select_tag "scenario_import[merges][#{index}][service_id]", options_for_select(agent_diff.agent_instance.valid_services(current_user).collect { |s| ["(#{s.provider}) #{s.name}", s.id]}, agent_diff.service_id.try(:current)), class: 'form-control' %> |
|
128 |
+ <%= select_tag "scenario_import[merges][#{index}][service_id]", options_for_select(agent_diff.agent_instance.valid_services_for(current_user).collect { |s| ["(#{s.provider}) #{s.name}", s.id]}, agent_diff.agent.try(:service_id)), class: 'form-control' %> |
|
129 | 129 |
</div> |
130 | 130 |
</div> |
131 | 131 |
</div> |
@@ -15,6 +15,18 @@ |
||
15 | 15 |
<%= f.text_field :name, :class => 'form-control', :placeholder => "Name your Scenario" %> |
16 | 16 |
</div> |
17 | 17 |
</div> |
18 |
+ <div class="col-md-2"> |
|
19 |
+ <div class="form-group"> |
|
20 |
+ <%= f.label :tag_bg_color, "Tag Background Color" %> |
|
21 |
+ <%= f.color_field :tag_bg_color, :class => 'form-control', :value => @scenario.tag_bg_color || default_scenario_bg_color %> |
|
22 |
+ </div> |
|
23 |
+ </div> |
|
24 |
+ <div class="col-md-2"> |
|
25 |
+ <div class="form-group"> |
|
26 |
+ <%= f.label :tag_fg_color, "Tag Foreground Color" %> |
|
27 |
+ <%= f.color_field :tag_fg_color, :class => 'form-control', :value => @scenario.tag_fg_color || default_scenario_fg_color %> |
|
28 |
+ </div> |
|
29 |
+ </div> |
|
18 | 30 |
</div> |
19 | 31 |
|
20 | 32 |
<div class="row"> |
@@ -54,4 +66,4 @@ |
||
54 | 66 |
</div> |
55 | 67 |
</div> |
56 | 68 |
</div> |
57 |
-<% end %> |
|
69 |
+<% end %> |
@@ -21,6 +21,7 @@ |
||
21 | 21 |
<% @scenarios.each do |scenario| %> |
22 | 22 |
<tr> |
23 | 23 |
<td> |
24 |
+ <%= scenario_label(scenario, content_tag(:i, '', class: 'glyphicon glyphicon-font')) %> |
|
24 | 25 |
<%= link_to(scenario.name, scenario) %> |
25 | 26 |
</td> |
26 | 27 |
<td><%= link_to pluralize(scenario.agents.count, "agent"), scenario %></td> |
@@ -47,4 +48,4 @@ |
||
47 | 48 |
</div> |
48 | 49 |
</div> |
49 | 50 |
</div> |
50 |
-</div> |
|
51 |
+</div> |
@@ -2,7 +2,7 @@ |
||
2 | 2 |
<div class='row'> |
3 | 3 |
<div class='col-md-12'> |
4 | 4 |
<div class="page-header"> |
5 |
- <h2>Share <span class='label label-info scenario'><%= @scenario.name %></span> with the world</h2> |
|
5 |
+ <h2>Share <%= scenario_label(@scenario) %> with the world</h2> |
|
6 | 6 |
</div> |
7 | 7 |
|
8 | 8 |
<p> |
@@ -30,4 +30,4 @@ |
||
30 | 30 |
</div> |
31 | 31 |
</div> |
32 | 32 |
</div> |
33 |
-</div> |
|
33 |
+</div> |
@@ -2,11 +2,12 @@ |
||
2 | 2 |
<div class='row'> |
3 | 3 |
<div class='col-md-12'> |
4 | 4 |
<div class="page-header"> |
5 |
- <h2><span class='label label-info scenario'><%= @scenario.name %></span> <%= "Public" if @scenario.public? %> Scenario</h2> |
|
5 |
+ <h2><%= scenario_label(@scenario) %> <%= "Public" if @scenario.public? %> Scenario</h2> |
|
6 |
+ |
|
6 | 7 |
</div> |
7 | 8 |
|
8 | 9 |
<% if @scenario.description.present? %> |
9 |
- <blockquote><%= @scenario.description %></blockquote> |
|
10 |
+ <blockquote><%= markdown(@scenario.description) %></blockquote> |
|
10 | 11 |
<% end %> |
11 | 12 |
|
12 | 13 |
<%= render 'agents/table', :returnTo => scenario_path(@scenario) %> |
@@ -7,13 +7,19 @@ |
||
7 | 7 |
</h2> |
8 | 8 |
</div> |
9 | 9 |
<p> |
10 |
- Before you can authenticate with a service, you need to set it up. Have a look at the |
|
10 |
+ Before you can authenticate with a service, you need to set it up. Have a look at the Huginn |
|
11 | 11 |
<%= link_to 'wiki', 'https://github.com/cantino/huginn/wiki/Configuring-OAuth-applications', target: :_blank %> |
12 | 12 |
for guidance. |
13 | 13 |
</p> |
14 |
- <p><%= link_to "Authenticate with Twitter", "/auth/twitter" %></p> |
|
15 |
- <p><%= link_to "Authenticate with 37Signals (Basecamp)", "/auth/37signals" %></p> |
|
16 |
- <p><%= link_to "Authenticate with Github", "/auth/github" %></p> |
|
14 |
+ <% if has_oauth_configuration_for('twitter') %> |
|
15 |
+ <p><%= link_to "Authenticate with Twitter", "/auth/twitter" %></p> |
|
16 |
+ <% end %> |
|
17 |
+ <% if has_oauth_configuration_for('thirty_seven_signals') %> |
|
18 |
+ <p><%= link_to "Authenticate with 37Signals (Basecamp)", "/auth/37signals" %></p> |
|
19 |
+ <% end -%> |
|
20 |
+ <% if has_oauth_configuration_for('github') %> |
|
21 |
+ <p><%= link_to "Authenticate with Github", "/auth/github" %></p> |
|
22 |
+ <% end -%> |
|
17 | 23 |
<hr> |
18 | 24 |
|
19 | 25 |
<div class='table-responsive'> |
@@ -33,9 +39,9 @@ |
||
33 | 39 |
<td> |
34 | 40 |
<div class="btn-group btn-group-xs"> |
35 | 41 |
<% if service.global %> |
36 |
- <%= link_to 'Make private', toggle_availability_service_path(service), method: :post, data: { confirm: 'Are you sure you want to remove the access to this service for every user?'}, class: "btn btn-default" %> |
|
42 |
+ <%= link_to 'Make private', toggle_availability_service_path(service), method: :post, data: { confirm: 'Are you sure you want to remove access to your data on this service for other users?'}, class: "btn btn-default" %> |
|
37 | 43 |
<% else %> |
38 |
- <%= link_to 'Make global', toggle_availability_service_path(service), method: :post, data: { confirm: 'Are you sure you want to grant every user access to this service?'}, class: "btn btn-default" %> |
|
44 |
+ <%= link_to 'Make global', toggle_availability_service_path(service), method: :post, data: { confirm: 'Are you sure you want to grant every user on this system access to your data on this service?'}, class: "btn btn-default" %> |
|
39 | 45 |
<% end %> |
40 | 46 |
<%= link_to 'Delete', service_path(service), method: :delete, data: { confirm: 'Are you sure?' }, class: "btn btn-default btn-danger" %> |
41 | 47 |
</div> |
@@ -5,14 +5,14 @@ |
||
5 | 5 |
</head> |
6 | 6 |
<body> |
7 | 7 |
<% if @headline %> |
8 |
- <h1><%= @headline %></h1> |
|
8 |
+ <h1><%= sanitize @headline %></h1> |
|
9 | 9 |
<% end %> |
10 | 10 |
<% @groups.each do |group| %> |
11 | 11 |
<div style='margin-bottom: 10px;'> |
12 |
- <div><%= group[:title] %></div> |
|
12 |
+ <div><%= sanitize group[:title] %></div> |
|
13 | 13 |
<% group[:entries].each do |entry| %> |
14 | 14 |
<div style='margin-left: 10px;'> |
15 |
- <%= entry %> |
|
15 |
+ <%= sanitize entry %> |
|
16 | 16 |
</div> |
17 | 17 |
<% end %> |
18 | 18 |
</div> |
@@ -26,6 +26,7 @@ class MigrateAgentsToServiceAuthentication < ActiveRecord::Migration |
||
26 | 26 |
agent.service_id = service.id |
27 | 27 |
agent.save!(validate: false) |
28 | 28 |
end |
29 |
+ migrated = false |
|
29 | 30 |
if agents.length > 0 |
30 | 31 |
puts <<-EOF.strip_heredoc |
31 | 32 |
|
@@ -34,18 +35,23 @@ class MigrateAgentsToServiceAuthentication < ActiveRecord::Migration |
||
34 | 35 |
TWITTER_OAUTH_KEY=#{twitter_consumer_key(agents.first)} |
35 | 36 |
TWITTER_OAUTH_SECRET=#{twitter_consumer_secret(agents.first)} |
36 | 37 |
|
38 |
+ To authenticate new accounts with your twitter OAuth application you need to log in the to twitter application management page (https://apps.twitter.com/) |
|
39 |
+ and set the callback URL of your application to "http#{ENV['FORCE_SSL'] == 'true' ? 's' : ''}://#{ENV['DOMAIN']}/auth/twitter/callback" |
|
37 | 40 |
|
38 | 41 |
EOF |
42 |
+ migrated = true |
|
39 | 43 |
end |
40 | 44 |
if Agent.where(type: ['Agents::BasecampAgent']).count > 0 |
41 | 45 |
puts <<-EOF.strip_heredoc |
42 | 46 |
|
43 |
- Your Basecamp agents can not be migrated automatically. You need to manually register an application with 37signals and authenticate huginn to use it. |
|
47 |
+ Your Basecamp agents can not be migrated automatically. You need to manually register an application with 37signals and authenticate Huginn to use it. |
|
44 | 48 |
Have a look at the wiki (https://github.com/cantino/huginn/wiki/Configuring-OAuth-applications) if you need help. |
45 | 49 |
|
46 | 50 |
|
47 | 51 |
EOF |
52 |
+ migrated = true |
|
48 | 53 |
end |
54 |
+ sleep 20 if migrated |
|
49 | 55 |
end |
50 | 56 |
|
51 | 57 |
def down |
@@ -0,0 +1,5 @@ |
||
1 |
+class RemoveServiceIndexOnUserId < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ remove_index :services, :user_id |
|
4 |
+ end |
|
5 |
+end |
@@ -0,0 +1,7 @@ |
||
1 |
+class AddUidColumnToServices < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ add_column :services, :uid, :string |
|
4 |
+ add_index :services, :uid |
|
5 |
+ add_index :services, :provider |
|
6 |
+ end |
|
7 |
+end |
@@ -0,0 +1,6 @@ |
||
1 |
+class AddTagColorToScenarios < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ add_column :scenarios, :tag_bg_color, :string |
|
4 |
+ add_column :scenarios, :tag_fg_color, :string |
|
5 |
+ end |
|
6 |
+end |
@@ -11,7 +11,7 @@ |
||
11 | 11 |
# |
12 | 12 |
# It's strongly recommended that you check this file into your version control system. |
13 | 13 |
|
14 |
-ActiveRecord::Schema.define(version: 20140813110107) do |
|
14 |
+ActiveRecord::Schema.define(version: 20140820003139) do |
|
15 | 15 |
|
16 | 16 |
# These are extensions that must be enabled in order to support this database |
17 | 17 |
enable_extension "plpgsql" |
@@ -22,8 +22,8 @@ ActiveRecord::Schema.define(version: 20140813110107) do |
||
22 | 22 |
t.integer "level", default: 3, null: false |
23 | 23 |
t.integer "inbound_event_id" |
24 | 24 |
t.integer "outbound_event_id" |
25 |
- t.datetime "created_at", null: false |
|
26 |
- t.datetime "updated_at", null: false |
|
25 |
+ t.datetime "created_at" |
|
26 |
+ t.datetime "updated_at" |
|
27 | 27 |
end |
28 | 28 |
|
29 | 29 |
create_table "agents", force: true do |t| |
@@ -45,8 +45,8 @@ ActiveRecord::Schema.define(version: 20140813110107) do |
||
45 | 45 |
t.datetime "last_error_log_at" |
46 | 46 |
t.boolean "propagate_immediately", default: false, null: false |
47 | 47 |
t.boolean "disabled", default: false, null: false |
48 |
- t.integer "service_id" |
|
49 | 48 |
t.string "guid", null: false, charset: "ascii", collation: "ascii_bin" |
49 |
+ t.integer "service_id" |
|
50 | 50 |
end |
51 | 51 |
|
52 | 52 |
add_index "agents", ["guid"], name: "index_agents_on_guid", using: :btree |
@@ -64,8 +64,8 @@ ActiveRecord::Schema.define(version: 20140813110107) do |
||
64 | 64 |
t.datetime "failed_at" |
65 | 65 |
t.string "locked_by" |
66 | 66 |
t.string "queue" |
67 |
- t.datetime "created_at", null: false |
|
68 |
- t.datetime "updated_at", null: false |
|
67 |
+ t.datetime "created_at" |
|
68 |
+ t.datetime "updated_at" |
|
69 | 69 |
end |
70 | 70 |
|
71 | 71 |
add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree |
@@ -88,8 +88,8 @@ ActiveRecord::Schema.define(version: 20140813110107) do |
||
88 | 88 |
create_table "links", force: true do |t| |
89 | 89 |
t.integer "source_id" |
90 | 90 |
t.integer "receiver_id" |
91 |
- t.datetime "created_at", null: false |
|
92 |
- t.datetime "updated_at", null: false |
|
91 |
+ t.datetime "created_at" |
|
92 |
+ t.datetime "updated_at" |
|
93 | 93 |
t.integer "event_id_at_creation", default: 0, null: false |
94 | 94 |
end |
95 | 95 |
|
@@ -115,15 +115,17 @@ ActiveRecord::Schema.define(version: 20140813110107) do |
||
115 | 115 |
t.boolean "public", default: false, null: false |
116 | 116 |
t.string "guid", null: false, charset: "ascii", collation: "ascii_bin" |
117 | 117 |
t.string "source_url" |
118 |
+ t.string "tag_bg_color" |
|
119 |
+ t.string "tag_fg_color" |
|
118 | 120 |
end |
119 | 121 |
|
120 | 122 |
add_index "scenarios", ["user_id", "guid"], name: "index_scenarios_on_user_id_and_guid", unique: true, using: :btree |
121 | 123 |
|
122 | 124 |
create_table "services", force: true do |t| |
123 |
- t.integer "user_id" |
|
124 |
- t.string "provider" |
|
125 |
- t.string "name" |
|
126 |
- t.text "token" |
|
125 |
+ t.integer "user_id", null: false |
|
126 |
+ t.string "provider", null: false |
|
127 |
+ t.string "name", null: false |
|
128 |
+ t.text "token", null: false |
|
127 | 129 |
t.text "secret" |
128 | 130 |
t.text "refresh_token" |
129 | 131 |
t.datetime "expires_at" |
@@ -131,10 +133,12 @@ ActiveRecord::Schema.define(version: 20140813110107) do |
||
131 | 133 |
t.text "options" |
132 | 134 |
t.datetime "created_at" |
133 | 135 |
t.datetime "updated_at" |
136 |
+ t.string "uid" |
|
134 | 137 |
end |
135 | 138 |
|
136 |
- add_index "services", ["user_id", "global"], name: "index_accounts_on_user_id_and_global", using: :btree |
|
137 |
- add_index "services", ["user_id"], name: "index_accounts_on_user_id", using: :btree |
|
139 |
+ add_index "services", ["provider"], name: "index_services_on_provider", using: :btree |
|
140 |
+ add_index "services", ["uid"], name: "index_services_on_uid", using: :btree |
|
141 |
+ add_index "services", ["user_id", "global"], name: "index_services_on_user_id_and_global", using: :btree |
|
138 | 142 |
|
139 | 143 |
create_table "user_credentials", force: true do |t| |
140 | 144 |
t.integer "user_id", null: false |
@@ -16,6 +16,8 @@ class AgentsExporter |
||
16 | 16 |
:description => options[:description].presence || 'No description provided', |
17 | 17 |
:source_url => options[:source_url], |
18 | 18 |
:guid => options[:guid], |
19 |
+ :tag_fg_color => options[:tag_fg_color], |
|
20 |
+ :tag_bg_color => options[:tag_bg_color], |
|
19 | 21 |
:exported_at => Time.now.utc.iso8601, |
20 | 22 |
:agents => agents.map { |agent| agent_as_json(agent) }, |
21 | 23 |
:links => links |
@@ -51,4 +53,4 @@ class AgentsExporter |
||
51 | 53 |
options[:propagate_immediately] = agent.propagate_immediately if agent.can_receive_events? |
52 | 54 |
end |
53 | 55 |
end |
54 |
-end |
|
56 |
+end |
@@ -0,0 +1,15 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe LiquidInterpolatable::Filters do |
|
4 |
+ before do |
|
5 |
+ @filter = Class.new do |
|
6 |
+ include LiquidInterpolatable::Filters |
|
7 |
+ end.new |
|
8 |
+ end |
|
9 |
+ |
|
10 |
+ describe 'uri_escape' do |
|
11 |
+ it 'should escape a string for use in URI' do |
|
12 |
+ @filter.uri_escape('abc:/?=').should == 'abc%3A%2F%3F%3D' |
|
13 |
+ end |
|
14 |
+ end |
|
15 |
+end |
@@ -50,6 +50,8 @@ describe ScenariosController do |
||
50 | 50 |
assigns(:exporter).options[:description].should == scenarios(:bob_weather).description |
51 | 51 |
assigns(:exporter).options[:agents].should == scenarios(:bob_weather).agents |
52 | 52 |
assigns(:exporter).options[:guid].should == scenarios(:bob_weather).guid |
53 |
+ assigns(:exporter).options[:tag_fg_color].should == scenarios(:bob_weather).tag_fg_color |
|
54 |
+ assigns(:exporter).options[:tag_bg_color].should == scenarios(:bob_weather).tag_bg_color |
|
53 | 55 |
assigns(:exporter).options[:source_url].should be_falsey |
54 | 56 |
response.headers['Content-Disposition'].should == 'attachment; filename="bob-s-weather-alert-scenario.json"' |
55 | 57 |
response.headers['Content-Type'].should == 'application/json; charset=utf-8' |
@@ -10,7 +10,7 @@ describe ServicesController do |
||
10 | 10 |
describe "GET index" do |
11 | 11 |
it "only returns sevices of the current user" do |
12 | 12 |
get :index |
13 |
- assigns(:services).all? {|i| i.user.should == users(:bob) }.should be_true |
|
13 |
+ assigns(:services).all? {|i| i.user.should == users(:bob) }.should == true |
|
14 | 14 |
end |
15 | 15 |
end |
16 | 16 |
|
@@ -41,17 +41,18 @@ describe ServicesController do |
||
41 | 41 |
end |
42 | 42 |
|
43 | 43 |
describe "accepting a callback url" do |
44 |
- it "should update the users credentials" do |
|
44 |
+ it "should update the user's credentials" do |
|
45 | 45 |
expect { |
46 | 46 |
get :callback, provider: 'twitter' |
47 | 47 |
}.to change { users(:bob).services.count }.by(1) |
48 | 48 |
end |
49 | 49 |
|
50 |
- it "should not work with an unknown provider" do |
|
50 |
+ it "should work with an unknown provider (for now)" do |
|
51 | 51 |
request.env["omniauth.auth"]['provider'] = 'unknown' |
52 | 52 |
expect { |
53 | 53 |
get :callback, provider: 'unknown' |
54 |
- }.to change { users(:bob).services.count }.by(0) |
|
54 |
+ }.to change { users(:bob).services.count }.by(1) |
|
55 |
+ users(:bob).services.first.provider.should == 'unknown' |
|
55 | 56 |
end |
56 | 57 |
end |
57 | 58 |
end |
@@ -115,3 +115,9 @@ bob_basecamp_agent: |
||
115 | 115 |
user: bob |
116 | 116 |
service: generic |
117 | 117 |
guid: <%= SecureRandom.hex %> |
118 |
+ |
|
119 |
+jane_basecamp_agent: |
|
120 |
+ type: Agents::BasecampAgent |
|
121 |
+ user: jane |
|
122 |
+ service: generic |
|
123 |
+ guid: <%= SecureRandom.hex %> |
@@ -0,0 +1,14 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe MarkdownHelper do |
|
4 |
+ |
|
5 |
+ describe '#markdown' do |
|
6 |
+ |
|
7 |
+ it 'renders HTML from a markdown text' do |
|
8 |
+ markdown('# Header').should =~ /<h1>Header<\/h1>/ |
|
9 |
+ markdown('## Header 2').should =~ /<h2>Header 2<\/h2>/ |
|
10 |
+ end |
|
11 |
+ |
|
12 |
+ end |
|
13 |
+ |
|
14 |
+end |
@@ -0,0 +1,30 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe ScenarioHelper do |
|
4 |
+ let(:scenario) { users(:bob).scenarios.build(name: 'Scene', tag_fg_color: '#AAAAAA', tag_bg_color: '#000000') } |
|
5 |
+ |
|
6 |
+ describe '#style_colors' do |
|
7 |
+ it 'returns a css style-formated version of the scenario foreground and background colors' do |
|
8 |
+ style_colors(scenario).should == "color:#AAAAAA;background-color:#000000" |
|
9 |
+ end |
|
10 |
+ |
|
11 |
+ it 'defauls foreground and background colors' do |
|
12 |
+ scenario.tag_fg_color = nil |
|
13 |
+ scenario.tag_bg_color = nil |
|
14 |
+ style_colors(scenario).should == "color:#FFFFFF;background-color:#5BC0DE" |
|
15 |
+ end |
|
16 |
+ end |
|
17 |
+ |
|
18 |
+ describe '#scenario_label' do |
|
19 |
+ it 'creates a scenario label with the scenario name' do |
|
20 |
+ scenario_label(scenario).should == |
|
21 |
+ '<span class="label scenario" style="color:#AAAAAA;background-color:#000000">Scene</span>' |
|
22 |
+ end |
|
23 |
+ |
|
24 |
+ it 'creates a scenario label with the given text' do |
|
25 |
+ scenario_label(scenario, 'Other').should == |
|
26 |
+ '<span class="label scenario" style="color:#AAAAAA;background-color:#000000">Other</span>' |
|
27 |
+ end |
|
28 |
+ end |
|
29 |
+ |
|
30 |
+end |
@@ -7,9 +7,13 @@ describe AgentsExporter do |
||
7 | 7 |
let(:name) { "My set of Agents" } |
8 | 8 |
let(:description) { "These Agents work together nicely!" } |
9 | 9 |
let(:guid) { "some-guid" } |
10 |
+ let(:tag_fg_color) { "#ffffff" } |
|
11 |
+ let(:tag_bg_color) { "#000000" } |
|
10 | 12 |
let(:source_url) { "http://yourhuginn.com/scenarios/2/export.json" } |
11 | 13 |
let(:agent_list) { [agents(:jane_weather_agent), agents(:jane_rain_notifier_agent)] } |
12 |
- let(:exporter) { AgentsExporter.new(:agents => agent_list, :name => name, :description => description, :source_url => source_url, :guid => guid) } |
|
14 |
+ let(:exporter) { AgentsExporter.new( |
|
15 |
+ :agents => agent_list, :name => name, :description => description, :source_url => source_url, |
|
16 |
+ :guid => guid, :tag_fg_color => tag_fg_color, :tag_bg_color => tag_bg_color) } |
|
13 | 17 |
|
14 | 18 |
it "outputs a structure containing name, description, the date, all agents & their links" do |
15 | 19 |
data = exporter.as_json |
@@ -17,6 +21,8 @@ describe AgentsExporter do |
||
17 | 21 |
data[:description].should == description |
18 | 22 |
data[:source_url].should == source_url |
19 | 23 |
data[:guid].should == guid |
24 |
+ data[:tag_fg_color].should == tag_fg_color |
|
25 |
+ data[:tag_bg_color].should == tag_bg_color |
|
20 | 26 |
Time.parse(data[:exported_at]).should be_within(2).of(Time.now.utc) |
21 | 27 |
data[:links].should == [{ :source => 0, :receiver => 1 }] |
22 | 28 |
data[:agents].should == agent_list.map { |agent| exporter.agent_as_json(agent) } |
@@ -58,4 +64,4 @@ describe AgentsExporter do |
||
58 | 64 |
AgentsExporter.new(:name => ",,").filename.should == "exported-agents.json" |
59 | 65 |
end |
60 | 66 |
end |
61 |
-end |
|
67 |
+end |
@@ -6,7 +6,7 @@ describe Agents::BasecampAgent do |
||
6 | 6 |
|
7 | 7 |
before(:each) do |
8 | 8 |
stub_request(:get, /json$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"}) |
9 |
- stub_request(:get, /Z$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"}) |
|
9 |
+ stub_request(:get, /02:00$/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/basecamp.json")), :status => 200, :headers => {"Content-Type" => "text/json"}) |
|
10 | 10 |
@valid_params = { :project_id => 6789 } |
11 | 11 |
|
12 | 12 |
@checker = Agents::BasecampAgent.new(:name => "somename", :options => @valid_params) |
@@ -43,7 +43,7 @@ describe Agents::BasecampAgent do |
||
43 | 43 |
|
44 | 44 |
it "should provide the since attribute after the first run" do |
45 | 45 |
time = (Time.now-1.minute).iso8601 |
46 |
- @checker.memory[:last_run] = time |
|
46 |
+ @checker.memory[:last_event] = time |
|
47 | 47 |
@checker.save |
48 | 48 |
@checker.reload.send(:query_parameters).should == {:query => {:since => time}} |
49 | 49 |
end |
@@ -51,9 +51,10 @@ describe Agents::BasecampAgent do |
||
51 | 51 |
describe "#check" do |
52 | 52 |
it "should not emit events on its first run" do |
53 | 53 |
expect { @checker.check }.to change { Event.count }.by(0) |
54 |
+ expect(@checker.memory[:last_event]).to eq '2014-04-17T10:25:31.000+02:00' |
|
54 | 55 |
end |
55 | 56 |
it "should check that initial run creates an event" do |
56 |
- @checker.last_check_at = Time.now - 1.minute |
|
57 |
+ @checker.memory[:last_event] = '2014-04-17T10:25:31.000+02:00' |
|
57 | 58 |
expect { @checker.check }.to change { Event.count }.by(1) |
58 | 59 |
end |
59 | 60 |
end |
@@ -61,7 +62,7 @@ describe Agents::BasecampAgent do |
||
61 | 62 |
describe "#working?" do |
62 | 63 |
it "it is working when at least one event was emited" do |
63 | 64 |
@checker.should_not be_working |
64 |
- @checker.last_check_at = Time.now - 1.minute |
|
65 |
+ @checker.memory[:last_event] = '2014-04-17T10:25:31.000+02:00' |
|
65 | 66 |
@checker.check |
66 | 67 |
@checker.reload.should be_working |
67 | 68 |
end |
@@ -398,18 +398,86 @@ describe Agents::WebsiteAgent do |
||
398 | 398 |
event.payload['response']['title'].should == "hello!" |
399 | 399 |
end |
400 | 400 |
end |
401 |
+ |
|
402 |
+ describe "text parsing" do |
|
403 |
+ before do |
|
404 |
+ stub_request(:any, /text-site/).to_return(body: <<-EOF, status: 200) |
|
405 |
+water: wet |
|
406 |
+fire: hot |
|
407 |
+ EOF |
|
408 |
+ site = { |
|
409 |
+ 'name' => 'Some Text Response', |
|
410 |
+ 'expected_update_period_in_days' => '2', |
|
411 |
+ 'type' => 'text', |
|
412 |
+ 'url' => 'http://text-site.com', |
|
413 |
+ 'mode' => 'on_change', |
|
414 |
+ 'extract' => { |
|
415 |
+ 'word' => { 'regexp' => '^(.+?): (.+)$', index: 1 }, |
|
416 |
+ 'property' => { 'regexp' => '^(.+?): (.+)$', index: 2 }, |
|
417 |
+ } |
|
418 |
+ } |
|
419 |
+ @checker = Agents::WebsiteAgent.new(name: 'Text Site', options: site) |
|
420 |
+ @checker.user = users(:bob) |
|
421 |
+ @checker.save! |
|
422 |
+ end |
|
423 |
+ |
|
424 |
+ it "works with regexp" do |
|
425 |
+ @checker.options = @checker.options.merge('extract' => { |
|
426 |
+ 'word' => { 'regexp' => '^(?<word>.+?): (?<property>.+)$', index: 'word' }, |
|
427 |
+ 'property' => { 'regexp' => '^(?<word>.+?): (?<property>.+)$', index: 'property' }, |
|
428 |
+ }) |
|
429 |
+ |
|
430 |
+ lambda { |
|
431 |
+ @checker.check |
|
432 |
+ }.should change { Event.count }.by(2) |
|
433 |
+ |
|
434 |
+ event1, event2 = Event.last(2) |
|
435 |
+ event1.payload['word'].should == 'water' |
|
436 |
+ event1.payload['property'].should == 'wet' |
|
437 |
+ event2.payload['word'].should == 'fire' |
|
438 |
+ event2.payload['property'].should == 'hot' |
|
439 |
+ end |
|
440 |
+ |
|
441 |
+ it "works with regexp with named capture" do |
|
442 |
+ lambda { |
|
443 |
+ @checker.check |
|
444 |
+ }.should change { Event.count }.by(2) |
|
445 |
+ |
|
446 |
+ event1, event2 = Event.last(2) |
|
447 |
+ event1.payload['word'].should == 'water' |
|
448 |
+ event1.payload['property'].should == 'wet' |
|
449 |
+ event2.payload['word'].should == 'fire' |
|
450 |
+ event2.payload['property'].should == 'hot' |
|
451 |
+ end |
|
452 |
+ end |
|
401 | 453 |
end |
402 | 454 |
|
403 | 455 |
describe "#receive" do |
404 |
- it "should scrape from the url element in incoming event payload" do |
|
456 |
+ before do |
|
405 | 457 |
@event = Event.new |
406 | 458 |
@event.agent = agents(:bob_rain_notifier_agent) |
407 | 459 |
@event.payload = { 'url' => "http://xkcd.com" } |
460 |
+ end |
|
461 |
+ |
|
462 |
+ it "should scrape from the url element in incoming event payload" do |
|
463 |
+ lambda { |
|
464 |
+ @checker.options = @valid_options |
|
465 |
+ @checker.receive([@event]) |
|
466 |
+ }.should change { Event.count }.by(1) |
|
467 |
+ end |
|
468 |
+ |
|
469 |
+ it "should interpolate values from incoming event payload" do |
|
470 |
+ @event.payload['title'] = 'XKCD' |
|
408 | 471 |
|
409 | 472 |
lambda { |
473 |
+ @valid_options['extract']['site_title'] = { |
|
474 |
+ 'css' => "#comic img", 'value' => "'{{title}}'" |
|
475 |
+ } |
|
410 | 476 |
@checker.options = @valid_options |
411 | 477 |
@checker.receive([@event]) |
412 | 478 |
}.should change { Event.count }.by(1) |
479 |
+ |
|
480 |
+ Event.last.payload['site_title'].should == 'XKCD' |
|
413 | 481 |
end |
414 | 482 |
end |
415 | 483 |
end |
@@ -16,14 +16,14 @@ shared_examples_for Oauthable do |
||
16 | 16 |
@agent.oauthable?.should == true |
17 | 17 |
end |
18 | 18 |
|
19 |
- describe "valid_services" do |
|
19 |
+ describe "valid_services_for" do |
|
20 | 20 |
it "should return all available services without specifying valid_oauth_providers" do |
21 | 21 |
@agent = Agents::OauthableTestAgent.new |
22 |
- @agent.valid_services(users(:bob)).collect(&:id).sort.should == [services(:generic), services(:global)].collect(&:id).sort |
|
22 |
+ @agent.valid_services_for(users(:bob)).collect(&:id).sort.should == [services(:generic), services(:global)].collect(&:id).sort |
|
23 | 23 |
end |
24 | 24 |
|
25 | 25 |
it "should filter the services based on the agent defaults" do |
26 |
- @agent.valid_services(users(:bob)).to_a.should == Service.where(provider: @agent.valid_oauth_providers) |
|
26 |
+ @agent.valid_services_for(users(:bob)).to_a.should == Service.where(provider: @agent.valid_oauth_providers) |
|
27 | 27 |
end |
28 | 28 |
end |
29 | 29 |
end |
@@ -102,6 +102,15 @@ describe EventDrop do |
||
102 | 102 |
interpolate(t, @event).should eq('some title: http://some.site.example.org/') |
103 | 103 |
end |
104 | 104 |
|
105 |
+ it 'should use created_at from the payload if it exists' do |
|
106 |
+ created_at = @event.created_at - 86400 |
|
107 |
+ # Avoid timezone issue by using %s |
|
108 |
+ @event.payload['created_at'] = created_at.strftime("%s") |
|
109 |
+ @event.save! |
|
110 |
+ t = '{{created_at | date:"%s" }}' |
|
111 |
+ interpolate(t, @event).should eq(created_at.strftime("%s")) |
|
112 |
+ end |
|
113 |
+ |
|
105 | 114 |
it 'should be iteratable' do |
106 | 115 |
# to_liquid returns self |
107 | 116 |
t = "{% for pair in to_liquid %}{{pair | join:':' }}\n{% endfor %}" |
@@ -3,6 +3,8 @@ require 'spec_helper' |
||
3 | 3 |
describe ScenarioImport do |
4 | 4 |
let(:user) { users(:bob) } |
5 | 5 |
let(:guid) { "somescenarioguid" } |
6 |
+ let(:tag_fg_color) { "#ffffff" } |
|
7 |
+ let(:tag_bg_color) { "#000000" } |
|
6 | 8 |
let(:description) { "This is a cool Huginn Scenario that does something useful!" } |
7 | 9 |
let(:name) { "A useful Scenario" } |
8 | 10 |
let(:source_url) { "http://example.com/scenarios/2/export.json" } |
@@ -58,10 +60,12 @@ describe ScenarioImport do |
||
58 | 60 |
} |
59 | 61 |
end |
60 | 62 |
let(:valid_parsed_data) do |
61 |
- { |
|
63 |
+ { |
|
62 | 64 |
:name => name, |
63 | 65 |
:description => description, |
64 | 66 |
:guid => guid, |
67 |
+ :tag_fg_color => tag_fg_color, |
|
68 |
+ :tag_bg_color => tag_bg_color, |
|
65 | 69 |
:source_url => source_url, |
66 | 70 |
:exported_at => 2.days.ago.utc.iso8601, |
67 | 71 |
:agents => [ |
@@ -154,7 +158,7 @@ describe ScenarioImport do |
||
154 | 158 |
end |
155 | 159 |
end |
156 | 160 |
end |
157 |
- |
|
161 |
+ |
|
158 | 162 |
describe "#dangerous?" do |
159 | 163 |
it "returns false on most Agents" do |
160 | 164 |
ScenarioImport.new(:data => valid_data).should_not be_dangerous |
@@ -183,6 +187,8 @@ describe ScenarioImport do |
||
183 | 187 |
scenario_import.scenario.name.should == name |
184 | 188 |
scenario_import.scenario.description.should == description |
185 | 189 |
scenario_import.scenario.guid.should == guid |
190 |
+ scenario_import.scenario.tag_fg_color.should == tag_fg_color |
|
191 |
+ scenario_import.scenario.tag_bg_color.should == tag_bg_color |
|
186 | 192 |
scenario_import.scenario.source_url.should == source_url |
187 | 193 |
scenario_import.scenario.public.should be_falsey |
188 | 194 |
end |
@@ -281,6 +287,8 @@ describe ScenarioImport do |
||
281 | 287 |
|
282 | 288 |
existing_scenario.reload |
283 | 289 |
existing_scenario.guid.should == guid |
290 |
+ existing_scenario.tag_fg_color.should == tag_fg_color |
|
291 |
+ existing_scenario.tag_bg_color.should == tag_bg_color |
|
284 | 292 |
existing_scenario.description.should == description |
285 | 293 |
existing_scenario.name.should == name |
286 | 294 |
existing_scenario.source_url.should == source_url |
@@ -463,4 +471,4 @@ describe ScenarioImport do |
||
463 | 471 |
end |
464 | 472 |
end |
465 | 473 |
end |
466 |
-end |
|
474 |
+end |
@@ -20,6 +20,30 @@ describe Scenario do |
||
20 | 20 |
new_instance.should_not be_valid |
21 | 21 |
end |
22 | 22 |
|
23 |
+ it "validates tag_fg_color is hex color" do |
|
24 |
+ new_instance.tag_fg_color = '#N07H3X' |
|
25 |
+ new_instance.should_not be_valid |
|
26 |
+ new_instance.tag_fg_color = '#BADA55' |
|
27 |
+ new_instance.should be_valid |
|
28 |
+ end |
|
29 |
+ |
|
30 |
+ it "allows nil tag_fg_color" do |
|
31 |
+ new_instance.tag_fg_color = nil |
|
32 |
+ new_instance.should be_valid |
|
33 |
+ end |
|
34 |
+ |
|
35 |
+ it "validates tag_bg_color is hex color" do |
|
36 |
+ new_instance.tag_bg_color = '#N07H3X' |
|
37 |
+ new_instance.should_not be_valid |
|
38 |
+ new_instance.tag_bg_color = '#BADA55' |
|
39 |
+ new_instance.should be_valid |
|
40 |
+ end |
|
41 |
+ |
|
42 |
+ it "allows nil tag_bg_color" do |
|
43 |
+ new_instance.tag_bg_color = nil |
|
44 |
+ new_instance.should be_valid |
|
45 |
+ end |
|
46 |
+ |
|
23 | 47 |
it "only allows Agents owned by user" do |
24 | 48 |
new_instance.agent_ids = [agents(:bob_website_agent).id] |
25 | 49 |
new_instance.should be_valid |
@@ -5,13 +5,32 @@ describe Service do |
||
5 | 5 |
@user = users(:bob) |
6 | 6 |
end |
7 | 7 |
|
8 |
- it "should toggle the global flag" do |
|
9 |
- @service = services(:generic) |
|
10 |
- @service.global.should == false |
|
11 |
- @service.toggle_availability! |
|
12 |
- @service.global.should == true |
|
13 |
- @service.toggle_availability! |
|
14 |
- @service.global.should == false |
|
8 |
+ describe "#toggle_availability!" do |
|
9 |
+ it "should toggle the global flag" do |
|
10 |
+ @service = services(:generic) |
|
11 |
+ @service.global.should == false |
|
12 |
+ @service.toggle_availability! |
|
13 |
+ @service.global.should == true |
|
14 |
+ @service.toggle_availability! |
|
15 |
+ @service.global.should == false |
|
16 |
+ end |
|
17 |
+ |
|
18 |
+ it "disconnects agents and disables them if the previously global service is made private again", focus: true do |
|
19 |
+ agent = agents(:bob_basecamp_agent) |
|
20 |
+ jane_agent = agents(:jane_basecamp_agent) |
|
21 |
+ |
|
22 |
+ service = agent.service |
|
23 |
+ service.toggle_availability! |
|
24 |
+ service.agents.length.should == 2 |
|
25 |
+ |
|
26 |
+ service.toggle_availability! |
|
27 |
+ jane_agent.reload |
|
28 |
+ jane_agent.service_id.should be_nil |
|
29 |
+ jane_agent.disabled.should be true |
|
30 |
+ |
|
31 |
+ service.reload |
|
32 |
+ service.agents.length.should == 1 |
|
33 |
+ end |
|
15 | 34 |
end |
16 | 35 |
|
17 | 36 |
it "disables all agents before beeing destroyed" do |
@@ -20,7 +39,7 @@ describe Service do |
||
20 | 39 |
service.destroy |
21 | 40 |
agent.reload |
22 | 41 |
agent.service_id.should be_nil |
23 |
- agent.disabled.should be_true |
|
42 |
+ agent.disabled.should be true |
|
24 | 43 |
end |
25 | 44 |
|
26 | 45 |
describe "preparing for a request" do |
@@ -74,6 +93,7 @@ describe Service do |
||
74 | 93 |
}.to change { @user.services.count }.by(1) |
75 | 94 |
service = @user.services.first |
76 | 95 |
service.name.should == 'johnqpublic' |
96 |
+ service.uid.should == '123456' |
|
77 | 97 |
service.provider.should == 'twitter' |
78 | 98 |
service.token.should == 'a1b2c3d4...' |
79 | 99 |
service.secret.should == 'abcdef1234' |
@@ -88,6 +108,7 @@ describe Service do |
||
88 | 108 |
service.provider.should == '37signals' |
89 | 109 |
service.name.should == 'Dominik Sander' |
90 | 110 |
service.token.should == 'abcde' |
111 |
+ service.uid.should == '12345' |
|
91 | 112 |
service.refresh_token.should == 'fghrefresh' |
92 | 113 |
service.options[:user_id].should == 12345 |
93 | 114 |
service.expires_at = Time.at(1401554352) |
@@ -101,6 +122,7 @@ describe Service do |
||
101 | 122 |
service = @user.services.first |
102 | 123 |
service.provider.should == 'github' |
103 | 124 |
service.name.should == 'dsander' |
125 |
+ service.uid.should == '12345' |
|
104 | 126 |
service.token.should == 'agithubtoken' |
105 | 127 |
end |
106 | 128 |
end |
@@ -10,7 +10,7 @@ end |
||
10 | 10 |
|
11 | 11 |
# Required ENV variables that are normally set in .env are setup here for the test environment. |
12 | 12 |
require 'dotenv' |
13 |
-Dotenv.load File.join(File.dirname(__FILE__), "env.test") |
|
13 |
+Dotenv.overload File.join(File.dirname(__FILE__), "env.test") |
|
14 | 14 |
|
15 | 15 |
require File.expand_path("../../config/environment", __FILE__) |
16 | 16 |
require 'rspec/rails' |