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,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