changeling 0.0.7 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -5,6 +5,8 @@
5
5
  [travis-home]: http://travis-ci.org/
6
6
  [brew-home]: http://mxcl.github.com/homebrew/
7
7
  [elasticsearch-home]: http://www.elasticsearch.org
8
+ [sidekiq-home]: https://github.com/mperham/sidekiq
9
+ [resque-home]: https://github.com/defunkt/resque
8
10
 
9
11
  A flexible and lightweight object change tracking system.
10
12
 
@@ -38,7 +40,7 @@ $ brew install elasticsearch
38
40
  ```
39
41
 
40
42
  ## Usage
41
-
43
+ ### Models
42
44
  Include the Trackling module for any class you want to keep track of:
43
45
 
44
46
  ```ruby
@@ -49,35 +51,40 @@ class Post
49
51
  end
50
52
  ```
51
53
 
52
- If you want to keep track of the user who made the changes:
54
+ ### Controllers
55
+ If you are using a Rails app, and have the notion of a "user"", you may want to track of which user made which changes.
56
+ Unfortunately models don't understand the concept of the currently signed in user. We'll have to leverage the controller to tack on this information.
53
57
 
54
58
  ```ruby
55
59
  # Doesn't have to be ApplicationController, perhaps you only want it in controllers for certain resources.
56
60
  class ApplicationController < ActionController::Base
57
61
  include Changeling::Blameling
58
-
62
+
59
63
  # Changeling assumes your user is current_user, but if not, override the changeling_blame_user method like so:
60
64
  def changeling_blame_user
61
65
  current_account
62
66
  end
63
-
67
+
64
68
  # Controller logic here...
65
69
  end
66
70
  ```
67
71
 
68
- That's it! Including the module(s) will silently keep track of any changes made to objects of this class.
69
- For example:
72
+ ### Asynchronous Tracking
73
+ Sometimes in high production load, you don't want another gem clogging up your resource pipeline, slowing down performance and potentially causing downtime.
74
+ Changeling has built-in Asynchronous support so you don't have to go and write your own callbacks and queues!
75
+ Changeling is compatible with [Sidekiq][sidekiq-home] and [Resque][resque-home].
76
+ When your object is saved, a job is placed in the 'changeling' queue.
70
77
 
71
78
  ```ruby
72
- @post = Post.first
73
- @post.title
74
- => 'Old Title'
75
- @post.title = 'New Title'
76
- @post.save
79
+ class Post
80
+ # include Changeling::Trackling
81
+ include Changeling::Async::Trackling
82
+
83
+ # Model logic here...
84
+ end
77
85
  ```
78
86
 
79
- This code will save a history that represents that the title for this post has been changed.
80
-
87
+ ### Accessing Changeling's history
81
88
  If you wish to see what has been logged, include the Probeling module:
82
89
 
83
90
  ```ruby
@@ -93,37 +100,40 @@ With Probeling, you can check out the changes that have been made! They're store
93
100
  You can access the up to the last 10 changes simply by calling
94
101
 
95
102
  ```ruby
96
- @post.history
103
+ # Alias for this method: @post.history
104
+ @post.loglings
97
105
  ```
98
106
 
99
- You can access a different number of records by passing in a number to the .history method:
107
+ You can access a different number of records by passing in a number to the .loglings method:
100
108
 
101
109
  ```ruby
102
110
  # Will automatically handle if there are less than the number of histories requested.
103
- @post.history(50)
111
+ @post.loglings(50)
104
112
  ```
105
113
 
106
114
  Access all of an objects history:
107
115
 
108
116
  ```ruby
109
- @post.all_history
117
+ # Alias for this method: @post.all_history
118
+ @post.all_loglings
110
119
  ```
111
120
 
112
121
  Access all of an objects history where a specific field was changed:
113
122
 
114
123
  ```ruby
115
- @post.history_for_field(:title)
124
+ # Alias for this method: @post.history_for_field(field)
125
+ @post.loglings_for_field(:title)
116
126
  # Or if you prefer stringified fields:
117
- @post.history_for_field('title')
127
+ @post.loglings_for_field('title')
118
128
 
119
129
  # You can also pass in a number to limit your results
120
- @post.history_for_field(:title, 10)
130
+ @post.loglings_for_field(:title, 10)
121
131
  ```
122
132
 
