HasRemote 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/HasRemote.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{HasRemote}
8
- s.version = "0.1.4"
8
+ s.version = "0.1.5"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Sjoerd Andringa"]
12
- s.date = %q{2010-01-25}
12
+ s.date = %q{2010-02-18}
13
13
  s.description = %q{Bind a remote ActiveResource object to your local ActiveRecord objects, delegate attributes and optionally cache remote attributes locally.}
14
14
  s.email = %q{sjoerd.andringa@innovationfactory.eu}
15
15
  s.extra_rdoc_files = [
data/README.rdoc CHANGED
@@ -106,7 +106,7 @@ Synchronize all records of one specific model:
106
106
  User.synchronize!
107
107
 
108
108
  The latter automatically requests all remote resources that have been changed (including new and deleted records) since the last successful synchronization for this particular model.
109
- You may need to override the <tt>changed_remotes_since</tt> class method in your model to match your host's REST API.
109
+ You may need to override the <tt>updated_remotes</tt> class method in your model to match your host's REST API.
110
110
 
111
111
  See <tt>HasRemote::Synchronizable</tt> for more information.
112
112
 
@@ -124,6 +124,9 @@ To specify additional parameters to send with the request that fetches updated r
124
124
 
125
125
  rake hr:sync PARAMS="since=01-01-2010&limit=25"
126
126
 
127
+ (If you've overridden the <tt>updated_remotes</tt> class method on one of your synchronizable models, then note that these parameters are
128
+ passed in as a hash to <tt>updated_remotes</tt> internally.)
129
+
127
130
  === Documentation
128
131
 
129
132
  To generate RDocs for this plugin, from the has_remote directory run:
@@ -146,4 +149,4 @@ To run the specs of the plugin, from the has_remote directory run:
146
149
  Questions, requests and patches can be directed to sjoerd.andringa[AT]innovationfactory[DOT]nl.
147
150
 
148
151
 
149
- Copyright (c) 2009 Innovation Factory.
152
+ Copyright (c) 2009-2010 Innovation Factory.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.4
1
+ 0.1.5
@@ -2,7 +2,8 @@ class CreateHasRemoteSynchronizations < ActiveRecord::Migration
2
2
  def self.up
3
3
  create_table :has_remote_synchronizations do |t|
4
4
  t.string :model_name, :null => false
5
- t.datetime :latest_change, :null => false
5
+ t.datetime :last_record_updated_at, :null => false
6
+ t.integer :last_record_id, :null => false
6
7
  t.datetime :created_at
7
8
  end
8
9
  end
@@ -36,25 +36,25 @@ module HasRemote
36
36
  # time is given. This may include new and optionally deleted (tagged by a 'deleted_at' attribute) resources.
37
37
  #
38
38
  # This is used by the <tt>synchronize!</tt> class method. By default
39
- # it queries '/updated?since=<time>' on your resources URL, where 'time' is
40
- # the latest updated_at time of the last processed remote objects.
39
+ # it queries '/updated?since=<time>&last_record_id=<id>' on your resources URL, where 'time' is
40
+ # the updated_at value of the last processed remote object and 'last_record_id' is its id.
41
+ #
42
+ # *Options*
43
+ #
44
+ # The options passed in to <tt>synchronize!</tt> will be passed through to <tt>updated_remotes</tt>.
45
+ # By default they are used as request parameters for remotely requesting the updated records.
41
46
  #
42
47
  # You may need to override this method in your model to match your host's REST API or to change
43
48
  # the default time, e.g.:
44
49
  #
45
- # def self.changed_remotes_since(time = nil)
46
- # time ||= 12.hours.ago
47
- # User::Remote.find :all, :from => :search, :params => {:updated_since => time.strftime('...') }
50
+ # def self.updated_remotes(options = nil)
51
+ # time = last_synchronization.try(:last_record_updated_at) || 12.hours.ago
52
+ # User::Remote.find :all, :from => :search, :params => { :updated_since => time.to_s(:db) }
48
53
  # end
49
54
  #
50
- # *Arguments*
51
- #
52
- # [<tt>time</tt>:] The given time that defines since (see above)
53
- # [<tt>params</tt>:] Optional hash of additional parameters to send with the request (e.g. {:limit => 5})
54
- #
55
- def changed_remotes_since(time = nil, params = {})
56
- time ||= 1.week.ago
57
- remote_class.find :all, :from => :updated, :params => {:since => time.to_s}.merge(params)
55
+ def updated_remotes( options = {} )
56
+ time = last_synchronization.try(:last_record_updated_at) || 1.week.ago
57
+ remote_class.find :all, :from => :updated, :params => { :since => time.to_s, :last_record_id => last_synchronization.try(:last_record_id) }.merge( options )
58
58
  end
59
59
 
60
60
  # Will update all records that have been created, updated or deleted on the remote host
@@ -62,15 +62,14 @@ module HasRemote
62
62
  #
63
63
  # *Options*
64
64
  #
65
- # [<tt>since</tt>:] Optionally override the time as of which updated resources are fetched. Also see <tt>changes_remotes_since</tt>.
66
- #
67
- # All other options are passed in as parameters to <tt>changes_remotes_since</tt>.
65
+ # Options are passed through to <tt>updated_remotes</tt> which is called in order to fetch the updated records.
66
+ # Use this if you want to override its default options.
68
67
  #
69
68
  def synchronize!(options = {})
70
69
  logger.info( "*** Start synchronizing #{table_name} at #{Time.now.to_s :long} ***\n" )
71
70
  @sync_count = 0
72
71
  begin
73
- changed_objects = changed_remotes_since( options.delete(:since) || synchronized_at, options )
72
+ changed_objects = updated_remotes( options )
74
73
  if changed_objects.any?
75
74
  # Do everything within transaction to prevent ending up in half-synchronized situation if an exception is raised.
76
75
  transaction { sync_all_records_for(changed_objects) }
@@ -80,25 +79,25 @@ module HasRemote
80
79
  rescue => e
81
80
  logger.warn( " - Synchronization of #{table_name} failed: #{e} \n #{e.backtrace}" )
82
81
  else
83
- self.synchronized_at = changed_objects.map { |o| time_of_update(o) }.sort.last if changed_objects.any?
82
+ if changed_objects.any?
83
+ last_record_updated_at = time_updated(changed_objects.last)
84
+ last_record_id = changed_objects.last.send(remote_primary_key)
85
+ HasRemote::Synchronization.create!(:model_name => self.name, :last_record_updated_at => last_record_updated_at, :last_record_id => last_record_id)
86
+ end
84
87
  logger.info( " - Synchronized #{@sync_count} #{table_name}.\n" ) if @sync_count > 0
85
88
  ensure
86
89
  logger.info( "*** Stopped synchronizing #{table_name} at #{Time.now.to_s :long} ***\n" )
87
90
  end
88
91
  end
89
92
 
90
- # Time of the last successful synchronization.
93
+ # The last successful synchronization for this model.
91
94
  #
92
- def synchronized_at
93
- HasRemote::Synchronization.for(self.name).latest_change
95
+ def last_synchronization
96
+ HasRemote::Synchronization.for(self.name).last
94
97
  end
95
98
 
96
99
  private
97
100
 
98
- def synchronized_at=(time) #:nodoc:
99
- HasRemote::Synchronization.create!(:model_name => self.name, :latest_change => time)
100
- end
101
-
102
101
  def sync_all_records_for(resources) #:nodoc:
103
102
  resources.each { |resource| sync_all_records_for_resource(resource) }
104
103
  end
@@ -143,7 +142,7 @@ module HasRemote
143
142
  update_and_save_record_for_resource(new(remote_foreign_key => resource.send(remote_primary_key)), resource)
144
143
  end
145
144
 
146
- def time_of_update(resource)
145
+ def time_updated(resource)
147
146
  (resource.respond_to?(:deleted_at) && resource.deleted_at) ? resource.deleted_at : resource.updated_at
148
147
  end
149
148
 
@@ -1,15 +1,11 @@
1
1
  module HasRemote
2
-
2
+
3
3
  class Synchronization < ActiveRecord::Base #:nodoc:
4
4
  set_table_name 'has_remote_synchronizations'
5
-
6
- named_scope :for, lambda { |model_name| {:conditions => ["model_name = ?", model_name.to_s.classify] } } do
7
- def latest_change
8
- self.find(:first, :order => 'latest_change DESC').latest_change rescue nil
9
- end
10
- end
11
-
12
- validates_presence_of :model_name, :latest_change
5
+
6
+ named_scope :for, lambda { |model_name| {:conditions => ["model_name = ?", model_name.to_s.classify] } }
7
+
8
+ validates_presence_of :model_name, :last_record_updated_at, :last_record_id
13
9
  end
14
-
10
+
15
11
  end
@@ -2,7 +2,7 @@ namespace :hr do
2
2
 
3
3
  desc 'Synchronizes all attributes locally cached by has_remote'
4
4
  task :sync => :environment do
5
- models = ENV['MODELS'].nil? ? HasRemote.models : extract_models
5
+ models = ENV['MODELS'] ? extract_models : HasRemote.models
6
6
  options = ENV['PARAMS'] ? Rack::Utils.parse_query(ENV['PARAMS']) : {}
7
7
  models.each{|model| model.synchronize!(options)}
8
8
  end
data/spec/caching_spec.rb CHANGED
@@ -1,108 +1,112 @@
1
1
  require File.dirname(__FILE__) + '/spec_helper.rb'
2
2
 
3
3
  context "Given existing remote resources" do
4
-
4
+
5
5
  before(:each) do
6
6
  User.delete_all
7
7
  stub_resource 1, :email => "joeremote@foo.bar"
8
8
  stub_resource 2, :email => "jane_remote@foo.bar"
9
9
  end
10
-
10
+
11
11
  describe "a user" do
12
-
12
+
13
13
  it "should return cached attributes" do
14
14
  User.should respond_to(:cached_attributes)
15
15
  User.cached_attributes.should include(:email)
16
16
  end
17
-
17
+
18
18
  it "should return changed remotes since yesterday" do
19
19
  user_1, user_2 = mock(:user), mock(:user)
20
20
  time = 1.day.ago
21
- User::Remote.should_receive(:find).with(:all,{:from => :updated, :params=>{:since=>time.to_s}}).once.and_return([user_1, user_2])
22
-
23
- User.should respond_to(:changed_remotes_since)
24
- User.changed_remotes_since(time).should include(user_1, user_2)
21
+ User::Remote.should_receive(:find).with(:all,{:from => :updated, :params=>{:since=>time.to_s, :last_record_id=>nil}}).once.and_return([user_1, user_2])
22
+
23
+ User.should respond_to(:updated_remotes)
24
+ User.updated_remotes(:since => time.to_s).should include(user_1, user_2)
25
25
  end
26
-
27
- it "should find last synchronization time" do
26
+
27
+ it "should find last synchronization" do
28
28
  times = []
29
- 1.upto(3) do |i|
30
- times << i.days.ago
31
- HasRemote::Synchronization.create(:model_name => 'HasRemoteSpec::User', :latest_change => times.last)
29
+ 3.downto(1) do |i|
30
+ HasRemote::Synchronization.create(:model_name => 'HasRemoteSpec::User', :last_record_updated_at => i.days.ago, :last_record_id => i)
32
31
  end
33
- User.should respond_to(:synchronized_at)
34
- User.synchronized_at.to_s.should == times.first.to_s
32
+ User.should respond_to(:last_synchronization)
33
+ sync = User.last_synchronization
34
+ sync.last_record_updated_at.to_s.should == 1.day.ago.to_s
35
+ sync.last_record_id.should == 1
35
36
  end
36
-
37
+
37
38
  it "should not delegate cached remote attributes" do
38
39
  user = User.create! :remote_id => 1
39
40
  User::Remote.should_not_receive(:find)
40
41
  user.email.should == "joeremote@foo.bar"
41
42
  end
42
-
43
+
43
44
  it "should update its cached remote attributes on save" do
44
45
  user = User.create! :remote_id => 1
45
46
  user[:email].should == "joeremote@foo.bar"
46
47
  user.update_attributes(:remote_id => 2)
47
- user[:email].should == "jane_remote@foo.bar"
48
+ user[:email].should == "jane_remote@foo.bar"
48
49
  end
49
50
 
50
51
  it "should not update its cached remote attributes if skip_update_cache is true" do
51
52
  user = User.create! :remote_id => 1, :skip_update_cache => true
52
53
  user[:email].should == nil
53
54
  end
54
-
55
+
55
56
  end
56
57
 
57
58
  describe "synchronization" do
58
-
59
+
59
60
  describe "for the User model" do
60
-
61
+
61
62
  describe "with updated and deleted remotes" do
62
-
63
+
63
64
  before(:each) do
64
- @user_1, @user_2, @user_3 = User.create!(:remote_id => 1), User.create!(:remote_id => 2), User.create!(:remote_id => 3)
65
-
66
65
  @yesterday = DateTime.parse 1.day.ago.to_s
67
-
66
+ @last_id = 5
67
+ @user_1, @user_2, @user_3 = User.create!(:remote_id => @last_id), User.create!(:remote_id => 1), User.create!(:remote_id => 2)
68
+
68
69
  resources = [
69
- mock(:user, :id => 1, :email => "changed@foo.bar", :updated_at => @yesterday),
70
- mock(:user, :id => 2, :email => "altered@foo.bar", :updated_at => 2.days.ago, :deleted_at => nil),
71
- mock(:user, :id => 3, :email => "deleted@foo.bar", :updated_at => 2.days.ago, :deleted_at => 2.days.ago),
70
+ mock(:user, :id => 1, :email => "altered@foo.bar", :updated_at => 2.days.ago, :deleted_at => nil),
71
+ mock(:user, :id => 2, :email => "deleted@foo.bar", :updated_at => 2.days.ago, :deleted_at => 2.days.ago),
72
+ mock(:user, :id => 3, :email => "new-deleted@foo.bar", :updated_at => 2.days.ago, :deleted_at => 2.days.ago),
72
73
  mock(:user, :id => 4, :email => "new@foo.bar", :updated_at => @yesterday),
73
- mock(:user, :id => 5, :email => "new-deleted@foo.bar", :updated_at => 2.days.ago, :deleted_at => 2.days.ago),
74
+ mock(:user, :id => @last_id, :email => "changed@foo.bar", :updated_at => @yesterday)
74
75
  ]
75
- User.stub!(:changed_remotes_since).and_return(resources)
76
-
76
+ User.stub!(:updated_remotes).and_return(resources)
77
+
77
78
  lambda { User.synchronize! }.should change(HasRemote::Synchronization, :count).by(1)
78
79
  end
79
-
80
- it "should keep track of the last synchronization" do
81
- HasRemote::Synchronization.for("HasRemoteSpec::User").latest_change.should == @yesterday
80
+
81
+ it "should keep track of the last synchronized record" do
82
+ sync = HasRemote::Synchronization.for("HasRemoteSpec::User").last
83
+
84
+ sync.last_record_updated_at.should == @yesterday
85
+ sync.last_record_id.should == @last_id
82
86
  end
83
-
87
+
84
88
  it "should update changed users" do
85
89
  @user_1.reload[:email].should == "changed@foo.bar"
86
90
  @user_2.reload[:email].should == "altered@foo.bar"
87
91
  end
88
-
92
+
89
93
  it "should destroy deleted users" do
90
94
  User.exists?(@user_3).should be_false
91
95
  end
92
-
96
+
93
97
  it "should create added users" do
94
98
  User.exists?(:remote_id => 4).should be_true
95
99
  end
96
-
100
+
97
101
  it "should not create deleted users" do
98
- User.exists?(:remote_id => 5).should be_false
102
+ User.exists?(:remote_id => 3).should be_false
99
103
  end
100
104
  end
101
-
105
+
102
106
  describe "that fails" do
103
-
107
+
104
108
  before(:each) do
105
-
109
+
106
110
  @failure = lambda {
107
111
  user_1, user_2 = User.create!(:remote_id => 1), User.create!(:remote_id => 2)
108
112
 
@@ -112,64 +116,64 @@ context "Given existing remote resources" do
112
116
  mock(:user, :id => 1, :email => "changed@foo.bar", :updated_at => yesterday),
113
117
  mock(:user, :id => 2, :email => "altered@foo.bar", :updated_at => 2.days.ago)
114
118
  ]
115
-
116
- User.stub!(:changed_remotes_since).and_return(resources)
117
-
119
+
120
+ User.stub!(:updated_remotes).and_return(resources)
121
+
118
122
  resources.last.should_receive(:send).and_raise "All hell breaks loose" # Raise when attr is read from resource 2.
119
-
123
+
120
124
  User.synchronize!
121
125
  }
