redcrumbs 0.5.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', '~> 4.0'
4
+ gem 'activesupport', '~> 4.0'
5
+
6
+ gemspec :path => '..'
7
+
8
+ group :test do
9
+ gem 'codeclimate-test-reporter', :group => :test, :require => nil
10
+ gem 'rspec', '~> 3.0'
11
+ gem 'sqlite3', '~> 1.0'
12
+ end
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activerecord', '~> 4.1'
4
+ gem 'activesupport', '~> 4.1'
5
+
6
+ gemspec :path => '..'
7
+
8
+ group :test do
9
+ gem 'codeclimate-test-reporter', :group => :test, :require => nil
10
+ gem 'rspec', '~> 3.0'
11
+ gem 'sqlite3', '~> 1.0'
12
+ end
@@ -1,34 +1,17 @@
1
1
  Redcrumbs.setup do |config|
2
- # When a change is made to a redcrumbed model the user associated with that model will
3
- # automatically be stored on the Crumb object. By default Redcrumbs will look for a User
4
- # object using 'id' as the primary key but you can override that here.
2
+ # If your activity feeds are user-based you can store creator and target
3
+ # attributes on the crumb object to avoid having to touch your main database
4
+ # at all. Keep it sensible and evaluate whether the additional space used in
5
+ # Redis is really worth the time saving.
6
+ # Note: You don't need to store the object id, it is already stored.
5
7
  #
6
- # config.creator_class_sym = :user
7
- # config.creator_primary_key = 'id'
8
- # config.target_class_sym = :user
9
- # config.target_primary_key = 'id'
10
- #
11
- #
12
- # If you're using the crumbs to report news back to a user you can store creator and target
13
- # attributes on the crumb object to avoid having to touch your main database at all. Keep it
14
- # sensible and evaluate whether the additional space used in Redis is really worth the time saving.
15
- #
16
- # config.store_creator_attributes = [:id, :name, :email]
17
- # config.store_target_attributes = [:id, :name, :email]
8
+ # config.store_creator_attributes = [:name, :email]
9
+ # config.store_target_attributes = [:name, :email]
18
10
  #
19
11
  #
20
12
  # Set the mortality to make crumbs automatically expire in time. Default is infinity.
21
13
  # config.mortality = 30.days
22
14
  end
23
15
 
24
- # Point this to your redis connection. It can be a Redis object or a string.
25
- Redcrumbs.redis = 'localhost:6379'
26
-
27
- # You may want to create a config/redcrumbs.yml file and base your redis setting
28
- # from that. e.g. (thanks to Resque for this):
29
-
30
- # rails_root = ENV['RAILS_ROOT'] || File.dirname(__FILE__) + '/../..'
31
- # rails_env = ENV['RAILS_ENV'] || 'development'
32
- #
33
- # redcrumbs_config = YAML.load_file(rails_root + '/config/redcrumbs.yml')
34
- # Redcrumbs.redis = redcrumbs_config[rails_env]
16
+ # Point this to your redis connection. It can be a Redis client, namespace or a URL string.
17
+ Redcrumbs.redis = 'localhost:6379'
@@ -1,24 +1,28 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
1
3
  require 'active_support/concern'
2
4
  require 'active_support/dependencies/autoload'
3
- require "redcrumbs/version"
4
- require 'redcrumbs/engine'
5
+ require 'active_record'
6
+ require 'redcrumbs/version'
5
7
  require 'redcrumbs/config'
6
8
  require 'redis'
7
9
  require 'redis-namespace'
8
10
  require 'dm-core'
9
11
 
10
- # Redcrumbs implements dirty models to track changes to ActiveRecord models in a way that is fast and
12
+ # Redcrumbs uses `dirty attributes` to track and store changes to ActiveRecord models in a way that is fast and
11
13
  # unobtrusive. By storing the data in Redis instead of a SQL database the footprint is greatly reduced, no
12
14
  # schema changes are necessary and we can harness all the advantages of a key value store; such as key expiry.
13
15
  #
14
16
  # Author:: John Hope
15
- # Copyright:: Copyright (c) 2012 John Hope for Project Zebra
17
+ # Copyright:: Copyright (c) 2014 John Hope for Project Zebra
16
18
  # License:: MIT License (http://www.opensource.org/licenses/mit-license.php)
