@@ -1,5 +1,6 @@ |
||
1 | 1 |
# Changes |
2 | 2 |
|
3 |
+* 0.31 (Jan 2, 2014) - Agents now have an optional keep\_events\_for option that is propagated to created events' expires\_at field, and they update their events' expires\_at fields on change. |
|
3 | 4 |
* 0.3 (Jan 1, 2014) - Remove symbolization of memory, options, and payloads; convert memory, options, and payloads to JSON from YAML. Migration will perform conversion and adjust tables to be UTF-8. Recommend making a DB backup before migrating. |
4 | 5 |
* 0.2 (Nov 6, 2013) - PeakDetectorAgent now uses `window_duration_in_days` and `min_peak_spacing_in_days`. Additionally, peaks trigger when the time series rises over the standard deviation multiple, not after it starts to fall. |
5 | 6 |
* June 29, 2013 - Removed rails\_admin because it was causing deployment issues. Better to have people install their favorite admin tool if they want one. |
@@ -34,6 +34,12 @@ showLinks = -> |
||
34 | 34 |
$(".link-region .cannot-receive-events").hide() |
35 | 35 |
showEventDescriptions() |
36 | 36 |
|
37 |
+hideEventCreation = -> |
|
38 |
+ $(".event-related-region").hide() |
|
39 |
+ |
|
40 |
+showEventCreation = -> |
|
41 |
+ $(".event-related-region").show() |
|
42 |
+ |
|
37 | 43 |
showEventDescriptions = -> |
38 | 44 |
if $("#agent_source_ids").val() |
39 | 45 |
$.getJSON "/agents/event_descriptions", { ids: $("#agent_source_ids").val().join(",") }, (json) => |
@@ -132,6 +138,11 @@ $(document).ready -> |
||
132 | 138 |
else |
133 | 139 |
hideLinks() |
134 | 140 |
|
141 |
+ if json.can_create_events |
|
142 |
+ showEventCreation() |
|
143 |
+ else |
|
144 |
+ hideEventCreation() |
|
145 |
+ |
|
135 | 146 |
$(".description").html(json.description_html) if json.description_html? |
136 | 147 |
|
137 | 148 |
if $("#agent_options").hasClass("showing-default") || $("#agent_options").val().match(/\A\s*(\{\s*\}|)\s*\Z/g) |
@@ -153,3 +164,9 @@ $(document).ready -> |
||
153 | 164 |
showLinks() |
154 | 165 |
else |
155 | 166 |
hideLinks() |
167 |
+ |
|
168 |
+ if $(".event-related-region") |
|
169 |
+ if $(".event-related-region").data("can-create-events") == true |
|
170 |
+ showEventCreation() |
|
171 |
+ else |
|
172 |
+ hideEventCreation() |
@@ -33,6 +33,7 @@ class AgentsController < ApplicationController |
||
33 | 33 |
render :json => { |
34 | 34 |
:can_be_scheduled => agent.can_be_scheduled?, |
35 | 35 |
:can_receive_events => agent.can_receive_events?, |
36 |
+ :can_create_events => agent.can_create_events?, |
|
36 | 37 |
:options => agent.default_options, |
37 | 38 |
:description_html => agent.html_description |
38 | 39 |
} |
@@ -15,11 +15,14 @@ class Agent < ActiveRecord::Base |
||
15 | 15 |
SCHEDULES = %w[every_2m every_5m every_10m every_30m every_1h every_2h every_5h every_12h every_1d every_2d every_7d |
16 | 16 |
midnight 1am 2am 3am 4am 5am 6am 7am 8am 9am 10am 11am noon 1pm 2pm 3pm 4pm 5pm 6pm 7pm 8pm 9pm 10pm 11pm] |
17 | 17 |
|
18 |
- attr_accessible :options, :memory, :name, :type, :schedule, :source_ids |
|
18 |
+ EVENT_RETENTION_SCHEDULES = [["Forever", 0], ["1 day", 1], *([2, 3, 4, 5, 7, 14, 21, 30, 45, 90, 180, 365].map {|n| ["#{n} days", n] })] |
|
19 |
+ |
|
20 |
+ attr_accessible :options, :memory, :name, :type, :schedule, :source_ids, :keep_events_for |
|
19 | 21 |
|
20 | 22 |
json_serialize :options, :memory |
21 | 23 |
|
22 | 24 |
validates_presence_of :name, :user |
25 |
+ validates_inclusion_of :keep_events_for, :in => EVENT_RETENTION_SCHEDULES.map(&:last) |
|
23 | 26 |
validate :sources_are_owned |
24 | 27 |
validate :validate_schedule |
25 | 28 |
validate :validate_options |
@@ -29,6 +32,7 @@ class Agent < ActiveRecord::Base |
||
29 | 32 |
before_validation :unschedule_if_cannot_schedule |
30 | 33 |
before_save :unschedule_if_cannot_schedule |
31 | 34 |
before_create :set_last_checked_event_id |
35 |
+ after_save :possibly_update_event_expirations |
|
32 | 36 |
|
33 | 37 |
belongs_to :user, :inverse_of => :agents |
34 | 38 |
has_many :events, :dependent => :delete_all, :inverse_of => :agent, :order => "events.id desc" |
@@ -87,21 +91,23 @@ class Agent < ActiveRecord::Base |
||
87 | 91 |
last_event_at && last_error_log_at && last_error_log_at > (last_event_at - 2.minutes) |
88 | 92 |
end |
89 | 93 |
|
90 |
- def sources_are_owned |
|
91 |
- errors.add(:sources, "must be owned by you") unless sources.all? {|s| s.user == user } |
|
92 |
- end |
|
93 |
- |
|
94 | 94 |
def create_event(attrs) |
95 | 95 |
if can_create_events? |
96 |
- events.create!({ :user => user }.merge(attrs)) |
|
96 |
+ events.create!({ :user => user, :expires_at => new_event_expiration_date }.merge(attrs)) |
|
97 | 97 |
else |
98 | 98 |
error "This Agent cannot create events!" |
99 | 99 |
end |
100 | 100 |
end |
101 | 101 |
|
102 |
- def validate_schedule |
|
103 |
- unless cannot_be_scheduled? |
|
104 |
- errors.add(:schedule, "is not a valid schedule") unless SCHEDULES.include?(schedule.to_s) |
|
102 |
+ def new_event_expiration_date |
|
103 |
+ keep_events_for > 0 ? keep_events_for.days.from_now : nil |
|
104 |
+ end |
|
105 |
+ |
|
106 |
+ def update_event_expirations! |
|
107 |
+ if keep_events_for == 0 |
|
108 |
+ events.update_all :expires_at => nil |
|
109 |
+ else |
|
110 |
+ events.update_all "expires_at = DATE_ADD(`created_at`, INTERVAL #{keep_events_for.to_i} DAY)" |
|
105 | 111 |
end |
106 | 112 |
end |
107 | 113 |
|
@@ -116,14 +122,6 @@ class Agent < ActiveRecord::Base |
||
116 | 122 |
end |
117 | 123 |
end |
118 | 124 |
|
119 |
- def set_default_schedule |
|
120 |
- self.schedule = default_schedule unless schedule.present? || cannot_be_scheduled? |
|
121 |
- end |
|
122 |
- |
|
123 |
- def unschedule_if_cannot_schedule |
|
124 |
- self.schedule = nil if cannot_be_scheduled? |
|
125 |
- end |
|
126 |
- |
|
127 | 125 |
def default_schedule |
128 | 126 |
self.class.default_schedule |
129 | 127 |
end |
@@ -152,10 +150,13 @@ class Agent < ActiveRecord::Base |
||
152 | 150 |
!cannot_create_events? |
153 | 151 |
end |
154 | 152 |
|
155 |
- def set_last_checked_event_id |
|
156 |
- if newest_event_id = Event.order("id desc").limit(1).pluck(:id).first |
|
157 |
- self.last_checked_event_id = newest_event_id |
|
158 |
- end |
|
153 |
+ def log(message, options = {}) |
|
154 |
+ puts "Agent##{id}: #{message}" unless Rails.env.test? |
|
155 |
+ AgentLog.log_for_agent(self, message, options) |
|
156 |
+ end |
|
157 |
+ |
|
158 |
+ def error(message, options = {}) |
|
159 |
+ log(message, options.merge(:level => 4)) |
|
159 | 160 |
end |
160 | 161 |
|
161 | 162 |
def delete_logs! |
@@ -163,16 +164,38 @@ class Agent < ActiveRecord::Base |
||
163 | 164 |
update_column :last_error_log_at, nil |
164 | 165 |
end |
165 | 166 |
|
166 |
- def log(message, options = {}) |
|
167 |
- puts "Agent##{id}: #{message}" unless Rails.env.test? |
|
168 |
- AgentLog.log_for_agent(self, message, options) |
|
167 |
+ # Validations and Callbacks |
|
168 |
+ |
|
169 |
+ def sources_are_owned |
|
170 |
+ errors.add(:sources, "must be owned by you") unless sources.all? {|s| s.user == user } |
|
169 | 171 |
end |
170 | 172 |
|
171 |
- def error(message, options = {}) |
|
172 |
- log(message, options.merge(:level => 4)) |
|
173 |
+ def validate_schedule |
|
174 |
+ unless cannot_be_scheduled? |
|
175 |
+ errors.add(:schedule, "is not a valid schedule") unless SCHEDULES.include?(schedule.to_s) |
|
176 |
+ end |
|
177 |
+ end |
|
178 |
+ |
|
179 |
+ def set_default_schedule |
|
180 |
+ self.schedule = default_schedule unless schedule.present? || cannot_be_scheduled? |
|
181 |
+ end |
|
182 |
+ |
|
183 |
+ def unschedule_if_cannot_schedule |
|
184 |
+ self.schedule = nil if cannot_be_scheduled? |
|
185 |
+ end |
|
186 |
+ |
|
187 |
+ def set_last_checked_event_id |
|
188 |
+ if newest_event_id = Event.order("id desc").limit(1).pluck(:id).first |
|
189 |
+ self.last_checked_event_id = newest_event_id |
|
190 |
+ end |
|
191 |
+ end |
|
192 |
+ |
|
193 |
+ def possibly_update_event_expirations |
|
194 |
+ update_event_expirations! if keep_events_for_changed? |
|
173 | 195 |
end |
174 | 196 |
|
175 | 197 |
# Class Methods |
198 |
+ |
|
176 | 199 |
class << self |
177 | 200 |
def cannot_be_scheduled! |
178 | 201 |
@cannot_be_scheduled = true |
@@ -21,6 +21,8 @@ class Event < ActiveRecord::Base |
||
21 | 21 |
end |
22 | 22 |
|
23 | 23 |
def self.cleanup_expired! |
24 |
+ affected_agents = Event.where("expires_at IS NOT NULL AND expires_at < ?", Time.now).group("agent_id").pluck(:agent_id) |
|
24 | 25 |
Event.where("expires_at IS NOT NULL AND expires_at < ?", Time.now).delete_all |
26 |
+ Agent.where(:id => affected_agents).update_all "events_count = (select count(*) from events where agent_id = agents.id)" |
|
25 | 27 |
end |
26 | 28 |
end |
@@ -44,6 +44,16 @@ |
||
44 | 44 |
</div> |
45 | 45 |
</div> |
46 | 46 |
|
47 |
+ |
|
48 |
+ <div class='event-related-region' data-can-create-events="<%= @agent.can_create_events? %>"> |
|
49 |
+ <div class="control-group"> |
|
50 |
+ <%= f.label :keep_events_for, "Keep events", :class => 'control-label' %> |
|
51 |
+ <div class="controls"> |
|
52 |
+ <%= f.select :keep_events_for, options_for_select(Agent::EVENT_RETENTION_SCHEDULES, @agent.keep_events_for), {}, :class => 'span4' %> |
|
53 |
+ </div> |
|
54 |
+ </div> |
|
55 |
+ </div> |
|
56 |
+ |
|
47 | 57 |
<div class="control-group"> |
48 | 58 |
<%= f.label :sources, :class => 'control-label' %> |
49 | 59 |
<div class="controls link-region" data-can-receive-events="<%= @agent.can_receive_events? %>"> |
@@ -84,6 +84,11 @@ |
||
84 | 84 |
|
85 | 85 |
<% if @agent.can_create_events? %> |
86 | 86 |
<p> |
87 |
+ <b>Keep events:</b> |
|
88 |
+ <%= (Agent::EVENT_RETENTION_SCHEDULES.detect {|s| s.last == @agent.keep_events_for } || [@agent.keep_events_for]).first %> |
|
89 |
+ </p> |
|
90 |
+ |
|
91 |
+ <p> |
|
87 | 92 |
<b>Last event created:</b> |
88 | 93 |
<%= @agent.last_event_at ? time_ago_in_words(@agent.last_event_at) + " ago" : "never" %> |
89 | 94 |
</p> |
@@ -6,6 +6,11 @@ |
||
6 | 6 |
</div> |
7 | 7 |
|
8 | 8 |
<p> |
9 |
+ <b>Expires in:</b> |
|
10 |
+ <%= @event.expires_at ? time_ago_in_words(@event.expires_at) : 'never' %> |
|
11 |
+ </p> |
|
12 |
+ |
|
13 |
+ <p> |
|
9 | 14 |
<b>Payload:</b> |
10 | 15 |
<pre><%= Utils.pretty_jsonify @event.payload || {} %></pre> |
11 | 16 |
</p> |
@@ -2,9 +2,10 @@ |
||
2 | 2 |
<div class='row'> |
3 | 3 |
<div class="span5 offset2"> |
4 | 4 |
<h1>Your agents are standing by</h1> |
5 |
- <p>Know the world around you</p> |
|
5 |
+ <p>Huginn monitors the world and acts on your behalf.</p> |
|
6 | 6 |
|
7 |
- <%= link_to "Signup", new_user_registration_path, :class => "btn btn-primary btn-large center" %> |
|
7 |
+ <%= link_to "Login", new_user_session_path, :class => "btn btn-large" %> |
|
8 |
+ <%= link_to "Signup", new_user_registration_path, :class => "btn btn-primary btn-large" %> |
|
8 | 9 |
</div> |
9 | 10 |
<div class="span3"> |
10 | 11 |
<%= image_tag 'odin.jpg', :class => 'img-rounded', :title => "Wägner, Wilhelm. 1882. Nordisch-germanische Götter und Helden. Otto Spamer, Leipzig & Berlin. Page 7." %> |
@@ -0,0 +1,5 @@ |
||
1 |
+class AddKeepEventsForToAgents < ActiveRecord::Migration |
|
2 |
+ def change |
|
3 |
+ add_column :agents, :keep_events_for, :integer, :null => false, :default => 0 |
|
4 |
+ end |
|
5 |
+end |
@@ -11,7 +11,7 @@ |
||
11 | 11 |
# |
12 | 12 |
# It's strongly recommended to check this file into your version control system. |
13 | 13 |
|
14 |
-ActiveRecord::Schema.define(:version => 20131105063248) do |
|
14 |
+ActiveRecord::Schema.define(:version => 20131227000021) do |
|
15 | 15 |
|
16 | 16 |
create_table "agent_logs", :force => true do |t| |
17 | 17 |
t.integer "agent_id", :null => false |
@@ -33,10 +33,13 @@ ActiveRecord::Schema.define(:version => 20131105063248) do |
||
33 | 33 |
t.datetime "last_check_at" |
34 | 34 |
t.datetime "last_receive_at" |
35 | 35 |
t.integer "last_checked_event_id" |
36 |
- t.datetime "created_at", :null => false |
|
37 |
- t.datetime "updated_at", :null => false |
|
36 |
+ t.datetime "created_at", :null => false |
|
37 |
+ t.datetime "updated_at", :null => false |
|
38 | 38 |
t.text "memory", :limit => 2147483647 |
39 | 39 |
t.datetime "last_webhook_at" |
40 |
+ t.datetime "last_event_at" |
|
41 |
+ t.datetime "last_error_log_at" |
|
42 |
+ t.integer "keep_events_for", :default => 0, :null => false |
|
40 | 43 |
end |
41 | 44 |
|
42 | 45 |
add_index "agents", ["schedule"], :name => "index_agents_on_schedule" |
@@ -49,7 +49,7 @@ describe EventsController do |
||
49 | 49 |
}.should change { Event.count }.by(1) |
50 | 50 |
Event.last.payload.should == events(:bob_website_agent_event).payload |
51 | 51 |
Event.last.agent.should == events(:bob_website_agent_event).agent |
52 |
- Event.last.created_at.should be_within(1).of(Time.now) |
|
52 |
+ Event.last.created_at.to_i.should be_within(2).of(Time.now.to_i) |
|
53 | 53 |
end |
54 | 54 |
|
55 | 55 |
it "can only re-emit Events for the current user" do |
@@ -1,6 +1,7 @@ |
||
1 | 1 |
jane_website_agent: |
2 | 2 |
type: Agents::WebsiteAgent |
3 | 3 |
user: jane |
4 |
+ events_count: 1 |
|
4 | 5 |
schedule: "5pm" |
5 | 6 |
name: "ZKCD" |
6 | 7 |
options: <%= { |
@@ -16,6 +17,7 @@ jane_website_agent: |
||
16 | 17 |
bob_website_agent: |
17 | 18 |
type: Agents::WebsiteAgent |
18 | 19 |
user: bob |
20 |
+ events_count: 1 |
|
19 | 21 |
schedule: "midnight" |
20 | 22 |
name: "ZKCD" |
21 | 23 |
options: <%= { |
@@ -33,6 +35,7 @@ bob_weather_agent: |
||
33 | 35 |
user: bob |
34 | 36 |
schedule: "midnight" |
35 | 37 |
name: "SF Weather" |
38 |
+ keep_events_for: 45 |
|
36 | 39 |
options: <%= { |
37 | 40 |
:location => 94102, |
38 | 41 |
:lat => 37.779329, |
@@ -45,6 +48,7 @@ jane_weather_agent: |
||
45 | 48 |
user: jane |
46 | 49 |
schedule: "midnight" |
47 | 50 |
name: "SF Weather" |
51 |
+ keep_events_for: 30 |
|
48 | 52 |
options: <%= { |
49 | 53 |
:location => 94103, |
50 | 54 |
:lat => 37.779329, |
@@ -249,7 +249,7 @@ describe Agent do |
||
249 | 249 |
agent.should have(0).errors_on(:base) |
250 | 250 |
end |
251 | 251 |
|
252 |
- it "symbolizes options before validating" do |
|
252 |
+ it "makes options symbol-indifferent before validating" do |
|
253 | 253 |
agent = Agents::SomethingSource.new(:name => "something") |
254 | 254 |
agent.user = users(:bob) |
255 | 255 |
agent.options["bad"] = true |
@@ -258,7 +258,7 @@ describe Agent do |
||
258 | 258 |
agent.should have(0).errors_on(:base) |
259 | 259 |
end |
260 | 260 |
|
261 |
- it "symbolizes memory before validating" do |
|
261 |
+ it "makes memory symbol-indifferent before validating" do |
|
262 | 262 |
agent = Agents::SomethingSource.new(:name => "something") |
263 | 263 |
agent.user = users(:bob) |
264 | 264 |
agent.memory["bad"] = 2 |
@@ -318,7 +318,85 @@ describe Agent do |
||
318 | 318 |
agent.user = users(:jane) |
319 | 319 |
agent.should have(0).errors_on(:sources) |
320 | 320 |
end |
321 |
+ |
|
322 |
+ it "validates keep_events_for" do |
|
323 |
+ agent = Agents::SomethingSource.new(:name => "something") |
|
324 |
+ agent.user = users(:bob) |
|
325 |
+ agent.should be_valid |
|
326 |
+ agent.keep_events_for = nil |
|
327 |
+ agent.should have(1).errors_on(:keep_events_for) |
|
328 |
+ agent.keep_events_for = 1000 |
|
329 |
+ agent.should have(1).errors_on(:keep_events_for) |
|
330 |
+ agent.keep_events_for = "" |
|
331 |
+ agent.should have(1).errors_on(:keep_events_for) |
|
332 |
+ agent.keep_events_for = 5 |
|
333 |
+ agent.should be_valid |
|
334 |
+ agent.keep_events_for = 0 |
|
335 |
+ agent.should be_valid |
|
336 |
+ agent.keep_events_for = 365 |
|
337 |
+ agent.should be_valid |
|
338 |
+ |
|
339 |
+ # Rails seems to call to_i on the input. This guards against future changes to that behavior. |
|
340 |
+ agent.keep_events_for = "drop table;" |
|
341 |
+ agent.keep_events_for.should == 0 |
|
342 |
+ end |
|
321 | 343 |
end |
344 |
+ |
|
345 |
+ describe "cleaning up now-expired events" do |
|
346 |
+ before do |
|
347 |
+ @agent = Agents::SomethingSource.new(:name => "something") |
|
348 |
+ @agent.keep_events_for = 5 |
|
349 |
+ @agent.user = users(:bob) |
|
350 |
+ @agent.save! |
|
351 |
+ @event = @agent.create_event :payload => { "hello" => "world" } |
|
352 |
+ @event.expires_at.to_i.should be_within(2).of(5.days.from_now.to_i) |
|
353 |
+ end |
|
354 |
+ |
|
355 |
+ describe "when keep_events_for has not changed" do |
|
356 |
+ it "does nothing" do |
|
357 |
+ mock(@agent).update_event_expirations!.times(0) |
|
358 |
+ |
|
359 |
+ @agent.options[:foo] = "bar1" |
|
360 |
+ @agent.save! |
|
361 |
+ |
|
362 |
+ @agent.options[:foo] = "bar1" |
|
363 |
+ @agent.keep_events_for = 5 |
|
364 |
+ @agent.save! |
|
365 |
+ end |
|
366 |
+ end |
|
367 |
+ |
|
368 |
+ describe "when keep_events_for is changed" do |
|
369 |
+ it "updates events' expires_at" do |
|
370 |
+ lambda { |
|
371 |
+ @agent.options[:foo] = "bar1" |
|
372 |
+ @agent.keep_events_for = 3 |
|
373 |
+ @agent.save! |
|
374 |
+ }.should change { @event.reload.expires_at } |
|
375 |
+ @event.expires_at.to_i.should be_within(2).of(3.days.from_now.to_i) |
|
376 |
+ end |
|
377 |
+ |
|
378 |
+ it "updates events relative to their created_at" do |
|
379 |
+ @event.update_attribute :created_at, 2.days.ago |
|
380 |
+ @event.reload.created_at.to_i.should be_within(2).of(2.days.ago.to_i) |
|
381 |
+ |
|
382 |
+ lambda { |
|
383 |
+ @agent.options[:foo] = "bar2" |
|
384 |
+ @agent.keep_events_for = 3 |
|
385 |
+ @agent.save! |
|
386 |
+ }.should change { @event.reload.expires_at } |
|
387 |
+ @event.expires_at.to_i.should be_within(2).of(1.days.from_now.to_i) |
|
388 |
+ end |
|
389 |
+ |
|
390 |
+ it "nulls out expires_at when keep_events_for is set to 0" do |
|
391 |
+ lambda { |
|
392 |
+ @agent.options[:foo] = "bar" |
|
393 |
+ @agent.keep_events_for = 0 |
|
394 |
+ @agent.save! |
|
395 |
+ }.should change { @event.reload.expires_at }.to(nil) |
|
396 |
+ end |
|
397 |
+ end |
|
398 |
+ end |
|
399 |
+ |
|
322 | 400 |
end |
323 | 401 |
|
324 | 402 |
describe "recent_error_logs?" do |
@@ -371,4 +449,28 @@ describe Agent do |
||
371 | 449 |
end |
372 | 450 |
end |
373 | 451 |
end |
452 |
+ |
|
453 |
+ describe "#create_event" do |
|
454 |
+ describe "when the agent has keep_events_for set" do |
|
455 |
+ before do |
|
456 |
+ agents(:jane_weather_agent).keep_events_for.should > 0 |
|
457 |
+ end |
|
458 |
+ |
|
459 |
+ it "sets expires_at on created events" do |
|
460 |
+ event = agents(:jane_weather_agent).create_event :payload => { 'hi' => 'there' } |
|
461 |
+ event.expires_at.to_i.should be_within(5).of(agents(:jane_weather_agent).keep_events_for.days.from_now.to_i) |
|
462 |
+ end |
|
463 |
+ end |
|
464 |
+ |
|
465 |
+ describe "when the agent does not have keep_events_for set" do |
|
466 |
+ before do |
|
467 |
+ agents(:jane_website_agent).keep_events_for.should == 0 |
|
468 |
+ end |
|
469 |
+ |
|
470 |
+ it "does not set expires_at on created events" do |
|
471 |
+ event = agents(:jane_website_agent).create_event :payload => { 'hi' => 'there' } |
|
472 |
+ event.expires_at.should be_nil |
|
473 |
+ end |
|
474 |
+ end |
|
475 |
+ end |
|
374 | 476 |
end |
@@ -4,15 +4,15 @@ describe Agents::WebsiteAgent do |
||
4 | 4 |
before do |
5 | 5 |
stub_request(:any, /xkcd/).to_return(:body => File.read(Rails.root.join("spec/data_fixtures/xkcd.html")), :status => 200) |
6 | 6 |
@site = { |
7 |
- :name => "XKCD", |
|
8 |
- :expected_update_period_in_days => 2, |
|
9 |
- :type => "html", |
|
10 |
- :url => "http://xkcd.com", |
|
11 |
- :mode => :on_change, |
|
12 |
- :extract => { |
|
13 |
- :url => {:css => "#comic img", :attr => "src"}, |
|
14 |
- :title => {:css => "#comic img", :attr => "title"} |
|
15 |
- } |
|
7 |
+ 'name' => "XKCD", |
|
8 |
+ 'expected_update_period_in_days' => 2, |
|
9 |
+ 'type' => "html", |
|
10 |
+ 'url' => "http://xkcd.com", |
|
11 |
+ 'mode' => 'on_change', |
|
12 |
+ 'extract' => { |
|
13 |
+ 'url' => {'css' => "#comic img", 'attr' => "src"}, |
|
14 |
+ 'title' => {'css' => "#comic img", 'attr' => "title"} |
|
15 |
+ } |
|
16 | 16 |
} |
17 | 17 |
@checker = Agents::WebsiteAgent.new(:name => "xkcd", :options => @site) |
18 | 18 |
@checker.user = users(:bob) |
@@ -27,7 +27,7 @@ describe Agents::WebsiteAgent do |
||
27 | 27 |
|
28 | 28 |
it "should always save events when in :all mode" do |
29 | 29 |
lambda { |
30 |
- @site[:mode] = :all |
|
30 |
+ @site['mode'] = 'all' |
|
31 | 31 |
@checker.options = @site |
32 | 32 |
@checker.check |
33 | 33 |
@checker.check |
@@ -35,7 +35,7 @@ describe Agents::WebsiteAgent do |
||
35 | 35 |
end |
36 | 36 |
|
37 | 37 |
it "should log an error if the number of results for a set of extraction patterns differs" do |
38 |
- @site[:extract][:url][:css] = "div" |
|
38 |
+ @site['extract']['url']['css'] = "div" |
|
39 | 39 |
@checker.options = @site |
40 | 40 |
@checker.check |
41 | 41 |
@checker.logs.first.message.should =~ /Got an uneven number of matches/ |
@@ -68,20 +68,20 @@ describe Agents::WebsiteAgent do |
||
68 | 68 |
it "parses CSS" do |
69 | 69 |
@checker.check |
70 | 70 |
event = Event.last |
71 |
- event.payload[:url].should == "http://imgs.xkcd.com/comics/evolving.png" |
|
72 |
- event.payload[:title].should =~ /^Biologists play reverse/ |
|
71 |
+ event.payload['url'].should == "http://imgs.xkcd.com/comics/evolving.png" |
|
72 |
+ event.payload['title'].should =~ /^Biologists play reverse/ |
|
73 | 73 |
end |
74 | 74 |
|
75 | 75 |
it "should turn relative urls to absolute" do |
76 | 76 |
rel_site = { |
77 |
- :name => "XKCD", |
|
78 |
- :expected_update_period_in_days => 2, |
|
79 |
- :type => "html", |
|
80 |
- :url => "http://xkcd.com", |
|
81 |
- :mode => :on_change, |
|
82 |
- :extract => { |
|
83 |
- :url => {:css => "#topLeft a", :attr => "href"}, |
|
84 |
- :title => {:css => "#topLeft a", :text => "true"} |
|
77 |
+ 'name' => "XKCD", |
|
78 |
+ 'expected_update_period_in_days' => 2, |
|
79 |
+ 'type' => "html", |
|
80 |
+ 'url' => "http://xkcd.com", |
|
81 |
+ 'mode' => :on_change, |
|
82 |
+ 'extract' => { |
|
83 |
+ 'url' => {'css' => "#topLeft a", 'attr' => "href"}, |
|
84 |
+ 'title' => {'css' => "#topLeft a", 'text' => "true"} |
|
85 | 85 |
} |
86 | 86 |
} |
87 | 87 |
rel = Agents::WebsiteAgent.new(:name => "xkcd", :options => rel_site) |
@@ -89,28 +89,28 @@ describe Agents::WebsiteAgent do |
||
89 | 89 |
rel.save! |
90 | 90 |
rel.check |
91 | 91 |
event = Event.last |
92 |
- event.payload[:url].should == "http://xkcd.com/about" |
|
92 |
+ event.payload['url'].should == "http://xkcd.com/about" |
|
93 | 93 |
end |
94 |
- |
|
94 |
+ |
|
95 | 95 |
describe "JSON" do |
96 | 96 |
it "works with paths" do |
97 | 97 |
json = { |
98 |
- :response => { |
|
99 |
- :version => 2, |
|
100 |
- :title => "hello!" |
|
101 |
- } |
|
98 |
+ 'response' => { |
|
99 |
+ 'version' => 2, |
|
100 |
+ 'title' => "hello!" |
|
101 |
+ } |
|
102 | 102 |
} |
103 | 103 |
stub_request(:any, /json-site/).to_return(:body => json.to_json, :status => 200) |
104 | 104 |
site = { |
105 |
- :name => "Some JSON Response", |
|
106 |
- :expected_update_period_in_days => 2, |
|
107 |
- :type => "json", |
|
108 |
- :url => "http://json-site.com", |
|
109 |
- :mode => :on_change, |
|
110 |
- :extract => { |
|
111 |
- :version => { :path => "response.version" }, |
|
112 |
- :title => { :path => "response.title" } |
|
113 |
- } |
|
105 |
+ 'name' => "Some JSON Response", |
|
106 |
+ 'expected_update_period_in_days' => 2, |
|
107 |
+ 'type' => "json", |
|
108 |
+ 'url' => "http://json-site.com", |
|
109 |
+ 'mode' => 'on_change', |
|
110 |
+ 'extract' => { |
|
111 |
+ 'version' => {'path' => "response.version"}, |
|
112 |
+ 'title' => {'path' => "response.title"} |
|
113 |
+ } |
|
114 | 114 |
} |
115 | 115 |
checker = Agents::WebsiteAgent.new(:name => "Weather Site", :options => site) |
116 | 116 |
checker.user = users(:bob) |
@@ -118,30 +118,30 @@ describe Agents::WebsiteAgent do |
||
118 | 118 |
|
119 | 119 |
checker.check |
120 | 120 |
event = Event.last |
121 |
- event.payload[:version].should == 2 |
|
122 |
- event.payload[:title].should == "hello!" |
|
121 |
+ event.payload['version'].should == 2 |
|
122 |
+ event.payload['title'].should == "hello!" |
|
123 | 123 |
end |
124 | 124 |
|
125 | 125 |
it "can handle arrays" do |
126 | 126 |
json = { |
127 |
- :response => { |
|
128 |
- :data => [ |
|
129 |
- { :title => "first", :version => 2 }, |
|
130 |
- { :title => "second", :version => 2.5 } |
|
131 |
- ] |
|
132 |
- } |
|
127 |
+ 'response' => { |
|
128 |
+ 'data' => [ |
|
129 |
+ {'title' => "first", 'version' => 2}, |
|
130 |
+ {'title' => "second", 'version' => 2.5} |
|
131 |
+ ] |
|
132 |
+ } |
|
133 | 133 |
} |
134 | 134 |
stub_request(:any, /json-site/).to_return(:body => json.to_json, :status => 200) |
135 | 135 |
site = { |
136 |
- :name => "Some JSON Response", |
|
137 |
- :expected_update_period_in_days => 2, |
|
138 |
- :type => "json", |
|
139 |
- :url => "http://json-site.com", |
|
140 |
- :mode => :on_change, |
|
141 |
- :extract => { |
|
142 |
- :title => { :path => "response.data[*].title" }, |
|
143 |
- :version => { :path => "response.data[*].version" } |
|
144 |
- } |
|
136 |
+ 'name' => "Some JSON Response", |
|
137 |
+ 'expected_update_period_in_days' => 2, |
|
138 |
+ 'type' => "json", |
|
139 |
+ 'url' => "http://json-site.com", |
|
140 |
+ 'mode' => 'on_change', |
|
141 |
+ 'extract' => { |
|
142 |
+ :title => {'path' => "response.data[*].title"}, |
|
143 |
+ :version => {'path' => "response.data[*].version"} |
|
144 |
+ } |
|
145 | 145 |
} |
146 | 146 |
checker = Agents::WebsiteAgent.new(:name => "Weather Site", :options => site) |
147 | 147 |
checker.user = users(:bob) |
@@ -152,28 +152,28 @@ describe Agents::WebsiteAgent do |
||
152 | 152 |
}.should change { Event.count }.by(2) |
153 | 153 |
|
154 | 154 |
event = Event.all[-1] |
155 |
- event.payload[:version].should == 2.5 |
|
156 |
- event.payload[:title].should == "second" |
|
155 |
+ event.payload['version'].should == 2.5 |
|
156 |
+ event.payload['title'].should == "second" |
|
157 | 157 |
|
158 | 158 |
event = Event.all[-2] |
159 |
- event.payload[:version].should == 2 |
|
160 |
- event.payload[:title].should == "first" |
|
159 |
+ event.payload['version'].should == 2 |
|
160 |
+ event.payload['title'].should == "first" |
|
161 | 161 |
end |
162 | 162 |
|
163 | 163 |
it "stores the whole object if :extract is not specified" do |
164 | 164 |
json = { |
165 |
- :response => { |
|
166 |
- :version => 2, |
|
167 |
- :title => "hello!" |
|
168 |
- } |
|
165 |
+ 'response' => { |
|
166 |
+ 'version' => 2, |
|
167 |
+ 'title' => "hello!" |
|
168 |
+ } |
|
169 | 169 |
} |
170 | 170 |
stub_request(:any, /json-site/).to_return(:body => json.to_json, :status => 200) |
171 | 171 |
site = { |
172 |
- :name => "Some JSON Response", |
|
173 |
- :expected_update_period_in_days => 2, |
|
174 |
- :type => "json", |
|
175 |
- :url => "http://json-site.com", |
|
176 |
- :mode => :on_change |
|
172 |
+ 'name' => "Some JSON Response", |
|
173 |
+ 'expected_update_period_in_days' => 2, |
|
174 |
+ 'type' => "json", |
|
175 |
+ 'url' => "http://json-site.com", |
|
176 |
+ 'mode' => 'on_change' |
|
177 | 177 |
} |
178 | 178 |
checker = Agents::WebsiteAgent.new(:name => "Weather Site", :options => site) |
179 | 179 |
checker.user = users(:bob) |
@@ -181,8 +181,8 @@ describe Agents::WebsiteAgent do |
||
181 | 181 |
|
182 | 182 |
checker.check |
183 | 183 |
event = Event.last |
184 |
- event.payload[:response][:version].should == 2 |
|
185 |
- event.payload[:response][:title].should == "hello!" |
|
184 |
+ event.payload['response']['version'].should == 2 |
|
185 |
+ event.payload['response']['title'].should == "hello!" |
|
186 | 186 |
end |
187 | 187 |
end |
188 | 188 |
end |
@@ -13,25 +13,50 @@ describe Event do |
||
13 | 13 |
Event.last.agent.should == events(:bob_website_agent_event).agent |
14 | 14 |
Event.last.lat.should == 2 |
15 | 15 |
Event.last.lng.should == 3 |
16 |
- Event.last.created_at.should be_within(1).of(Time.now) |
|
16 |
+ Event.last.created_at.to_i.should be_within(2).of(Time.now.to_i) |
|
17 | 17 |
end |
18 | 18 |
end |
19 | 19 |
|
20 | 20 |
describe ".cleanup_expired!" do |
21 |
- it "removes any Events whose expired_at date is non-null and in the past" do |
|
22 |
- event = agents(:jane_weather_agent).create_event :expires_at => 2.hours.from_now |
|
21 |
+ it "removes any Events whose expired_at date is non-null and in the past, updating Agent counter caches" do |
|
22 |
+ half_hour_event = agents(:jane_weather_agent).create_event :expires_at => 20.minutes.from_now |
|
23 |
+ one_hour_event = agents(:bob_weather_agent).create_event :expires_at => 1.hours.from_now |
|
24 |
+ two_hour_event = agents(:jane_weather_agent).create_event :expires_at => 2.hours.from_now |
|
25 |
+ three_hour_event = agents(:jane_weather_agent).create_event :expires_at => 3.hours.from_now |
|
26 |
+ non_expiring_event = agents(:bob_weather_agent).create_event({}) |
|
27 |
+ |
|
28 |
+ initial_bob_count = agents(:bob_weather_agent).reload.events_count |
|
29 |
+ initial_jane_count = agents(:jane_weather_agent).reload.events_count |
|
23 | 30 |
|
24 | 31 |
current_time = Time.now |
25 | 32 |
stub(Time).now { current_time } |
26 | 33 |
|
27 | 34 |
Event.cleanup_expired! |
28 |
- Event.find_by_id(event.id).should_not be_nil |
|
29 |
- current_time = 119.minutes.from_now |
|
35 |
+ Event.find_by_id(half_hour_event.id).should_not be_nil |
|
36 |
+ Event.find_by_id(one_hour_event.id).should_not be_nil |
|
37 |
+ Event.find_by_id(two_hour_event.id).should_not be_nil |
|
38 |
+ Event.find_by_id(three_hour_event.id).should_not be_nil |
|
39 |
+ Event.find_by_id(non_expiring_event.id).should_not be_nil |
|
40 |
+ agents(:bob_weather_agent).reload.events_count.should == initial_bob_count |
|
41 |
+ agents(:jane_weather_agent).reload.events_count.should == initial_jane_count |
|
42 |
+ |
|
43 |
+ current_time = 119.minutes.from_now # move almost 2 hours into the future |
|
30 | 44 |
Event.cleanup_expired! |
31 |
- Event.find_by_id(event.id).should_not be_nil |
|
32 |
- current_time = 2.minutes.from_now |
|
45 |
+ Event.find_by_id(half_hour_event.id).should be_nil |
|
46 |
+ Event.find_by_id(one_hour_event.id).should be_nil |
|
47 |
+ Event.find_by_id(two_hour_event.id).should_not be_nil |
|
48 |
+ Event.find_by_id(three_hour_event.id).should_not be_nil |
|
49 |
+ Event.find_by_id(non_expiring_event.id).should_not be_nil |
|
50 |
+ agents(:bob_weather_agent).reload.events_count.should == initial_bob_count - 1 |
|
51 |
+ agents(:jane_weather_agent).reload.events_count.should == initial_jane_count - 1 |
|
52 |
+ |
|
53 |
+ current_time = 2.minutes.from_now # move 2 minutes further into the future |
|
33 | 54 |
Event.cleanup_expired! |
34 |
- Event.find_by_id(event.id).should be_nil |
|
55 |
+ Event.find_by_id(two_hour_event.id).should be_nil |
|
56 |
+ Event.find_by_id(three_hour_event.id).should_not be_nil |
|
57 |
+ Event.find_by_id(non_expiring_event.id).should_not be_nil |
|
58 |
+ agents(:bob_weather_agent).reload.events_count.should == initial_bob_count - 1 |
|
59 |
+ agents(:jane_weather_agent).reload.events_count.should == initial_jane_count - 2 |
|
35 | 60 |
end |
36 | 61 |
|
37 | 62 |
it "doesn't touch Events with no expired_at" do |