122
126
  end
123
-
127
+
124
128
  it "should do it silently" do
125
129
  @failure.should_not raise_error
126
130
  end
127
-
131
+
128
132
  it "should not create a synchronization record" do
129
133
  @failure.should_not change(HasRemote::Synchronization, :count)
130
134
  end
131
-
135
+
132
136
  end
133
-
137
+
134
138
  end
135
-
139
+
136
140
  describe "for a single user" do
137
-
141
+
138
142
  it "should update the user" do
139
143
  user = User.create! :remote_id => 1
140
144
  user[:email].should == "joeremote@foo.bar"
141
-
145
+
142
146
  stub_resource 1, :email => "changed@foo.bar"
143
-
147
+
144
148
  user.update_cached_attributes!
145
149
  user[:email].should == "changed@foo.bar"
146
150
  end
147
-
151
+
148
152
  end
149
-
153
+
150
154
  end
151
-
155
+
152
156
  describe "synchronizing new cheeses" do
153
157
  before do
154
158
  resources = [
155
159
  mock(:cheese, :id => 1, :name => "Brie", :updated_at => Date.yesterday)
156
160
  ]
157
- Cheese.stub!(:changed_remotes_since).and_return(resources)
161
+ Cheese.stub!(:updated_remotes).and_return(resources)
158
162
  lambda{ Cheese.synchronize! }.should change(Cheese, :count).from(0).to(1)
