active_record-events 2.0.0 → 4.0.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.
- checksums.yaml +5 -13
- data/{MIT-LICENSE → LICENSE} +0 -0
- data/README.md +139 -12
- data/Rakefile +27 -3
- data/lib/active_record/events.rb +6 -32
- data/lib/active_record/events/extension.rb +19 -0
- data/lib/active_record/events/macro.rb +36 -0
- data/lib/active_record/events/method_factory.rb +88 -0
- data/lib/active_record/events/naming.rb +37 -6
- data/lib/active_record/events/version.rb +1 -1
- data/lib/generators/active_record/event/USAGE +15 -0
- data/lib/generators/active_record/event/event_generator.rb +60 -0
- data/spec/active_record/events/macro_spec.rb +49 -0
- data/spec/active_record/events/method_factory_spec.rb +59 -0
- data/spec/active_record/events/naming_spec.rb +64 -23
- data/spec/active_record/events_spec.rb +46 -10
- data/spec/dummy/app/models/task.rb +11 -0
- data/spec/dummy/config/boot.rb +4 -3
- data/spec/dummy/config/database.yml +3 -3
- data/spec/dummy/config/environment.rb +6 -4
- data/spec/dummy/db/migrate/20150813132804_create_tasks.rb +1 -1
- data/spec/dummy/db/schema.rb +5 -12
- data/spec/generators/active_record/event/event_generator_spec.rb +45 -0
- data/spec/spec_helper.rb +10 -10
- data/spec/support/{factories.rb → factories/task_factory.rb} +0 -1
- data/spec/support/helpers/generator_helpers.rb +8 -0
- data/spec/support/matchers/have_method.rb +5 -0
- metadata +107 -59
- data/spec/dummy/app/models/user.rb +0 -3
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/migrate/20170103215307_create_users.rb +0 -9
- data/spec/dummy/db/test.sqlite3 +0 -0
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
data.tar.gz: !binary |-
|
6
|
-
ZDljYzhkYTk1ZjI1ZjIyNWRlYmZmZTVhNDIzNDNhNjFiNmRlZjA5Ng==
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 18d2fc6a8788d3190b5da3dd8dc45fbf307bb55344a6072ba24db67ff9b0e6cb
|
4
|
+
data.tar.gz: 17767f7e00810c9717cc5fa4bd92d49303b22427eb5da06a3f27ea39f8a4fbf8
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
NDVhZjJlOGM2YzNlNGFhZjc2ZjgzNzMwN2Y1YzU5MjVmOTU3OTMyMzJjY2Zl
|
11
|
-
Y2NiZDU1ZTk4M2UxYjdlOTRjNzM1ZTUwZjMwMjQ0Njk1MWMzOTk=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
ODYzYjIyZTNjM2I2NDNhNTk0YWFmOTkxZGU4NjQwZjQ1YzdjODJjM2M4OTkz
|
14
|
-
YzUzZjZmMWM4ZWVhZTNmYWEwYzZkZjA0YzFjNDZhY2Q5MDVhOTk2ODk1ODA3
|
15
|
-
NjE2ZmI5NzdiYzcxN2IyOTFiNjU3NWJkNTA3OWFmYjIxMWMyNzY=
|
6
|
+
metadata.gz: d39a25b8c5695aa8c81b9780744f0639ab2b0bac74296486721ec1c28cad0c67f801441b78c3faa48162d60938297a33638dada9e597a84a7c8c888c94cc86b2
|
7
|
+
data.tar.gz: afb5395aba7821776ff9aa3790d0f04f625daa1a4de6495e9069cf210a4ea3023b7449bb6cce58e9eb158887a541da6f927b9ba03e61df0974b6cd473a6beacf
|
data/{MIT-LICENSE → LICENSE}
RENAMED
File without changes
|
data/README.md
CHANGED
@@ -1,7 +1,15 @@
|
|
1
|
-
# ActiveRecord::Events [](https://rubygems.org/gems/active_record-events) [](https://travis-ci.org/pienkowb/active_record-events) [](https://coveralls.io/github/pienkowb/active_record-events) [](https://codeclimate.com/github/pienkowb/active_record-events)
|
2
2
|
|
3
3
|
An ActiveRecord extension providing convenience methods for timestamp management.
|
4
4
|
|
5
|
+
## Screencast
|
6
|
+
|
7
|
+
<a href="https://www.youtube.com/watch?v=TIR7YDF3O-4">
|
8
|
+
<img src="https://img.youtube.com/vi/TIR7YDF3O-4/maxresdefault.jpg" title="ActiveRecord::Events - Awesome Ruby Gems" width="50%">
|
9
|
+
</a>
|
10
|
+
|
11
|
+
[Watch screencast](https://www.youtube.com/watch?v=TIR7YDF3O-4) (courtesy of [Mike Rogers](https://github.com/MikeRogers0))
|
12
|
+
|
5
13
|
## Installation
|
6
14
|
|
7
15
|
Add the following line to your application's Gemfile:
|
@@ -24,9 +32,12 @@ $ gem install active_record-events
|
|
24
32
|
|
25
33
|
## Usage
|
26
34
|
|
27
|
-
Recording a timestamp in order to mark that an event occurred to an object is a common practice when dealing with ActiveRecord models
|
35
|
+
Recording a timestamp in order to mark that an event occurred to an object is a common practice when dealing with ActiveRecord models.
|
36
|
+
A good example of such an approach is how ActiveRecord handles the `created_at` and `updated_at` fields.
|
28
37
|
This gem allows you to manage custom timestamp fields in the exact same manner.
|
29
38
|
|
39
|
+
### Example
|
40
|
+
|
30
41
|
Consider a `Task` model with a `completed_at` field and the following methods:
|
31
42
|
|
32
43
|
```ruby
|
@@ -35,17 +46,25 @@ class Task < ActiveRecord::Base
|
|
35
46
|
completed_at.present?
|
36
47
|
end
|
37
48
|
|
49
|
+
def not_completed?
|
50
|
+
!completed?
|
51
|
+
end
|
52
|
+
|
38
53
|
def complete
|
39
|
-
complete!
|
54
|
+
complete! if not_completed?
|
40
55
|
end
|
41
56
|
|
42
57
|
def complete!
|
43
58
|
touch(:completed_at)
|
44
59
|
end
|
60
|
+
|
61
|
+
def self.complete_all
|
62
|
+
touch_all(:completed_at)
|
63
|
+
end
|
45
64
|
end
|
46
65
|
```
|
47
66
|
|
48
|
-
Instead of defining these
|
67
|
+
Instead of defining all of these methods by hand, you can use the `has_event` macro provided by the gem.
|
49
68
|
|
50
69
|
```ruby
|
51
70
|
class Task < ActiveRecord::Base
|
@@ -53,12 +72,10 @@ class Task < ActiveRecord::Base
|
|
53
72
|
end
|
54
73
|
```
|
55
74
|
|
56
|
-
|
57
|
-
In such a case, many lines of code can be replaced with an expressive one-liner.
|
75
|
+
As a result, the methods will be generated automatically.
|
58
76
|
|
59
|
-
|
60
|
-
|
61
|
-
```
|
77
|
+
*It's important to note that the `completed_at` column has to already exist in the database.*
|
78
|
+
*Consider [using the generator](#using-a-rails-generator) to create a necessary migration.*
|
62
79
|
|
63
80
|
### Scopes
|
64
81
|
|
@@ -69,7 +86,40 @@ scope :not_completed, -> { where(completed_at: nil) }
|
|
69
86
|
scope :completed, -> { where.not(completed_at: nil) }
|
70
87
|
```
|
71
88
|
|
72
|
-
|
89
|
+
The inclusion of scope methods can be omitted by passing the `skip_scopes` flag.
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
has_event :complete, skip_scopes: true
|
93
|
+
```
|
94
|
+
|
95
|
+
### Multiple events
|
96
|
+
|
97
|
+
Using the macro is efficient when more than one field has to be handled that way.
|
98
|
+
In such a case, many lines of code can be replaced with an expressive one-liner.
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
has_events :complete, :archive
|
102
|
+
```
|
103
|
+
|
104
|
+
### Date fields
|
105
|
+
|
106
|
+
In case of date fields, which by convention have names ending with `_on` instead of `_at` (e.g. `completed_on`), the `field_type` option needs to be passed to the macro:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
has_event :complete, field_type: :date
|
110
|
+
```
|
111
|
+
|
112
|
+
### Custom field name
|
113
|
+
|
114
|
+
If there's a field with a name that doesn't follow the naming convention (i.e. does not end with `_at` or `_on`), you can pass it as the `field_name` option.
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
has_event :complete, field_name: :completion_time
|
118
|
+
```
|
119
|
+
|
120
|
+
Note that the `field_name` option takes precedence over the `field_type` option.
|
121
|
+
|
122
|
+
### Specifying an object
|
73
123
|
|
74
124
|
There are events which do not relate to a model itself but to one of its attributes – take the `User` model with the `email_confirmed_at` field as an example.
|
75
125
|
In order to keep method names grammatically correct, you can specify an object using the `object` option.
|
@@ -80,8 +130,85 @@ class User < ActiveRecord::Base
|
|
80
130
|
end
|
81
131
|
```
|
82
132
|
|
83
|
-
This will generate
|
133
|
+
This will generate the following methods:
|
134
|
+
|
135
|
+
- `email_not_confirmed?`
|
136
|
+
- `email_confirmed?`
|
137
|
+
- `confirm_email`
|
138
|
+
- `confirm_email!`
|
139
|
+
- `confirm_all_emails` (class method)
|
140
|
+
|
141
|
+
As well as these two scopes:
|
142
|
+
|
143
|
+
- `email_confirmed`
|
144
|
+
- `email_not_confirmed`
|
145
|
+
|
146
|
+
### Using a Rails generator
|
147
|
+
|
148
|
+
If you want to quickly add a new event, you can make use of a Rails generator provided by the gem:
|
149
|
+
|
150
|
+
```
|
151
|
+
$ rails generate active_record:event task complete
|
152
|
+
```
|
153
|
+
|
154
|
+
It will create a necessary migration and insert a `has_event` statement into the model class.
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
# db/migrate/XXX_add_completed_at_to_tasks.rb
|
158
|
+
|
159
|
+
class AddCompletedAtToTasks < ActiveRecord::Migration[6.0]
|
160
|
+
def change
|
161
|
+
add_column :tasks, :completed_at, :datetime
|
162
|
+
end
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
166
|
+
```ruby
|
167
|
+
# app/models/task.rb
|
168
|
+
|
169
|
+
class Task < ActiveRecord::Base
|
170
|
+
has_event :complete
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
All of the macro options are supported by the generator and can be passed via the command line.
|
175
|
+
For instance:
|
176
|
+
|
177
|
+
```
|
178
|
+
$ rails generate active_record:event user confirm --object=email --skip-scopes
|
179
|
+
```
|
180
|
+
|
181
|
+
For more information, run the generator with the `--help` option.
|
182
|
+
|
183
|
+
### Overriding methods
|
184
|
+
|
185
|
+
If there's a need to override any of the methods generated by the macro, you can define a new method with the same name in the corresponding model class.
|
186
|
+
This applies to instance methods as well as class methods.
|
187
|
+
In both cases, the `super` keyword invokes the original method.
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
class Task < ActiveRecord::Base
|
191
|
+
has_event :complete
|
192
|
+
|
193
|
+
def complete!
|
194
|
+
super
|
195
|
+
logger.info("Task #{id} has been completed")
|
196
|
+
end
|
197
|
+
|
198
|
+
def self.complete_all
|
199
|
+
super
|
200
|
+
logger.info('All tasks have been completed')
|
201
|
+
end
|
202
|
+
end
|
203
|
+
```
|
204
|
+
|
205
|
+
## Contributors
|
206
|
+
|
207
|
+
- [Bartosz Pieńkowski](https://github.com/pienkowb)
|
208
|
+
- [Tomasz Skupiński](https://github.com/tskupinski)
|
209
|
+
- [Oskar Janusz](https://github.com/oskaror)
|
210
|
+
- [Mike Rogers](https://github.com/MikeRogers0)
|
84
211
|
|
85
212
|
## See also
|
86
213
|
|
87
|
-
- [ActiveRecord::Enum](
|
214
|
+
- [ActiveRecord::Enum](https://api.rubyonrails.org/classes/ActiveRecord/Enum.html)
|
data/Rakefile
CHANGED
@@ -16,14 +16,38 @@ end
|
|
16
16
|
|
17
17
|
RDoc::Task.new(:rdoc) do |rdoc|
|
18
18
|
rdoc.rdoc_dir = 'rdoc'
|
19
|
-
rdoc.title
|
19
|
+
rdoc.title = 'ActiveRecord::Events'
|
20
20
|
rdoc.options << '--line-numbers'
|
21
21
|
rdoc.rdoc_files.include('README.rdoc')
|
22
22
|
rdoc.rdoc_files.include('lib/**/*.rb')
|
23
23
|
end
|
24
24
|
|
25
|
-
require '
|
26
|
-
|
25
|
+
require 'active_record'
|
26
|
+
require 'yaml'
|
27
|
+
|
28
|
+
include ActiveRecord::Tasks
|
29
|
+
|
30
|
+
dummy_root = File.expand_path('spec/dummy', __dir__)
|
31
|
+
database_config = YAML.load_file("#{dummy_root}/config/database.yml")
|
32
|
+
|
33
|
+
DatabaseTasks.root = dummy_root
|
34
|
+
DatabaseTasks.env = ENV['RAILS_ENV'] || 'development'
|
35
|
+
DatabaseTasks.db_dir = "#{dummy_root}/db"
|
36
|
+
DatabaseTasks.database_configuration = database_config
|
37
|
+
DatabaseTasks.migrations_paths = "#{dummy_root}/db/migrate"
|
38
|
+
|
39
|
+
task :environment do
|
40
|
+
require "#{dummy_root}/config/environment.rb"
|
41
|
+
end
|
42
|
+
|
43
|
+
ACTIVE_RECORD_MIGRATION_CLASS =
|
44
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
45
|
+
ActiveRecord::Migration[4.2]
|
46
|
+
else
|
47
|
+
ActiveRecord::Migration
|
48
|
+
end
|
49
|
+
|
50
|
+
load 'active_record/railties/databases.rake'
|
27
51
|
|
28
52
|
Bundler::GemHelper.install_tasks
|
29
53
|
|
data/lib/active_record/events.rb
CHANGED
@@ -1,39 +1,13 @@
|
|
1
1
|
require 'active_support'
|
2
|
-
require 'active_record/events/naming'
|
3
|
-
|
4
|
-
module ActiveRecord
|
5
|
-
module Events
|
6
|
-
def has_events(*names)
|
7
|
-
options = names.extract_options!
|
8
|
-
names.each { |n| has_event(n, options) }
|
9
|
-
end
|
10
|
-
|
11
|
-
def has_event(name, options = {})
|
12
|
-
naming = Naming.new(name, options)
|
13
|
-
|
14
|
-
define_method("#{naming.predicate}?") do
|
15
|
-
self[naming.field].present?
|
16
|
-
end
|
17
2
|
|
18
|
-
|
19
|
-
touch(naming.field) if self[naming.field].blank?
|
20
|
-
end
|
3
|
+
require 'active_record/events/version'
|
21
4
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
define_singleton_method(naming.scope) do
|
27
|
-
where(arel_table[naming.field].not_eq(nil))
|
28
|
-
end
|
5
|
+
require 'active_record/events/naming'
|
6
|
+
require 'active_record/events/method_factory'
|
7
|
+
require 'active_record/events/extension'
|
29
8
|
|
30
|
-
|
31
|
-
where(arel_table[naming.field].eq(nil))
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
9
|
+
require 'active_record/events/macro'
|
36
10
|
|
37
11
|
ActiveSupport.on_load(:active_record) do
|
38
|
-
extend ActiveRecord::Events
|
12
|
+
extend ActiveRecord::Events::Extension
|
39
13
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'active_record/events/method_factory'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Events
|
5
|
+
module Extension
|
6
|
+
def has_events(*names)
|
7
|
+
options = names.extract_options!
|
8
|
+
names.each { |n| has_event(n, options) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def has_event(name, options = {})
|
12
|
+
method_factory = MethodFactory.new(name, options)
|
13
|
+
|
14
|
+
include method_factory.instance_methods
|
15
|
+
extend method_factory.class_methods
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Events
|
3
|
+
class Macro
|
4
|
+
def initialize(event_name, options)
|
5
|
+
@event_name = event_name.to_s
|
6
|
+
@options = options
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
"has_event :#{event_name}#{options_list}"
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def event_name
|
16
|
+
@event_name.underscore
|
17
|
+
end
|
18
|
+
|
19
|
+
def options_list
|
20
|
+
options.unshift('').join(', ') if options.present?
|
21
|
+
end
|
22
|
+
|
23
|
+
def options
|
24
|
+
@options.map { |k, v| "#{k}: #{convert_value(v)}" }
|
25
|
+
end
|
26
|
+
|
27
|
+
def convert_value(value)
|
28
|
+
symbol_or_string?(value) ? ":#{value}" : value
|
29
|
+
end
|
30
|
+
|
31
|
+
def symbol_or_string?(value)
|
32
|
+
value.is_a?(Symbol) || value.is_a?(String)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'active_record/events/naming'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Events
|
5
|
+
class MethodFactory
|
6
|
+
def initialize(event_name, options)
|
7
|
+
@options = options
|
8
|
+
@naming = Naming.new(event_name, options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def instance_methods
|
12
|
+
Module.new.tap do |module_|
|
13
|
+
define_predicate_method(module_, naming)
|
14
|
+
define_inverse_predicate_method(module_, naming)
|
15
|
+
|
16
|
+
define_action_method(module_, naming)
|
17
|
+
define_safe_action_method(module_, naming)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def class_methods
|
22
|
+
Module.new.tap do |module_|
|
23
|
+
define_collective_action_method(module_, naming)
|
24
|
+
|
25
|
+
unless options[:skip_scopes]
|
26
|
+
define_scope_method(module_, naming)
|
27
|
+
define_inverse_scope_method(module_, naming)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :options
|
35
|
+
attr_reader :naming
|
36
|
+
|
37
|
+
def define_predicate_method(module_, naming)
|
38
|
+
module_.send(:define_method, naming.predicate) do
|
39
|
+
self[naming.field].present?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def define_inverse_predicate_method(module_, naming)
|
44
|
+
module_.send(:define_method, naming.inverse_predicate) do
|
45
|
+
!__send__(naming.predicate)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def define_action_method(module_, naming)
|
50
|
+
module_.send(:define_method, naming.action) do
|
51
|
+
if persisted?
|
52
|
+
touch(naming.field)
|
53
|
+
else
|
54
|
+
self[naming.field] = current_time_from_proper_timezone
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def define_safe_action_method(module_, naming)
|
60
|
+
module_.send(:define_method, naming.safe_action) do
|
61
|
+
__send__(naming.action) if __send__(naming.inverse_predicate)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def define_collective_action_method(module_, naming)
|
66
|
+
module_.send(:define_method, naming.collective_action) do
|
67
|
+
if respond_to?(:touch_all)
|
68
|
+
touch_all(naming.field)
|
69
|
+
else
|
70
|
+
update_all(naming.field => Time.current)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def define_scope_method(module_, naming)
|
76
|
+
module_.send(:define_method, naming.scope) do
|
77
|
+
where(arel_table[naming.field].not_eq(nil))
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def define_inverse_scope_method(module_, naming)
|
82
|
+
module_.send(:define_method, naming.inverse_scope) do
|
83
|
+
where(arel_table[naming.field].eq(nil))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|