123
- Properties of Loglings (history objects):
133
+ ### Logling Properties (history objects):
124
134
 
125
135
  ```ruby
126
- log = @post.history.first
136
+ log = @post.loglings.first
127
137
 
128
138
  log.klass # class of the object that the Logling is tracking.
129
139
  => Post
data/Rakefile CHANGED
@@ -2,6 +2,20 @@
2
2
  require "bundler/gem_tasks"
3
3
  require "rspec/core/rake_task"
4
4
 
5
+ Dir.glob('benchmarks/*.rake').each { |r| import r }
6
+
7
+ desc "Run all benchmarks."
8
+ task :benchmark, :count, :fields do |t, args|
9
+ args.with_defaults(:count => 1000, :fields => 20)
10
+
11
+ Rake::Task["benchmark_setup"].invoke
12
+ Rake::Task["benchmark_tire"].reenable
13
+ Rake::Task["benchmark_tire"].invoke
14
+ puts "====================================================================\n\n"
15
+ Rake::Task["benchmark_changeling"].reenable
16
+ Rake::Task["benchmark_changeling"].invoke
17
+ end
18
+
5
19
  RSpec::Core::RakeTask.new("spec") do |spec|
6
20
  spec.pattern = "spec/**/*_spec.rb"
7
21
  end
@@ -12,3 +26,36 @@ RSpec::Core::RakeTask.new('spec:progress') do |spec|
12
26
  end
13
27
 
14
28
  task :default => :spec
29
+
30
+
31
+ # Helper methods
32
+ def generate_loglings(count, fields)
33
+ hashes = generate_logling_hashes(count, fields)
34
+
35
+ loglings = []
36
+
37
+ hashes.each { |hash| loglings << Changeling::Models::Logling.new(hash) }
38
+
39
+ loglings
40
+ end
41
+
42
+ def generate_logling_hashes(count, fields)
43
+ hashes = []
44
+ count.times do
45
+ hash = {}
46
+
47
+ hash['klass'] = "Object"
48
+ hash['oid'] = BSON::ObjectId.new.to_s
49
+ hash['modified_by'] = BSON::ObjectId.new.to_s
50
+ hash['modified_at'] = Time.now.to_s
51
+ hash['modifications'] = {}
52
+ fields.times do |time|
53
+ hash['modifications']["name_of_field_#{time}"] = ["value_of_field_before_#{time}", "value_of_field_after_#{time}"]
54
+ end
55
+ hash['modifications'] = hash['modifications'].to_json
56
+
57
+ hashes << hash
58
+ end
59
+
60
+ hashes
61
+ end
@@ -0,0 +1,16 @@
1
+ task :benchmark_setup do
2
+ Rake::Task["install"].invoke
3
+
4
+ begin
5
+ require 'changeling'
6
+ require 'bson'
7
+ rescue LoadError
8
+ require 'rubygems'
9
+ require 'changeling'
10
+ require 'bson'
11
+ end
12
+
13
+ # Setup Tire stuff for benchmarks.
14
+ Tire::Model::Search.index_prefix "changeling_benchmark"
15
+ Changeling::Models::Logling.tire.index.delete
16
+ end
@@ -0,0 +1,34 @@
1
+ desc "Benchmark Changeling processing times, from after_save hook through the final Tire index update."
2
+ task :benchmark_changeling, :count, :fields do |t, args|
3
+ args.with_defaults(:count => 1000, :fields => 20)
4
+ count = args[:count].to_i
5
+ fields = args[:fields].to_i
6
+
7
+ Rake::Task["benchmark_setup"].invoke
8
+
9
+ puts "Benchmarking Changeling: #{count} object(s) with #{fields} fields each.".upcase
10
+ puts "Generating objects. This may take a minute..."
11
+
12
+ hashes = generate_logling_hashes(count, fields)
13
+ loglings = []
14
+
15
+ puts "Done generating objects. Proceeding to benchmark..."
16
+
17
+ Benchmark.bmbm do |bm|
18
+ bm.report('Creation of Logling Index') do
19
+ Changeling::Models::Logling.tire.create_elasticsearch_index
20
+ end
21
+
22
+ bm.report("Initializing Loglings") do
23
+ hashes.each { |hash| Changeling::Models::Logling.new(hash) }
24
+ end
25
+
26
+ bm.report('Creating Loglings') do
27
+ hashes.each { |hash| Changeling::Models::Logling.create(hash) }
28
+ end
29
+
30
+ bm.report('Deletion of Logling Index') do
31
+ Changeling::Models::Logling.tire.index.delete
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ desc "Benchmark Tire indexing times alone when objects are already created."
2
+ task :benchmark_tire, :count, :fields do |t, args|
3
+ args.with_defaults(:count => 1000, :fields => 20)
4
+ count = args[:count].to_i
5
+ fields = args[:fields].to_i
6
+
7
+ Rake::Task["benchmark_setup"].invoke
8
+
9
+ puts "Benchmarking Tire: #{count} Logling(s) with #{fields} fields each.".upcase
10
+ puts "Generating loglings. This may take a minute..."
11
+
12
+ loglings = generate_loglings(count, fields)
13
+
14
+ puts "Done generating loglings. Proceeding to benchmark..."
15
+
16
+ Benchmark.bmbm do |bm|
17
+ bm.report('Creation of Logling Index') do
18
+ Changeling::Models::Logling.tire.create_elasticsearch_index
19
+ end
20
+
21
+ bm.report('Inserting Loglings into Index') do
22
+ loglings.each { |logling| logling.update_index }
23
+ end
24
+
25
+ bm.report('Deletion of Logling Index') do
26
+ Changeling::Models::Logling.tire.index.delete
27
+ end
28
+ end
29
+ end
@@ -16,7 +16,7 @@ Gem::Specification.new do |gem|
16
16
  gem.version = Changeling::VERSION
17
17
 
18
18
  # Dependencies
19
- gem.add_dependency "tire", "~> 0.5.4"
19
+ gem.add_dependency "tire", "~> 0.5.3"
20
20
  gem.add_dependency "activemodel"
21
21
 
22
22
  # Development Dependencies
@@ -37,4 +37,6 @@ Gem::Specification.new do |gem|
37
37
  gem.add_development_dependency "database_cleaner"
38
38
  gem.add_development_dependency "sqlite3"
39
39
  gem.add_development_dependency "rails"
40
+ gem.add_development_dependency "sidekiq"
41
+ gem.add_development_dependency "resque"
40
42
  end
@@ -17,6 +17,16 @@ module Changeling
17
17
  autoload :Search, 'changeling/support/search'
18
18
  end
19
19
 
20
+ module Async
21
+ autoload :Trackling, 'changeling/async/trackling'
22
+ autoload :SidekiqWorker, 'changeling/async/sidekiq_worker'
23
+ autoload :ResqueWorker, 'changeling/async/resque_worker'
24
+ end
25
+
26
+ module Exceptions
27
+ autoload :AsyncGemRequired, 'changeling/exceptions/async_gem_required'
28
+ end
29
+
20
30
  def self.blame_user
21
31
  self.changeling_store[:blame_user]
22
32
  end
@@ -0,0 +1,11 @@
1
+ module Changeling
2
+ module Async
3
+ class ResqueWorker
4
+ @queue = :changeling
5
+
6
+ def self.perform(json)
7
+ Changeling::Models::Logling.create(JSON.parse(json))
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ module Changeling
2
+ module Async
3
+ class SidekiqWorker
4
+ # If Sidekiq is not installed, we'll throw an error when trying to access Changeling::Async::Trackling
5
+ begin
6
+ include Sidekiq::Worker
7
+ sidekiq_options :queue => :changeling
8
+ rescue
9
+ end
10
+
11
+ def perform(json)
12
+ Changeling::Models::Logling.create(JSON.parse(json))
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ module Changeling
2
+ module Async
3
+ module Trackling
4
+ def self.included(base)
5
+ base.after_update :async_save_logling
6
+ end
7
+
8
+ def async_save_logling
9
+ unless defined?(Sidekiq) || defined?(Resque)
10
+ raise Changeling::Exceptions::AsyncGemRequired
11
+ end
12
+
13
+ if self.changes && !self.changes.empty?
14
+ logling = Changeling::Models::Logling.new(self)
15
+
16
+ if defined?(Sidekiq)
17
+ Changeling::Async::SidekiqWorker.perform_async(logling.to_indexed_json)
18
+ elsif defined?(Resque)
19
+ Resque.enqueue(Changeling::Async::ResqueWorker, logling.to_indexed_json)
20
+ end
21
+ end
22
+
23
+ true
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ module Changeling
2
+ module Exceptions
3
+ class AsyncGemRequired < StandardError
4
+ def to_s
5
+ "Changeling's asynchronous features require either the Sidekiq (https://github.com/mperham/sidekiq) or Resque (https://github.com/defunkt/resque) background job processing gems. Please install either one and try again."
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,15 +1,18 @@
1
1
  module Changeling
2
2
  module Probeling
3
- def all_history
3
+ def all_loglings
4
4
  Changeling::Models::Logling.records_for(self)
5
5
  end
6
+ alias_method :all_history, :all_loglings
6
7
 
7
- def history(records = 10)
8
+ def loglings(records = 10)
8
9
  Changeling::Models::Logling.records_for(self, records.to_i)
9
10
  end
11
+ alias_method :history, :loglings
10
12
 
11
- def history_for_field(field_name, records = nil)
13
+ def loglings_for_field(field_name, records = nil)
12
14
  Changeling::Models::Logling.records_for(self, records ? records.to_i : nil, field_name.to_s)
13
15
  end
16
+ alias_method :history_for_field, :loglings_for_field
14
17
  end
15
18
  end
@@ -8,6 +8,8 @@ module Changeling
8
8
  if self.changes && !self.changes.empty?
9
9
  logling = Changeling::Models::Logling.create(self)
10
10
  end
11
+
12
+ true
11
13
  end
12
14
  end
13
15
  end
@@ -1,3 +1,3 @@
1
1
  module Changeling
2
- VERSION = "0.0.7"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -0,0 +1,79 @@
1
+ require 'spec_helper'
2
+
3
+ describe RailsApp, "Testing Blameling Integration" do
4
+ context "Controller without Blameling" do
5
+ controller(RailsApp::BlogPostsController) do
6
+ extend(RSpec::Rails::ControllerExampleGroup::BypassRescue)
7
+ end
8
+
9
+ before(:each) do
10
+ # Request needs to be setup to avoid path setting error
11
+ @request = ActionController::TestRequest.new
12
+ end
13
+
14
+ it "should not set current_user" do
15
+ Thread.new {
16
+ post :create
17
+ # Look in application.rb for the User class and it's id method.
18
+ BlogPost.last.all_loglings.last.modified_by.should == nil
19
+ }.join
20
+ end
21
+ end
22
+
23
+ context "Controller with Blameling" do
24
+ controller(RailsApp::BlamelingController) do
25
+ extend(RSpec::Rails::ControllerExampleGroup::BypassRescue)
26
+ end
27
+
28
+ before(:each) do
29
+ # Request needs to be setup to avoid path setting error
30
+ @request = ActionController::TestRequest.new
31
+ end
32
+
33
+ it "should set current_user" do
34
+ Thread.new {
35
+ post :create
36
+ # Look in application.rb for the User class and it's id method.
37
+ BlogPost.last.all_loglings.last.modified_by.should == 33
38
+ }.join
39
+ end
40
+ end
41
+
42
+ context "Controller with undefined current_user method" do
43
+ controller(RailsApp::NoCurrentUserController) do
44
+ extend(RSpec::Rails::ControllerExampleGroup::BypassRescue)
45
+ end
46
+
47
+ before(:each) do
48
+ # Request needs to be setup to avoid path setting error
49
+ @request = ActionController::TestRequest.new
50
+ end
51
+
52
+ it "should not set current_user, nor should it error out" do
53
+ Thread.new {
54
+ post :create
55
+ # Look in application.rb for the User class and it's id method.
56
+ BlogPost.last.all_loglings.last.modified_by.should == nil
57
+ }.join
58
+ end
59
+ end
60
+
61
+ context "Controller with a different overridden 'changeling_blame_user' method" do
62
+ controller(RailsApp::CurrentAccountController) do
63
+ extend(RSpec::Rails::ControllerExampleGroup::BypassRescue)
64
+ end
65
+
66
+ before(:each) do
67
+ # Request needs to be setup to avoid path setting error
68
+ @request = ActionController::TestRequest.new
69
+ end
70
+
71
+ it "should not set current_user if current_user is not defined" do
72
+ Thread.new {
73
+ post :create
74
+ # Look in application.rb for the User class and it's id method.
75
+ BlogPost.last.all_loglings.last.modified_by.should == 88
76
+ }.join
77
+ end
78
+ end
79
+ end