17
19
  #
18
20
  # To start tracking a model use the 'redcrumbed' method:
19
21
  #
20
22
  # class Venue
21
23
  # redcrumbed, :only => [:name, :latlng]
24
+ #
25
+ #  has_one :creator, :class_name => 'User'
22
26
  # end
23
27
  #
24
28
  # venue = Venue.last
@@ -38,6 +42,10 @@ module Redcrumbs
38
42
  autoload :Options
39
43
  autoload :Users
40
44
  autoload :Creation
45
+ autoload :Crumb
46
+
47
+ include Options
48
+ include Users
41
49
 
42
50
  def self.setup
43
51
  yield self
@@ -49,9 +57,6 @@ module Redcrumbs
49
57
 
50
58
  module ClassMethods
51
59
  def redcrumbed(options = {})
52
-
53
- include Options
54
- include Users
55
60
  include Creation
56
61
 
57
62
  prepare_redcrumbed_options(options)
@@ -10,14 +10,32 @@ module Redcrumbs
10
10
  mattr_accessor :mortality
11
11
  mattr_accessor :redis
12
12
 
13
+ mattr_accessor :class_name
14
+
15
+
16
+ # This should only be used to load old crumbs from previous versions
17
+ # of the gem, in future require an explicit creator/target method to set.
18
+ #
13
19
  @@creator_class_sym ||= :user
14
20
  @@creator_primary_key ||= 'id'
15
- @@target_class_sym ||= :user
21
+ @@target_class_sym ||= :user
16
22
  @@target_primary_key ||= 'id'
17
23
 
18
24
  @@store_creator_attributes ||= []
19
25
  @@store_target_attributes ||= []
26
+
27
+
28
+ # Constantises the class_name attribute, falls back to the Crumb default.
29
+ #
30
+ def self.crumb_class
31
+ if @@class_name and @@class_name.length > 0
32
+ constantize_class_name
33
+ else
34
+ Crumb
35
+ end
36
+ end
20
37
 
38
+
21
39
  # Stolen from resque. Thanks!
22
40
  # Accepts:
23
41
  # 1. A 'hostname:port' String
@@ -26,6 +44,7 @@ module Redcrumbs
26
44
  # 4. A Redis URL String 'redis://host:port'
27
45
  # 5. An instance of `Redis`, `Redis::Client`, `Redis::DistRedis`,
28
46
  # or `Redis::Namespace`.
47
+ #
29
48
  def self.redis=(server)
30
49
  case server
31
50
  when String
@@ -45,12 +64,43 @@ module Redcrumbs
45
64
  else
46
65
  @@redis = Redis::Namespace.new(:redcrumbs, :redis => server)
47
66
  end
67
+
68
+ setup_datamapper!
69
+
48
70
  @@redis
49
71
  end
50
72
 
51
- def self.redis
52
- return @@redis if @@redis
53
- @@redis = Redis.respond_to?(:connect) ? Redis.connect : "localhost:6379"
54
- @@redis
73
+ private
74
+
75
+ # Note: Since it's not possible to access the exact connection the DataMapper adapter
76
+ # uses we have to use the @@redis module variable and make sure it's consistent.
77
+ #
78
+ def self.setup_datamapper!
79
+ adapter = DataMapper.setup(:default,
80
+ { :adapter => "redis",
81
+ :host => self.redis.client.host,
82
+ :port => self.redis.client.port,
83
+ :password => self.redis.client.password
84
+ })
85
+
86
+ # For supporting namespaces:
87
+ #
88
+ adapter.resource_naming_convention = lambda do |value|
89
+ inflected_value = DataMapper::Inflector.pluralize(DataMapper::Inflector.underscore(value)).gsub('/', '_')
90
+
91
+ "#{self.redis.namespace}:#{inflected_value}"
92
+ end
93
+ end
94
+
95
+ def self.constantize_class_name
96
+ klass = @@class_name.to_s.classify.constantize
97
+
98
+ unless klass < Redcrumbs::Crumb
99
+ raise ArgumentError, 'Redcrumbs crumb_class must inherit from Redcrumbs::Crumb'
100
+ end
101
+
102
+ klass
103
+ rescue NameError
104
+ Crumb
55
105
  end
