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,193 @@
1
+ require 'dm-core'
2
+ require 'dm-types'
3
+
4
+ module Redcrumbs
5
+ module SerializableAssociation
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+
9
+ base.class_eval do
10
+ include DataMapper::Resource unless self < DataMapper::Resource
11
+ end
12
+ end
13
+
14
+ module ClassMethods
15
+ def serializable_association(name)
16
+ raise ArgumentError unless name and [:creator, :target, :subject].include?(name)
17
+
18
+ property "stored_#{name}".to_sym, DataMapper::Property::Json, :lazy => false
19
+ property "#{name}_id".to_sym, DataMapper::Property::Integer, :index => true, :lazy => false
20
+ property "#{name}_type".to_sym, DataMapper::Property::String, :index => true, :lazy => false
21
+
22
+ define_setter_for(name)
23
+ define_getter_for(name)
24
+ define_loader_for(name)
25
+ define_load_state_getter(name)
26
+
27
+ self
28
+ end
29
+
30
+ private
31
+
32
+ # Define a setter, e.g. object.creator=
33
+ #
34
+ def define_setter_for(name)
35
+ define_method("#{name}=") do |associated|
36
+ instance_variable_set("@#{name}".to_sym, associated)
37
+
38
+ assign_id_for(name, associated)
39
+ assign_type_for(name, associated)
40
+ assign_serialized_attributes(name, associated)
41
+ end
42
+ end
43
+
44
+
45
+ # Define a getter, e.g. object.creator
46
+ #
47
+ def define_getter_for(name)
48
+ define_method("#{name}") do
49
+ instance_variable_get("@#{name}") or
50
+ instance_variable_set("@#{name}", deserialize(name)) or
51
+ instance_variable_set("@#{name}", load_associated(name))
52
+ end
53
+ end
54
+
55
+
56
+ # Define method to force a load of the association from
57
+ # the database or return it if already loaded.
58
+ #
59
+ def define_loader_for(name)
60
+ define_method("full_#{name}") do
61
+ if send("has_loaded_#{name}?")
62
+ instance_variable_get("@#{name}")
63
+ else
64
+ instance_variable_set("@#{name}_load_state", true)
65
+ instance_variable_set("@#{name}", load_associated(name))
66
+ end
67
+ end
68
+ end
69
+
70
+
71
+ # Define method to check if association has been fully loaded
72
+ # from the database.
73
+ #
74
+ def define_load_state_getter(name)
75
+ instance_variable_set("@#{name}_load_state", false)
76
+
77
+ define_method("has_loaded_#{name}?") do
78
+ instance_variable_get("@#{name}_load_state")
79
+ end
80
+ end
81
+ end
82
+
83
+
84
+ # Load the association from the database.
85
+ #
86
+ def load_associated(name)
87
+ return nil unless association_id = send("#{name}_id")
88
+
89
+ class_name = send("#{name}_type") || config_class_name_for(name)
90
+ klass = class_name.classify.constantize
91
+
92
+ primary_key = config_primary_key_for(name) || klass.primary_key
93
+
94
+ klass.where(primary_key => association_id).first
95
+ end
96
+
97
+ private
98
+
99
+ # Assign the association id based on default primary key
100
+ #
101
+ def assign_id_for(name, associated)
102
+ id = if associated
103
+ primary_key = config_primary_key_for(name) or associated.class.primary_key
104
+ associated[primary_key]
105
+ end
106
+
107
+ send("#{name}_id=", id)
108
+ end
109
+
110
+
111
+ # Assign the association type based on default primary key
112
+ #
113
+ def assign_type_for(name, associated)
114
+ type = associated ? associated.class.name : nil
115
+
116
+ send("#{name}_type=", type)
117
+ end
118
+
119
+
120
+ # Serialize and assign the association
121
+ #
122
+ def assign_serialized_attributes(name, associated)
123
+ serialized = associated ? serialize(name, associated) : {}
124
+
125
+ send("stored_#{name}=", serialized)
126
+ end
127
+
128
+
129
+ # Get the class name from the config options, e.g.
130
+ # Redcrumbs.creator_class_sym
131
+ #
132
+ def config_class_name_for(name)
133
+ Redcrumbs.send("#{name}_class_sym").to_s
134
+ end
135
+
136
+
137
+ # Get the expected primary key for the association from
138
+ # the config options.
139
+ #
140
+ def config_primary_key_for(name)
141
+ Redcrumbs.send("#{name}_primary_key")
142
+ rescue NoMethodError
143
+ nil
144
+ end
145
+
146
+ # Serializes a given object by looking for its configuration options
147
+ # or calling serialization method.
148
+ #
149
+ def serialize(name, associated)
150
+ if name == :subject
151
+ associated.serialized_as_redcrumbs_subject
152
+ else
153
+ keys = Redcrumbs.send("store_#{name}_attributes").dup
154
+
155
+ associated.attributes.select {|k,v| keys.include?(k.to_sym)}
156
+ end
157
+ end
158
+
159
+
160
+ # Returns a new instance of the associated object based on the
161
+ # serialized attributes only.
162
+ #
163
+ def deserialize(name)
164
+ properties = send("stored_#{name}")
165
+ associated_id = send("#{name}_id")
166
+
167
+ return nil unless properties.present? and associated_id
168
+
169
+ class_name = send("#{name}_type")
170
+ class_name ||= config_class_name_for(name) unless name == :subject
171
+
172
+ instantiate_with_id(class_name, properties, associated_id)
173
+ end
174
+
175
+
176
+ # Return a properties hash that corresponds to the given class's
177
+ # column names.
178
+ #
179
+ def clean_properties(klass, properties)
180
+ properties.select {|k,v| klass.column_names.include?(k.to_s)}
181
+ end
182
+
183
+
184
+ def instantiate_with_id(class_name, properties, associated_id)
185
+ klass = class_name.classify.constantize
186
+ properties = clean_properties(klass, properties)
187
+
188
+ associated = klass.new(properties, :without_protection => true)
189
+ associated.id = associated_id
190
+ associated
191
+ end
192
+ end
193
+ end
@@ -1,45 +1,35 @@
1
1
  module Redcrumbs
