active_record-events 2.2.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/{MIT-LICENSE → LICENSE} +0 -0
- data/README.md +102 -13
- data/Rakefile +6 -5
- data/lib/active_record/events.rb +37 -27
- data/lib/active_record/events/macro.rb +36 -0
- data/lib/active_record/events/naming.rb +25 -11
- data/lib/active_record/events/verbs.rb +5 -0
- 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/naming_spec.rb +40 -26
- data/spec/active_record/events_spec.rb +46 -44
- data/spec/dummy/app/models/task.rb +11 -0
- data/spec/dummy/config/boot.rb +2 -3
- data/spec/dummy/config/environment.rb +6 -4
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/schema.rb +1 -7
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/generators/active_record/event/event_generator_spec.rb +45 -0
- data/spec/spec_helper.rb +5 -2
- data/spec/support/factories.rb +0 -1
- data/spec/support/generator_helpers.rb +8 -0
- metadata +81 -34
- data/spec/dummy/app/models/user.rb +0 -3
- data/spec/dummy/db/migrate/20170103215307_create_users.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4d3c49af800cff40043af18ec6357dd41a8492a75942525701fa607b078a0bb0
|
4
|
+
data.tar.gz: 01b3d06c56050a764fa67b772e8fdf6f791603be7e220773d7e7547a53450ef6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a3f18c26a27d1eca9ee0517ea19dbf087019200e02fa02b95f0412c4901f4a2aec4239115fc38191d1e6ad7f643067f1567fde10633ace0beb71ddb4623f027b
|
7
|
+
data.tar.gz: a388c51a55a45a13bccc7aad2288aefbef85c49ba322f6bd7fe0791aa4f378b1be1e41ac85562c732168be41553499560e1f8d79a2e0ea2b4691d25980189087
|
data/{MIT-LICENSE → LICENSE}
RENAMED
File without changes
|
data/README.md
CHANGED
@@ -24,21 +24,24 @@ $ gem install active_record-events
|
|
24
24
|
|
25
25
|
## Usage
|
26
26
|
|
27
|
-
Recording a timestamp in order to mark that an event occurred to an object is a common practice when dealing with ActiveRecord models
|
27
|
+
Recording a timestamp in order to mark that an event occurred to an object is a common practice when dealing with ActiveRecord models.
|
28
|
+
A good example of such an approach is how ActiveRecord handles the `created_at` and `updated_at` fields.
|
28
29
|
This gem allows you to manage custom timestamp fields in the exact same manner.
|
29
30
|
|
31
|
+
### Example
|
32
|
+
|
30
33
|
Consider a `Task` model with a `completed_at` field and the following methods:
|
31
34
|
|
32
35
|
```ruby
|
33
36
|
class Task < ActiveRecord::Base
|
34
|
-
def not_completed?
|
35
|
-
!completed?
|
36
|
-
end
|
37
|
-
|
38
37
|
def completed?
|
39
38
|
completed_at.present?
|
40
39
|
end
|
41
40
|
|
41
|
+
def not_completed?
|
42
|
+
!completed?
|
43
|
+
end
|
44
|
+
|
42
45
|
def complete
|
43
46
|
complete! if not_completed?
|
44
47
|
end
|
@@ -53,7 +56,7 @@ class Task < ActiveRecord::Base
|
|
53
56
|
end
|
54
57
|
```
|
55
58
|
|
56
|
-
Instead of defining these
|
59
|
+
Instead of defining all of these methods by hand, you can use the `has_event` macro provided by the gem.
|
57
60
|
|
58
61
|
```ruby
|
59
62
|
class Task < ActiveRecord::Base
|
@@ -61,12 +64,10 @@ class Task < ActiveRecord::Base
|
|
61
64
|
end
|
62
65
|
```
|
63
66
|
|
64
|
-
|
65
|
-
In such a case, many lines of code can be replaced with an expressive one-liner.
|
67
|
+
As a result, the methods will be generated automatically.
|
66
68
|
|
67
|
-
|
68
|
-
|
69
|
-
```
|
69
|
+
*It's important to note that the `completed_at` column has to already exist in the database.*
|
70
|
+
*Consider [using the generator](#using-a-rails-generator) to create a necessary migration.*
|
70
71
|
|
71
72
|
### Scopes
|
72
73
|
|
@@ -77,7 +78,30 @@ scope :not_completed, -> { where(completed_at: nil) }
|
|
77
78
|
scope :completed, -> { where.not(completed_at: nil) }
|
78
79
|
```
|
79
80
|
|
80
|
-
|
81
|
+
The inclusion of scope methods can be omitted by passing the `skip_scopes` flag.
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
has_event :complete, skip_scopes: true
|
85
|
+
```
|
86
|
+
|
87
|
+
### Multiple events
|
88
|
+
|
89
|
+
Using the macro is efficient when more than one field has to be handled that way.
|
90
|
+
In such a case, many lines of code can be replaced with an expressive one-liner.
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
has_events :complete, :archive
|
94
|
+
```
|
95
|
+
|
96
|
+
### Setting a field type
|
97
|
+
|
98
|
+
In case of date fields, which by convention have names ending with `_on` instead of `_at` (e.g. `delivered_on`), the `field_type` option needs to be passed to the macro:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
has_event :deliver, field_type: :date
|
102
|
+
```
|
103
|
+
|
104
|
+
### Specifying an object
|
81
105
|
|
82
106
|
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.
|
83
107
|
In order to keep method names grammatically correct, you can specify an object using the `object` option.
|
@@ -101,6 +125,71 @@ As well as these two scopes:
|
|
101
125
|
- `email_confirmed`
|
102
126
|
- `email_not_confirmed`
|
103
127
|
|
128
|
+
### Using a Rails generator
|
129
|
+
|
130
|
+
If you want to quickly add a new event, you can make use of a Rails generator provided by the gem:
|
131
|
+
|
132
|
+
```
|
133
|
+
$ rails generate active_record:event task complete
|
134
|
+
```
|
135
|
+
|
136
|
+
It will create a necessary migration and insert a `has_event` statement into the model class.
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
# db/migrate/XXX_add_completed_at_to_tasks.rb
|
140
|
+
|
141
|
+
class AddCompletedAtToTasks < ActiveRecord::Migration[6.0]
|
142
|
+
def change
|
143
|
+
add_column :tasks, :completed_at, :datetime
|
144
|
+
end
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
# app/models/task.rb
|
150
|
+
|
151
|
+
class Task < ActiveRecord::Base
|
152
|
+
has_event :complete
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
All of the macro options are supported by the generator and can be passed via the command line.
|
157
|
+
For instance:
|
158
|
+
|
159
|
+
```
|
160
|
+
$ rails generate active_record:event user confirm --object=email --skip-scopes
|
161
|
+
```
|
162
|
+
|
163
|
+
For more information, run the generator with the `--help` option.
|
164
|
+
|
165
|
+
### Overriding methods
|
166
|
+
|
167
|
+
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.
|
168
|
+
This applies to instance methods as well as class methods.
|
169
|
+
In both cases, the `super` keyword invokes the original method.
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
class Task < ActiveRecord::Base
|
173
|
+
has_event :complete
|
174
|
+
|
175
|
+
def complete!
|
176
|
+
super
|
177
|
+
logger.info("Task #{id} has been completed")
|
178
|
+
end
|
179
|
+
|
180
|
+
def self.complete_all
|
181
|
+
super
|
182
|
+
logger.info('All tasks have been completed')
|
183
|
+
end
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
## Contributors
|
188
|
+
|
189
|
+
- [Bartosz Pieńkowski](https://github.com/pienkowb)
|
190
|
+
- [Tomasz Skupiński](https://github.com/tskupinski)
|
191
|
+
- [Oskar Janusz](https://github.com/oskaror)
|
192
|
+
|
104
193
|
## See also
|
105
194
|
|
106
|
-
- [ActiveRecord::Enum](
|
195
|
+
- [ActiveRecord::Enum](https://api.rubyonrails.org/classes/ActiveRecord/Enum.html)
|
data/Rakefile
CHANGED
@@ -26,11 +26,12 @@ require 'standalone_migrations'
|
|
26
26
|
|
27
27
|
StandaloneMigrations::Tasks.load_tasks
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
29
|
+
ACTIVE_RECORD_MIGRATION_CLASS =
|
30
|
+
if ActiveRecord::VERSION::MAJOR >= 5
|
31
|
+
ActiveRecord::Migration[4.2]
|
32
|
+
else
|
33
|
+
ActiveRecord::Migration
|
34
|
+
end
|
34
35
|
|
35
36
|
Bundler::GemHelper.install_tasks
|
36
37
|
|
data/lib/active_record/events.rb
CHANGED
@@ -11,33 +11,43 @@ module ActiveRecord
|
|
11
11
|
def has_event(name, options = {})
|
12
12
|
naming = Naming.new(name, options)
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
14
|
+
include(Module.new do
|
15
|
+
define_method(naming.predicate) do
|
16
|
+
self[naming.field].present?
|
17
|
+
end
|
18
|
+
|
19
|
+
define_method(naming.inverse_predicate) do
|
20
|
+
!__send__(naming.predicate)
|
21
|
+
end
|
22
|
+
|
23
|
+
define_method(naming.action) do
|
24
|
+
touch(naming.field)
|
25
|
+
end
|
26
|
+
|
27
|
+
define_method(naming.safe_action) do
|
28
|
+
__send__(naming.action) if __send__(naming.inverse_predicate)
|
29
|
+
end
|
30
|
+
end)
|
31
|
+
|
32
|
+
extend(Module.new do
|
33
|
+
define_method(naming.collective_action) do
|
34
|
+
if respond_to?(:touch_all)
|
35
|
+
touch_all(naming.field)
|
36
|
+
else
|
37
|
+
update_all(naming.field => Time.current)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
unless options[:skip_scopes]
|
42
|
+
define_method(naming.scope) do
|
43
|
+
where(arel_table[naming.field].not_eq(nil))
|
44
|
+
end
|
45
|
+
|
46
|
+
define_method(naming.inverse_scope) do
|
47
|
+
where(arel_table[naming.field].eq(nil))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end)
|
41
51
|
end
|
42
52
|
end
|
43
53
|
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
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'verbs'
|
1
|
+
require 'active_record/events/verbs'
|
2
2
|
|
3
3
|
module ActiveRecord
|
4
4
|
module Events
|
@@ -6,44 +6,58 @@ module ActiveRecord
|
|
6
6
|
def initialize(infinitive, options = {})
|
7
7
|
@infinitive = infinitive
|
8
8
|
@object = options[:object].presence
|
9
|
+
@field_type = options[:field_type].try(:to_sym)
|
9
10
|
end
|
10
11
|
|
11
12
|
def field
|
12
|
-
|
13
|
+
suffix = field_type == :date ? 'on' : 'at'
|
14
|
+
concatenate(object, past_participle, suffix)
|
13
15
|
end
|
14
16
|
|
15
17
|
def predicate
|
16
|
-
|
18
|
+
concatenate(object, past_participle) + '?'
|
17
19
|
end
|
18
20
|
|
19
21
|
def inverse_predicate
|
20
|
-
|
22
|
+
concatenate(object, 'not', past_participle) + '?'
|
21
23
|
end
|
22
24
|
|
23
25
|
def action
|
24
|
-
|
26
|
+
concatenate(infinitive, object) + '!'
|
27
|
+
end
|
28
|
+
|
29
|
+
def safe_action
|
30
|
+
concatenate(infinitive, object)
|
25
31
|
end
|
26
32
|
|
27
33
|
def collective_action
|
28
|
-
|
34
|
+
concatenate(infinitive, 'all', plural_object)
|
29
35
|
end
|
30
36
|
|
31
37
|
def scope
|
32
|
-
|
38
|
+
concatenate(object, past_participle)
|
33
39
|
end
|
34
40
|
|
35
41
|
def inverse_scope
|
36
|
-
|
42
|
+
concatenate(object, 'not', past_participle)
|
37
43
|
end
|
38
44
|
|
39
45
|
private
|
40
46
|
|
47
|
+
attr_reader :infinitive
|
48
|
+
attr_reader :object
|
49
|
+
attr_reader :field_type
|
50
|
+
|
51
|
+
def concatenate(*parts)
|
52
|
+
parts.compact.join('_')
|
53
|
+
end
|
54
|
+
|
41
55
|
def past_participle
|
42
|
-
|
56
|
+
infinitive.verb.conjugate(tense: :past, aspect: :perfective)
|
43
57
|
end
|
44
58
|
|
45
|
-
def
|
46
|
-
|
59
|
+
def plural_object
|
60
|
+
object.to_s.pluralize if object.present?
|
47
61
|
end
|
48
62
|
end
|
49
63
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
Description:
|
2
|
+
Generates a database migration and updates the model file (if it exists).
|
3
|
+
Pass the model name, either CamelCased or under_scored, and the event name
|
4
|
+
as an infinitive.
|
5
|
+
|
6
|
+
Examples:
|
7
|
+
`bin/rails generate active_record:event task complete`
|
8
|
+
|
9
|
+
Generates a migration adding a `completed_at` column to the `tasks`
|
10
|
+
table and updates the Task model.
|
11
|
+
|
12
|
+
`bin/rails generate active_record:event user confirm --object=email`
|
13
|
+
|
14
|
+
Generates a migration adding an `email_confirmed_at` column to the
|
15
|
+
`users` table and updates the User model.
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
require 'active_record/events/naming'
|
4
|
+
require 'active_record/events/macro'
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
module Generators
|
8
|
+
class EventGenerator < Rails::Generators::Base
|
9
|
+
MACRO_OPTIONS = %w[object field_type skip_scopes].freeze
|
10
|
+
|
11
|
+
argument :model_name, type: :string
|
12
|
+
argument :event_name, type: :string
|
13
|
+
|
14
|
+
class_option :skip_scopes, type: :boolean,
|
15
|
+
desc: 'Skip the inclusion of scope methods'
|
16
|
+
class_option :field_type, type: :string,
|
17
|
+
desc: 'The field type (datetime or date)'
|
18
|
+
class_option :object, type: :string,
|
19
|
+
desc: 'The name of the object'
|
20
|
+
|
21
|
+
source_root File.expand_path('templates', __dir__)
|
22
|
+
|
23
|
+
def generate_migration_file
|
24
|
+
naming = ActiveRecord::Events::Naming.new(event_name, options)
|
25
|
+
|
26
|
+
table_name = model_name.tableize
|
27
|
+
field_name = naming.field
|
28
|
+
field_type = options[:field_type] || 'datetime'
|
29
|
+
|
30
|
+
migration_name = "add_#{field_name}_to_#{table_name}"
|
31
|
+
attributes = "#{field_name}:#{field_type}"
|
32
|
+
|
33
|
+
invoke 'active_record:migration', [migration_name, attributes]
|
34
|
+
end
|
35
|
+
|
36
|
+
def update_model_file
|
37
|
+
return unless model_file_exists?
|
38
|
+
|
39
|
+
macro_options = options.slice(*MACRO_OPTIONS)
|
40
|
+
macro = ActiveRecord::Events::Macro.new(event_name, macro_options)
|
41
|
+
|
42
|
+
inject_into_file model_file_path, "\s\s#{macro}\n", after: /class.+\n/
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def model_file_exists?
|
48
|
+
File.exist?(model_file_path)
|
49
|
+
end
|
50
|
+
|
51
|
+
def model_file_path
|
52
|
+
File.expand_path("app/models/#{model_file_name}", destination_root)
|
53
|
+
end
|
54
|
+
|
55
|
+
def model_file_name
|
56
|
+
"#{model_name.underscore.singularize}.rb"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'active_record/events/macro'
|
3
|
+
|
4
|
+
RSpec.describe ActiveRecord::Events::Macro do
|
5
|
+
let(:event_name) { :confirm }
|
6
|
+
|
7
|
+
subject { described_class.new(event_name, options) }
|
8
|
+
|
9
|
+
context 'without options' do
|
10
|
+
let(:options) { {} }
|
11
|
+
|
12
|
+
it "doesn't include any options" do
|
13
|
+
expect(subject.to_s).to eq('has_event :confirm')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'with a string option' do
|
18
|
+
let(:options) { { object: 'email' } }
|
19
|
+
|
20
|
+
it 'prepends the option value with a colon' do
|
21
|
+
expect(subject.to_s).to eq('has_event :confirm, object: :email')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'with a symbol option' do
|
26
|
+
let(:options) { { object: :email } }
|
27
|
+
|
28
|
+
it 'prepends the option value with a colon' do
|
29
|
+
expect(subject.to_s).to eq('has_event :confirm, object: :email')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'with a boolean option' do
|
34
|
+
let(:options) { { skip_scopes: true } }
|
35
|
+
|
36
|
+
it "doesn't prepend the option value with a colon" do
|
37
|
+
expect(subject.to_s).to eq('has_event :confirm, skip_scopes: true')
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'with multiple options' do
|
42
|
+
let(:options) { { object: :email, field_type: :date } }
|
43
|
+
|
44
|
+
it 'includes all of the options' do
|
45
|
+
macro = 'has_event :confirm, object: :email, field_type: :date'
|
46
|
+
expect(subject.to_s).to eq(macro)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -1,36 +1,38 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe ActiveRecord::Events::Naming do
|
4
|
-
|
5
|
-
subject { described_class.new(:complete) }
|
4
|
+
subject { described_class.new(:complete) }
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
|
6
|
+
it 'generates a field name' do
|
7
|
+
expect(subject.field).to eq('completed_at')
|
8
|
+
end
|
10
9
|
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
it 'generates a predicate name' do
|
11
|
+
expect(subject.predicate).to eq('completed?')
|
12
|
+
end
|
14
13
|
|
15
|
-
|
16
|
-
|
17
|
-
|
14
|
+
it 'generates an inverse predicate name' do
|
15
|
+
expect(subject.inverse_predicate).to eq('not_completed?')
|
16
|
+
end
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
it 'generates an action name' do
|
19
|
+
expect(subject.action).to eq('complete!')
|
20
|
+
end
|
22
21
|
|
23
|
-
|
24
|
-
|
25
|
-
|
22
|
+
it 'generates a safe action name' do
|
23
|
+
expect(subject.safe_action).to eq('complete')
|
24
|
+
end
|
26
25
|
|
27
|
-
|
28
|
-
|
29
|
-
|
26
|
+
it 'generates a collective action name' do
|
27
|
+
expect(subject.collective_action).to eq('complete_all')
|
28
|
+
end
|
30
29
|
|
31
|
-
|
32
|
-
|
33
|
-
|
30
|
+
it 'generates a scope name' do
|
31
|
+
expect(subject.scope).to eq('completed')
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'generates an inverse scope name' do
|
35
|
+
expect(subject.inverse_scope).to eq('not_completed')
|
34
36
|
end
|
35
37
|
|
36
38
|
context 'with an object' do
|
@@ -41,15 +43,19 @@ RSpec.describe ActiveRecord::Events::Naming do
|
|
41
43
|
end
|
42
44
|
|
43
45
|
it 'generates a predicate name' do
|
44
|
-
expect(subject.predicate).to eq('email_confirmed')
|
46
|
+
expect(subject.predicate).to eq('email_confirmed?')
|
45
47
|
end
|
46
48
|
|
47
49
|
it 'generates an inverse predicate name' do
|
48
|
-
expect(subject.inverse_predicate).to eq('email_not_confirmed')
|
50
|
+
expect(subject.inverse_predicate).to eq('email_not_confirmed?')
|
49
51
|
end
|
50
52
|
|
51
53
|
it 'generates an action name' do
|
52
|
-
expect(subject.action).to eq('confirm_email')
|
54
|
+
expect(subject.action).to eq('confirm_email!')
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'generates a safe action name' do
|
58
|
+
expect(subject.safe_action).to eq('confirm_email')
|
53
59
|
end
|
54
60
|
|
55
61
|
it 'generates a collective action name' do
|
@@ -64,4 +70,12 @@ RSpec.describe ActiveRecord::Events::Naming do
|
|
64
70
|
expect(subject.inverse_scope).to eq('email_not_confirmed')
|
65
71
|
end
|
66
72
|
end
|
73
|
+
|
74
|
+
context 'with a date field' do
|
75
|
+
subject { described_class.new(:deliver, field_type: :date) }
|
76
|
+
|
77
|
+
it 'generates a field name' do
|
78
|
+
expect(subject.field).to eq('delivered_on')
|
79
|
+
end
|
80
|
+
end
|
67
81
|
end
|
@@ -1,69 +1,71 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe ActiveRecord::Events do
|
4
|
-
around
|
4
|
+
around { |e| Timecop.freeze { e.run } }
|
5
5
|
|
6
|
-
|
7
|
-
let!(:task) { create(:task) }
|
6
|
+
let!(:task) { create(:task) }
|
8
7
|
|
9
|
-
|
10
|
-
|
8
|
+
it 'records a timestamp' do
|
9
|
+
task.complete
|
11
10
|
|
12
|
-
|
13
|
-
|
11
|
+
expect(task.completed?).to eq(true)
|
12
|
+
expect(task.not_completed?).to eq(false)
|
14
13
|
|
15
|
-
|
16
|
-
|
14
|
+
expect(task.completed_at).to eq(Time.current)
|
15
|
+
end
|
17
16
|
|
18
|
-
|
19
|
-
|
20
|
-
task.complete
|
17
|
+
it 'preserves a timestamp' do
|
18
|
+
task = create(:task, completed_at: 3.days.ago)
|
21
19
|
|
22
|
-
|
23
|
-
end
|
20
|
+
task.complete
|
24
21
|
|
25
|
-
|
26
|
-
|
27
|
-
task.complete!
|
22
|
+
expect(task.completed_at).to eq(3.days.ago)
|
23
|
+
end
|
28
24
|
|
29
|
-
|
30
|
-
|
25
|
+
it 'updates a timestamp' do
|
26
|
+
task = create(:task, completed_at: 3.days.ago)
|
31
27
|
|
32
|
-
|
33
|
-
Task.complete_all
|
34
|
-
expect(task.reload.completed?).to eq(true)
|
35
|
-
end
|
28
|
+
task.complete!
|
36
29
|
|
37
|
-
|
38
|
-
|
39
|
-
end
|
30
|
+
expect(task.completed_at).to eq(Time.current)
|
31
|
+
end
|
40
32
|
|
41
|
-
|
42
|
-
|
43
|
-
|
33
|
+
it 'records multiple timestamps at once' do
|
34
|
+
Task.complete_all
|
35
|
+
expect(task.reload).to be_completed
|
44
36
|
end
|
45
37
|
|
46
|
-
|
47
|
-
|
38
|
+
it 'defines a scope' do
|
39
|
+
expect(Task.completed).not_to include(task)
|
40
|
+
end
|
48
41
|
|
49
|
-
|
50
|
-
|
42
|
+
it 'defines an inverse scope' do
|
43
|
+
expect(Task.not_completed).to include(task)
|
44
|
+
end
|
51
45
|
|
52
|
-
|
53
|
-
|
46
|
+
context 'with the skip scopes flag' do
|
47
|
+
it "doesn't define a scope" do
|
48
|
+
expect(Task).not_to respond_to(:archived)
|
54
49
|
end
|
55
50
|
|
56
|
-
it '
|
57
|
-
|
58
|
-
expect(user.reload.email_confirmed?).to eq(true)
|
51
|
+
it "doesn't define an inverse scope" do
|
52
|
+
expect(Task).not_to respond_to(:not_archived)
|
59
53
|
end
|
54
|
+
end
|
60
55
|
|
61
|
-
|
62
|
-
|
63
|
-
end
|
56
|
+
it 'allows overriding instance methods' do
|
57
|
+
expect(ActiveRecord::Base.logger).to receive(:info)
|
64
58
|
|
65
|
-
|
66
|
-
|
67
|
-
|
59
|
+
task.complete!
|
60
|
+
|
61
|
+
expect(task).to be_completed
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'allows overriding class methods' do
|
65
|
+
expect(ActiveRecord::Base.logger).to receive(:info)
|
66
|
+
|
67
|
+
Task.complete_all
|
68
|
+
|
69
|
+
expect(task.reload).to be_completed
|
68
70
|
end
|
69
71
|
end
|
@@ -1,3 +1,14 @@
|
|
1
1
|
class Task < ActiveRecord::Base
|
2
2
|
has_event :complete
|
3
|
+
has_event :archive, skip_scopes: true
|
4
|
+
|
5
|
+
def complete!
|
6
|
+
super
|
7
|
+
logger.info("Task #{id} has been completed")
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.complete_all
|
11
|
+
super
|
12
|
+
logger.info('All tasks have been completed')
|
13
|
+
end
|
3
14
|
end
|
data/spec/dummy/config/boot.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
|
-
|
2
|
-
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__)
|
1
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__)
|
3
2
|
|
4
3
|
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
|
5
4
|
|
6
|
-
$LOAD_PATH.unshift
|
5
|
+
$LOAD_PATH.unshift(File.expand_path('../../../lib', __dir__))
|
@@ -1,14 +1,16 @@
|
|
1
|
-
require File.expand_path('
|
1
|
+
require File.expand_path('boot', __dir__)
|
2
2
|
|
3
3
|
require 'active_record'
|
4
4
|
|
5
5
|
Bundler.require(:default, ENV['RAILS_ENV'])
|
6
6
|
|
7
7
|
# Load application files
|
8
|
-
Dir["#{
|
8
|
+
Dir["#{__dir__}/../app/**/*.rb"].each { |f| require f }
|
9
9
|
|
10
10
|
# Load the database configuration
|
11
|
-
config_file = File.expand_path('
|
12
|
-
config = YAML
|
11
|
+
config_file = File.expand_path('database.yml', __dir__)
|
12
|
+
config = YAML.load_file(config_file)[ENV['RAILS_ENV']]
|
13
|
+
|
14
|
+
ActiveRecord::Base.logger = Logger.new(File::NULL)
|
13
15
|
|
14
16
|
ActiveRecord::Base.establish_connection(config)
|
Binary file
|
data/spec/dummy/db/schema.rb
CHANGED
@@ -10,7 +10,7 @@
|
|
10
10
|
#
|
11
11
|
# It's strongly recommended that you check this file into your version control system.
|
12
12
|
|
13
|
-
ActiveRecord::Schema.define(version:
|
13
|
+
ActiveRecord::Schema.define(version: 2015_08_13_132804) do
|
14
14
|
|
15
15
|
create_table "tasks", force: :cascade do |t|
|
16
16
|
t.datetime "completed_at"
|
@@ -18,10 +18,4 @@ ActiveRecord::Schema.define(version: 2017_01_03_215307) do
|
|
18
18
|
t.datetime "updated_at", null: false
|
19
19
|
end
|
20
20
|
|
21
|
-
create_table "users", force: :cascade do |t|
|
22
|
-
t.datetime "email_confirmed_at"
|
23
|
-
t.datetime "created_at", null: false
|
24
|
-
t.datetime "updated_at", null: false
|
25
|
-
end
|
26
|
-
|
27
21
|
end
|
data/spec/dummy/db/test.sqlite3
CHANGED
Binary file
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'generators/active_record/event/event_generator'
|
3
|
+
|
4
|
+
RSpec.describe ActiveRecord::Generators::EventGenerator, type: :generator do
|
5
|
+
arguments %w[task complete --field-type=date --skip-scopes]
|
6
|
+
destination File.expand_path('../../../../tmp', __dir__)
|
7
|
+
|
8
|
+
before { prepare_destination }
|
9
|
+
|
10
|
+
it 'generates a migration file' do
|
11
|
+
run_generator
|
12
|
+
|
13
|
+
assert_migration 'db/migrate/add_completed_on_to_tasks' do |migration|
|
14
|
+
assert_instance_method :change, migration do |content|
|
15
|
+
assert_match 'add_column :tasks, :completed_on, :date', content
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'when the model file exists' do
|
21
|
+
before do
|
22
|
+
write_file 'app/models/task.rb', <<-RUBY.strip_heredoc
|
23
|
+
class Task < ActiveRecord::Base
|
24
|
+
end
|
25
|
+
RUBY
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'updates the model file' do
|
29
|
+
run_generator
|
30
|
+
|
31
|
+
assert_file 'app/models/task.rb', <<-RUBY.strip_heredoc
|
32
|
+
class Task < ActiveRecord::Base
|
33
|
+
has_event :complete, field_type: :date, skip_scopes: true
|
34
|
+
end
|
35
|
+
RUBY
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "when the model file doesn't exist" do
|
40
|
+
it "doesn't create the model file" do
|
41
|
+
run_generator
|
42
|
+
assert_no_file 'app/models/task.rb'
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,15 +1,18 @@
|
|
1
1
|
ENV['RAILS_ENV'] ||= 'test'
|
2
2
|
|
3
|
-
require File.expand_path('
|
3
|
+
require File.expand_path('dummy/config/environment.rb', __dir__)
|
4
4
|
|
5
5
|
require 'factory_girl'
|
6
|
+
require 'generator_spec'
|
6
7
|
require 'timecop'
|
8
|
+
require 'zonebie/rspec'
|
7
9
|
|
8
|
-
Dir["#{
|
10
|
+
Dir["#{__dir__}/support/**/*.rb"].each { |f| require f }
|
9
11
|
|
10
12
|
RSpec.configure do |config|
|
11
13
|
config.color = true
|
12
14
|
config.order = :random
|
13
15
|
|
14
16
|
config.include FactoryGirl::Syntax::Methods
|
17
|
+
config.include GeneratorHelpers, type: :generator
|
15
18
|
end
|
data/spec/support/factories.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_record-events
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bartosz Pieńkowski
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-04-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -42,30 +42,72 @@ dependencies:
|
|
42
42
|
name: appraisal
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
47
|
+
version: '2.2'
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
54
|
+
version: '2.2'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: factory_girl
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
58
58
|
requirements:
|
59
|
-
- - "
|
59
|
+
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
61
|
+
version: 4.8.1
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "
|
66
|
+
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
68
|
+
version: 4.8.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: generator_spec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.9'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.9'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.9'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.9'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.50.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.50.0
|
69
111
|
- !ruby/object:Gem::Dependency
|
70
112
|
name: sqlite3
|
71
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -81,47 +123,47 @@ dependencies:
|
|
81
123
|
- !ruby/object:Gem::Version
|
82
124
|
version: 1.3.13
|
83
125
|
- !ruby/object:Gem::Dependency
|
84
|
-
name:
|
126
|
+
name: standalone_migrations
|
85
127
|
requirement: !ruby/object:Gem::Requirement
|
86
128
|
requirements:
|
87
|
-
- - "
|
129
|
+
- - "~>"
|
88
130
|
- !ruby/object:Gem::Version
|
89
|
-
version: '
|
131
|
+
version: '5.2'
|
90
132
|
type: :development
|
91
133
|
prerelease: false
|
92
134
|
version_requirements: !ruby/object:Gem::Requirement
|
93
135
|
requirements:
|
94
|
-
- - "
|
136
|
+
- - "~>"
|
95
137
|
- !ruby/object:Gem::Version
|
96
|
-
version: '
|
138
|
+
version: '5.2'
|
97
139
|
- !ruby/object:Gem::Dependency
|
98
|
-
name:
|
140
|
+
name: timecop
|
99
141
|
requirement: !ruby/object:Gem::Requirement
|
100
142
|
requirements:
|
101
|
-
- - "
|
143
|
+
- - "~>"
|
102
144
|
- !ruby/object:Gem::Version
|
103
|
-
version: '0'
|
145
|
+
version: '0.9'
|
104
146
|
type: :development
|
105
147
|
prerelease: false
|
106
148
|
version_requirements: !ruby/object:Gem::Requirement
|
107
149
|
requirements:
|
108
|
-
- - "
|
150
|
+
- - "~>"
|
109
151
|
- !ruby/object:Gem::Version
|
110
|
-
version: '0'
|
152
|
+
version: '0.9'
|
111
153
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
154
|
+
name: zonebie
|
113
155
|
requirement: !ruby/object:Gem::Requirement
|
114
156
|
requirements:
|
115
|
-
- - "
|
157
|
+
- - "~>"
|
116
158
|
- !ruby/object:Gem::Version
|
117
|
-
version: '0'
|
159
|
+
version: '0.6'
|
118
160
|
type: :development
|
119
161
|
prerelease: false
|
120
162
|
version_requirements: !ruby/object:Gem::Requirement
|
121
163
|
requirements:
|
122
|
-
- - "
|
164
|
+
- - "~>"
|
123
165
|
- !ruby/object:Gem::Version
|
124
|
-
version: '0'
|
166
|
+
version: '0.6'
|
125
167
|
description: An ActiveRecord extension providing convenience methods for timestamp
|
126
168
|
management.
|
127
169
|
email: pienkowb@gmail.com
|
@@ -129,26 +171,31 @@ executables: []
|
|
129
171
|
extensions: []
|
130
172
|
extra_rdoc_files: []
|
131
173
|
files:
|
132
|
-
-
|
174
|
+
- LICENSE
|
133
175
|
- README.md
|
134
176
|
- Rakefile
|
135
177
|
- lib/active_record/events.rb
|
178
|
+
- lib/active_record/events/macro.rb
|
136
179
|
- lib/active_record/events/naming.rb
|
180
|
+
- lib/active_record/events/verbs.rb
|
137
181
|
- lib/active_record/events/version.rb
|
182
|
+
- lib/generators/active_record/event/USAGE
|
183
|
+
- lib/generators/active_record/event/event_generator.rb
|
184
|
+
- spec/active_record/events/macro_spec.rb
|
138
185
|
- spec/active_record/events/naming_spec.rb
|
139
186
|
- spec/active_record/events_spec.rb
|
140
187
|
- spec/dummy/app/models/task.rb
|
141
|
-
- spec/dummy/app/models/user.rb
|
142
188
|
- spec/dummy/config/boot.rb
|
143
189
|
- spec/dummy/config/database.yml
|
144
190
|
- spec/dummy/config/environment.rb
|
145
191
|
- spec/dummy/db/development.sqlite3
|
146
192
|
- spec/dummy/db/migrate/20150813132804_create_tasks.rb
|
147
|
-
- spec/dummy/db/migrate/20170103215307_create_users.rb
|
148
193
|
- spec/dummy/db/schema.rb
|
149
194
|
- spec/dummy/db/test.sqlite3
|
195
|
+
- spec/generators/active_record/event/event_generator_spec.rb
|
150
196
|
- spec/spec_helper.rb
|
151
197
|
- spec/support/factories.rb
|
198
|
+
- spec/support/generator_helpers.rb
|
152
199
|
homepage: https://github.com/pienkowb/active_record-events
|
153
200
|
licenses:
|
154
201
|
- MIT
|
@@ -161,30 +208,30 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
161
208
|
requirements:
|
162
209
|
- - ">="
|
163
210
|
- !ruby/object:Gem::Version
|
164
|
-
version:
|
211
|
+
version: 2.0.0
|
165
212
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
166
213
|
requirements:
|
167
214
|
- - ">="
|
168
215
|
- !ruby/object:Gem::Version
|
169
216
|
version: '0'
|
170
217
|
requirements: []
|
171
|
-
|
172
|
-
rubygems_version: 2.7.8
|
218
|
+
rubygems_version: 3.0.3
|
173
219
|
signing_key:
|
174
220
|
specification_version: 4
|
175
221
|
summary: Manage timestamps in ActiveRecord models
|
176
222
|
test_files:
|
177
223
|
- spec/spec_helper.rb
|
178
|
-
- spec/dummy/app/models/user.rb
|
179
224
|
- spec/dummy/app/models/task.rb
|
180
225
|
- spec/dummy/config/environment.rb
|
181
226
|
- spec/dummy/config/database.yml
|
182
227
|
- spec/dummy/config/boot.rb
|
183
228
|
- spec/dummy/db/schema.rb
|
184
229
|
- spec/dummy/db/test.sqlite3
|
185
|
-
- spec/dummy/db/migrate/20170103215307_create_users.rb
|
186
230
|
- spec/dummy/db/migrate/20150813132804_create_tasks.rb
|
187
231
|
- spec/dummy/db/development.sqlite3
|
188
232
|
- spec/active_record/events/naming_spec.rb
|
233
|
+
- spec/active_record/events/macro_spec.rb
|
189
234
|
- spec/active_record/events_spec.rb
|
235
|
+
- spec/support/generator_helpers.rb
|
190
236
|
- spec/support/factories.rb
|
237
|
+
- spec/generators/active_record/event/event_generator_spec.rb
|