56
106
  end
@@ -2,57 +2,70 @@ module Redcrumbs
2
2
  module Creation
3
3
  extend ActiveSupport::Concern
4
4
 
5
- module InstanceMethods
6
- def crumbs
7
- Crumb.all(:subject_type => self.class.to_s, :subject_id => self.id)
8
- end
5
+ def crumbs
6
+ Redcrumbs.crumb_class.all(
7
+ :subject_type => self.class.to_s,
8
+ :subject_id => self.id
9
+ )
10
+ end
9
11
 
10
- def watched_changes
11
- changes.slice(*self.class.redcrumbs_options[:only])
12
- end
12
+ def watched_changes
13
+ changes.slice(*self.class.redcrumbs_options[:only])
14
+ end
13
15
 
14
- def storeable_attributes
15
- store = self.class.redcrumbs_options[:store]
16
- if store.has_key?(:only)
17
- attributes.reject {|k,v| !store[:only].include?(k.to_sym)}
18
- elsif store.has_key?(:except)
19
- attributes.reject {|k,v| store[:except].include?(k.to_sym)}
20
- else
21
- {}
22
- end
23
- end
24
-
25
- def attributes_from_storeable_methods
26
- store = self.class.redcrumbs_options[:store]
27
- if store.has_key?(:methods)
28
- # get the methods that actually exist on the model
29
- methods = methods_from_array(store[:methods])
30
- # inject them into a hash with their outcomes as values
31
- methods.inject({}) {|h,a| h.merge(a => send(a))}
32
- else
33
- {}
34
- end
35
- end
36
-
37
- def storeable_attributes_and_method_attributes
38
- storeable_attributes.merge(attributes_from_storeable_methods)
39
- end
16
+ def storable_attributes_keys
17
+ store = self.class.redcrumbs_options[:store]
40
18
 
41
- def create_crumb
42
- n = Crumb.build_with_modifications(self)
43
- n.save
44
- end
45
-
46
- # This is called after the record is saved to store the changes on the model, including anything done in before_save validations
47
- def notify_changes
48
- create_crumb unless watched_changes.empty?
49
- end
50
-
51
- private
52
-
53
- def methods_from_array(array)
54
- self.class.instance_methods.select {|method| array.include?(method.to_sym)}
19
+ store[:only] or
20
+ symbolized_attribute_keys(store[:except]) or
21
+ []
22
+ end
23
+
24
+ def storeable_attributes
25
+ attributes.slice *storable_attributes_keys.map(&:to_s)
26
+ end
27
+
28
+ def storable_methods_names
29
+ store = self.class.redcrumbs_options[:store]
30
+
31
+ if store[:methods]
32
+ methods.select {|method| store[:methods].include?(method.to_sym)}
33
+ else
34
+ []
55
35
  end
56
36
  end
37
+
38
+ # Todo: Fix inconsistent naming; storable vs storeable
39
+ def storable_methods
40
+ storable_methods_names.inject({}) {|h, n| h.merge(n.to_s => send(n))}
41
+ end
42
+
43
+ def serialized_as_redcrumbs_subject
44
+ storeable_attributes.merge(storable_methods)
45
+ end
46
+
47
+ def create_crumb
48
+ n = Redcrumbs.crumb_class.build_with_modifications(self)
49
+ n.save
50
+ n
51
+ end
52
+
53
+ # This is called after the record is saved to store the changes on the model, including anything done in before_save validations
54
+ def notify_changes
55
+ create_crumb unless watched_changes.empty?
56
+ end
57
+
58
+ private
59
+
60
+ def symbolized_attribute_keys(except = [])
61
+ return nil unless except
62
+
63
+ symbolized_attribute_keys = attributes.dup.symbolize_keys!.keys
64
+ symbolized_attribute_keys.reject {|key| except.include?(key)}
65
+ end
66
+
67
+ def methods_from_array(array)
68
+ self.class.instance_methods.select {|method| array.include?(method.to_sym)}
69
+ end
57
70
  end
58
71
  end