159
163
  end
160
-
164
+
161
165
  after { Cheese.delete_all }
162
-
166
+
163
167
  it "should populate the local 'maturity' attribute with its default database value" do
164
168
  Cheese.first.maturity.should == 5
165
169
  end
166
-
170
+
167
171
  it "should populate the local 'smell' attribute with the value set inside of a before_validation callback" do
168
- Cheese.first.smell.should == 5 * 10
172
+ Cheese.first.smell.should == 5 * 10
169
173
  end
170
-
174
+
171
175
  end
172
-
176
+
173
177
  end
174
178
 
175
179
  def stub_resource(id, attrs)
data/spec/schema.rb CHANGED
@@ -4,22 +4,23 @@ ActiveRecord::Schema.define(:version => 0) do
4
4
  create_table :books do |t|
5
5
  t.integer :custom_remote_id
6
6
  end
7
-
7
+
8
8
  create_table :users do |t|
9
9
  t.integer :remote_id
10
10
  t.string :email
11
11
  end
12
-
12
+
13
13
  create_table :products do |t|
14
14
  t.integer :remote_id
15
15
  end
16
16
 
17
17
  create_table :has_remote_synchronizations do |t|
18
18
  t.string :model_name, :null => false
19
- t.datetime :latest_change, :null => false
19
+ t.datetime :last_record_updated_at, :null => false
20
+ t.integer :last_record_id, :null => false
20
21
  t.datetime :created_at
21
22
  end
22
-
23
+
23
24
  create_table :cheeses do |t|
24
25
  t.string :name
25
26
  t.integer :maturity, :default => 5
@@ -1,11 +1,12 @@
1
1
  require File.dirname(__FILE__) + '/spec_helper.rb'
2
2
 
3
3
  describe HasRemote::Synchronization do
4
-
4
+
5
5
  subject { HasRemote::Synchronization.new }
6
-
6
+
7
7
  it { should validate_presence_of(:model_name) }
8
- it { should validate_presence_of(:latest_change) }
8
+ it { should validate_presence_of(:last_record_updated_at) }
9
+ it { should validate_presence_of(:last_record_id) }
9
10
  it { should have_named_scope("for('User')") }
10
11
 
11
12
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: HasRemote
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sjoerd Andringa
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-01-25 00:00:00 +01:00
12
+ date: 2010-02-18 00:00:00 +01:00
13
13
  default_executable:
14
14
  dependencies: []
15
15