active_record-events 2.2.0 → 3.0.0
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 +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
|