@@ -0,0 +1,100 @@
1
+ require 'dm-core'
2
+ require 'dm-types'
3
+ require 'dm-timestamps'
4
+ require 'dm-redis-adapter'
5
+ require 'redcrumbs/serializable_association'
6
+
7
+ module Redcrumbs
8
+ class Crumb
9
+
10
+ include DataMapper::Resource
11
+ include Redcrumbs::SerializableAssociation
12
+
13
+ property :id, Serial
14
+ property :modifications, Json, :default => "{}", :lazy => false
15
+ property :created_at, DateTime
16
+ property :updated_at, DateTime
17
+
18
+ DataMapper.finalize
19
+
20
+ after :save, :set_mortality
21
+
22
+ serializable_association :creator
23
+ serializable_association :target
24
+ serializable_association :subject
25
+
26
+ def initialize(params = {})
27
+ self.subject = params[:subject]
28
+ self.modifications = params[:modifications]
29
+ end
30
+
31
+ def self.build_with_modifications(subject)
32
+ return if subject.watched_changes.empty?
33
+
34
+ new(:modifications => subject.watched_changes, :subject => subject)
35
+ end
36
+
37
+ def self.created_by(creator)
38
+ all(:creator_id => creator[Redcrumbs.creator_primary_key]) &
39
+ all(:creator_type => creator.class.name)
40
+ end
41
+
42
+ def self.targetted_by(target)
43
+ all(:target_id => target[Redcrumbs.target_primary_key]) &
44
+ all(:target_type => target.class.name)
45
+ end
46
+
47
+ # Overrides the subject setter created by the SerializableAttributes
48
+ # module.
49
+ #
50
+ def subject=(subject)
51
+ @subject = subject
52
+
53
+ self.stored_subject = subject ? serialize(:subject, subject) : {}
54
+ self.subject_id = subject ? subject.id : nil
55
+ assign_type_for(:subject, subject)
56
+
57
+ self.target = subject.target if subject.respond_to?(:target)
58
+ self.creator = subject.creator if subject.respond_to?(:creator)
59
+
60
+ subject
61
+ end
62
+
63
+ def redis_key
64
+ "redcrumbs_crumbs:#{id}" if id
65
+ end
66
+
67
+ # Designed to mimic ActiveRecord's count. Probably not performant and only should be used for tests really
68
+ def self.count
69
+ Redcrumbs.redis.keys("redcrumbs_crumbs:*").size - 8
70
+ end
71
+
72
+ # Expiry
73
+
74
+ def mortal?
75
+ return false if new?
76
+
77
+ time_to_live >= 0
78
+ end
79
+
80
+ def time_to_live
81
+ return nil if new?
82
+
83
+ @ttl ||= Redcrumbs.redis.ttl(redis_key)
84
+ end
85
+
86
+ def expires_at
87
+ Time.now + time_to_live if time_to_live
88
+ end
89
+
90
+ private
91
+
92
+ def set_mortality
93
+ Redcrumbs.redis.expireat(redis_key, expire_from_now.to_i) if Redcrumbs.mortality
94
+ end
95
+
96
+ def expire_from_now
97
+ Time.now + Redcrumbs.mortality
98
+ end
99
+ end
100
+ end
@@ -6,6 +6,8 @@ module Redcrumbs
6
6
  # prepare_redcrumbed_options prepares class level options that customise the behaviour of
7
7
  # redcrumbed. See documentation for a full explanation of redcrumbed options.
8
8
  def prepare_redcrumbed_options(options)
9
+ options.symbolize_keys!
10
+
9
11
  defaults = {
10
12
  :only => [],
11
13
  :store => {}
@@ -14,13 +16,16 @@ module Redcrumbs
14
16
  options.reverse_merge!(defaults)
15
17
 
16
18
  options[:only] = Array(options[:only])
17
- options[:store] = options[:store]
18
19
 
19
20
  class_attribute :redcrumbs_options
20
- class_attribute :redcrumbs_callback_options
21
21
 
22
22
  self.redcrumbs_options = options.dup
23
- self.redcrumbs_callback_options = options.dup.select {|k,v| [:if, :unless].include?(k.to_sym)}
23
+
24
+ options
25
+ end
26
+
27
+ def redcrumbs_callback_options
28
+ redcrumbs_options.slice(:if, :unless)
24
29
  end
25
30
  end
26
31
  end