2
2
  # Provides methods for giving user context to crumbs. Retrieves crumbs created by a user (creator) or
3
3
  # affecting a user (target)
4
- module Users
4
+ module Users
5
5
  extend ActiveSupport::Concern
6
-
7
- module InstanceMethods
8
- # Retrieves crumbs related to the user
9
- def crumbs_for
10
- crumb_or_custom_class.all(:target_id => self[Redcrumbs.target_primary_key], :order => [:created_at.desc])
11
- end
12
6
 
13
- # Retrieves crumbs created by the user
14
- def crumbs_by
15
- crumb_or_custom_class.all(:creator_id => self[Redcrumbs.creator_primary_key], :order => [:created_at.desc])
16
- end
17
-
18
- # A limitable collection of both crumbs_for and crumbs_by
19
- # This is an unforunate hack to get over the redis dm adapter's non-support of addition (OR) queries
20
- def crumbs_as_user(opts = {})
21
- opts[:limit] ||= 100
22
- arr = crumbs_for
23
- arr += crumbs_by
24
- arr.all(opts)
25
- end
26
-
27
- # Creator method defines who should be considered the creator when a model is updated. This
28
- # can be overridden in the redcrumbed model to define who the creator should be. Defaults
29
- # to the current user (or creator class) associated with the model.
30
- def creator
31
- send(Redcrumbs.creator_class_sym) if respond_to?(Redcrumbs.creator_class_sym)
32
- end
33
-
34
- private
35
-
36
- def crumb_or_custom_class
37
- if self.class.redcrumbs_options[:class_name]
38
- self.class.redcrumbs_options[:class_name].to_s.capitalize.constantize
39
- else
40
- Crumb
41
- end
42
- end
7
+ # Retrieves crumbs related to the user
8
+ #
9
+ def crumbs_for(opts = {})
10
+ klass = Redcrumbs.crumb_class
11
+
12
+ klass.targetted_by(self).all(opts)
13
+ end
14
+
15
+ # Retrieves crumbs created by the user
16
+ #
17
+ def crumbs_by(opts = {})
18
+ klass = Redcrumbs.crumb_class
19
+
20
+ klass.created_by(self).all(opts)
21
+ end
22
+
23
+ # Or queries don't seem to be working with dm-redis-adapter. This
24
+ # is a temporary workaround.
25
+ #
26
+ def crumbs_as_user(opts = {})
27
+ opts[:limit] ||= 100
28
+
29
+ arr = crumbs_by.to_a + crumbs_for.to_a
30
+ arr.uniq!
31
+
32
+ arr.sort_by! {|c| [c.created_at, c.id]}.reverse
43
33
  end
