redcrumbs 0.5.0 → 0.5.1

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.
@@ -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