44
34
  end
45
35
  end
@@ -1,3 +1,3 @@
1
1
  module Redcrumbs
2
- VERSION = "0.5.0"
2
+ VERSION = "0.5.1"
3
3
  end
@@ -6,20 +6,22 @@ Gem::Specification.new do |s|
6
6
  s.name = "redcrumbs"
7
7
  s.version = Redcrumbs::VERSION
8
8
  s.authors = ["John Hope"]
9
- s.email = ["info@midhirrecords.com"]
10
- s.homepage = "https://github.com/projectzebra/Redcrumbs"
9
+ s.email = ["john@shiftdock.com"]
10
+ s.homepage = "https://github.com/JonMidhir/Redcrumbs"
11
11
  s.summary = %q{Fast and unobtrusive activity tracking of ActiveRecord models using DataMapper and Redis}
12
12
  s.description = %q{Fast and unobtrusive activity tracking of ActiveRecord models using DataMapper and Redis}
13
+ s.license = 'MIT'
13
14
 
14
15
  s.rubyforge_project = "redcrumbs"
16
+
17
+ s.add_dependency 'data_mapper', '>= 1.2.0'
18
+ s.add_dependency 'redis', '>= 2.2.2'
19
+ s.add_dependency 'dm-redis-adapter', '>= 0.6.2'
20
+ s.add_dependency 'redis-namespace', '>= 1.3.0'
21
+ s.add_dependency 'activerecord', '>= 3.1', '< 5'
22
+ s.add_dependency 'activesupport', '>= 3.1', '< 5'
15
23
 
16
24
  s.files = `git ls-files`.split("\n")
17
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
- s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
25
+ s.test_files = s.files.grep(/^spec/)
19
26
  s.require_paths = ["lib"]
20
-
21
- s.add_dependency 'data_mapper', '>= 1.2.0'
22
- s.add_dependency 'dm-redis-adapter', '~> 0.6.2'
23
- s.add_dependency 'redis', '~> 2.2.2'
24
- s.add_dependency 'redis-namespace', '>= 1.1.0'
25
27
  end
@@ -0,0 +1,168 @@
1
+ require 'spec_helper'
2
+
3
+ describe Redcrumbs do
4
+ describe '.creator_class_sym' do
5
+ subject { Redcrumbs.creator_class_sym }
6
+
7
+ context 'when unchanged' do
8
+ it { is_expected.to eq(:user) }
9
+ end
10
+
11
+ context 'when changed to :game' do
12
+ before { Redcrumbs.creator_class_sym = :game }
13
+ after { Redcrumbs.creator_class_sym = :user }
14
+
15
+ it { is_expected.to eq(:game)}
16
+ end
17
+ end
18
+
19
+
20
+ describe '.creator_primary_key' do
21
+ subject { Redcrumbs.creator_primary_key }
22
+
23
+ context 'when unchanged' do
24
+ it { is_expected.to eq('id') }
25
+ end
26
+
27
+ context 'when changed to :name' do
28
+ before { Redcrumbs.creator_primary_key = :name }
29
+ after { Redcrumbs.creator_primary_key = :id }
30
+
31
+ it { is_expected.to eq(:name)}
32
+ end
33
+ end
34
+
35
+
36
+ describe '.target_class_sym' do
37
+ subject { Redcrumbs.target_class_sym }
38
+
39
+ context 'when unchanged' do
40
+ it { is_expected.to eq(:user) }
41
+ end
42
+
43
+ context 'when changed to :game' do
44
+ before { Redcrumbs.target_class_sym = :game }
45
+ after { Redcrumbs.target_class_sym = :user }
46
+
47
+ it { is_expected.to eq(:game)}
48
+ end
49
+ end
50
+
51
+
52
+ describe '.target_primary_key' do
53
+ subject { Redcrumbs.target_primary_key }
54
+
55
+ context 'when unchanged' do
56
+ it { is_expected.to eq('id') }
57
+ end
58
+
59
+ context 'when changed to :name' do
60
+ before { Redcrumbs.target_primary_key = :name }
61
+ after { Redcrumbs.target_primary_key = :id }
62
+
63
+ it { is_expected.to eq(:name)}
64
+ end
65
+ end
66
+
67
+
68
+ describe '.store_creator_attributes' do
69
+ subject { Redcrumbs.store_creator_attributes }
70
+
71
+ context 'when unchanged' do
72
+ it { is_expected.to eq([]) }
73
+ end
74
+
75
+ context 'when given attribute keys' do
76
+ before { Redcrumbs.store_creator_attributes = [:id, :name] }
77
+ after { Redcrumbs.store_creator_attributes = [] }
78
+
79
+ it { is_expected.to eq([:id, :name])}
80
+ end
81
+ end
82
+
83
+
84
+ describe '.store_target_attributes' do
85
+ subject { Redcrumbs.store_target_attributes }
86
+
87
+ context 'when unchanged' do
88
+ it { is_expected.to eq([]) }
89
+ end
90
+
91
+ context 'when given attribute keys' do
92
+ before { Redcrumbs.store_target_attributes = [:id, :name] }
93
+ after { Redcrumbs.store_target_attributes = [] }
94
+
95
+ it { is_expected.to eq([:id, :name])}
96
+ end
97
+ end
98
+
99
+
100
+ describe '.redis' do
101
+ let!(:default_redis) { Redcrumbs.redis }
102
+ subject { Redcrumbs.redis }
103
+
104
+ context 'when given a URL string with port and scheme' do
105
+ before { Redcrumbs.redis = 'redis://localhost:6379' }
106
+ after { Redcrumbs.redis = default_redis }
107
+
108
+ it { expect(subject.namespace).to eq(:redcrumbs) }
109
+ it { expect(subject.client.host).to eq('localhost') }
110
+ it { expect(subject.client.port).to eq(6379) }
111
+ end
112
+
113
+ context 'when given a URL string without scheme' do
114
+ before { Redcrumbs.redis = 'localhost:6379' }
115
+ after { Redcrumbs.redis = default_redis }
116
+
117
+ it { expect(subject.namespace).to eq(:redcrumbs) }
118
+ it { expect(subject.client.host).to eq('localhost') }
119
+ it { expect(subject.client.port).to eq(6379) }
120
+ end
121
+
122
+ context 'when given a URL string with namespace' do
123
+ before { Redcrumbs.redis = 'localhost:6379/some_namespace' }
124
+ after { Redcrumbs.redis = default_redis }
125
+
126
+ it { expect(subject.namespace).to eq('some_namespace') }
127
+ end
128
+
129
+ context 'when given an existing redis client' do
130
+ let(:redis) { Redis.new }
131
+ before { Redcrumbs.redis = redis }
132
+ after { Redcrumbs.redis = default_redis }
133
+
134
+ it { expect(subject.redis).to eq(redis) }
135
+ end
136
+
137
+ context 'when given an existing redis namespace' do
138
+ let(:redis) { Redis::Namespace.new('some_namespace') }
139
+ before { Redcrumbs.redis = redis }
140
+ after { Redcrumbs.redis = default_redis }
141
+
142
+ it { is_expected.to eq(redis) }
143
+ end
144
+ end
145
+
146
+
147
+ describe '.crumb_class' do
148
+ subject { Redcrumbs.crumb_class }
149
+
150
+ context 'when class_name unchanged' do
151
+ it { is_expected.to be(Redcrumbs::Crumb) }
152
+ end
153
+
154
+ context 'when class_name set to unknown class' do
155
+ before { Redcrumbs.class_name = :foo }
156
+ after { Redcrumbs.class_name = nil }
157
+
158
+ it { is_expected.to be(Redcrumbs::Crumb) }
159
+ end
160
+
161
+ context 'when class doesnt inherit from Crumb' do
162
+ before { Redcrumbs.class_name = :game }
163
+ after { Redcrumbs.class_name = nil }
164
+
165
+ it { expect { subject }.to raise_error(ArgumentError) }
166
+ end
167
+ end
168
+ end