active_record-events 2.0.0 → 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- ZTljYzhjZWM1OGRmODA4YjIzYjYyYzMwMDg3YmMxOWVmMWM5ZTMwOA==
5
- data.tar.gz: !binary |-
6
- ZDljYzhkYTk1ZjI1ZjIyNWRlYmZmZTVhNDIzNDNhNjFiNmRlZjA5Ng==
2
+ SHA256:
3
+ metadata.gz: 18d2fc6a8788d3190b5da3dd8dc45fbf307bb55344a6072ba24db67ff9b0e6cb
4
+ data.tar.gz: 17767f7e00810c9717cc5fa4bd92d49303b22427eb5da06a3f27ea39f8a4fbf8
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- Nzg4ODRhMzRiNzFiMjUwZjkzNWNiNTFmZmE5MjAzZDZjNGE1YzkzNmI5MWQx
10
- NDVhZjJlOGM2YzNlNGFhZjc2ZjgzNzMwN2Y1YzU5MjVmOTU3OTMyMzJjY2Zl
11
- Y2NiZDU1ZTk4M2UxYjdlOTRjNzM1ZTUwZjMwMjQ0Njk1MWMzOTk=
12
- data.tar.gz: !binary |-
13
- ODYzYjIyZTNjM2I2NDNhNTk0YWFmOTkxZGU4NjQwZjQ1YzdjODJjM2M4OTkz
14
- YzUzZjZmMWM4ZWVhZTNmYWEwYzZkZjA0YzFjNDZhY2Q5MDVhOTk2ODk1ODA3
15
- NjE2ZmI5NzdiYzcxN2IyOTFiNjU3NWJkNTA3OWFmYjIxMWMyNzY=
6
+ metadata.gz: d39a25b8c5695aa8c81b9780744f0639ab2b0bac74296486721ec1c28cad0c67f801441b78c3faa48162d60938297a33638dada9e597a84a7c8c888c94cc86b2
7
+ data.tar.gz: afb5395aba7821776ff9aa3790d0f04f625daa1a4de6495e9069cf210a4ea3023b7449bb6cce58e9eb158887a541da6f927b9ba03e61df0974b6cd473a6beacf
File without changes
data/README.md CHANGED
@@ -1,7 +1,15 @@
1
- # ActiveRecord::Events [![Gem version](https://img.shields.io/gem/v/active_record-events.svg)](https://rubygems.org/gems/active_record-events) [![Build status](https://img.shields.io/travis/pienkowb/active_record-events.svg)](https://travis-ci.org/pienkowb/active_record-events)
1
+ # ActiveRecord::Events [![Gem version](https://img.shields.io/gem/v/active_record-events)](https://rubygems.org/gems/active_record-events) [![Build status](https://img.shields.io/travis/pienkowb/active_record-events/develop)](https://travis-ci.org/pienkowb/active_record-events) [![Coverage status](https://img.shields.io/coveralls/github/pienkowb/active_record-events/develop)](https://coveralls.io/github/pienkowb/active_record-events) [![Maintainability status](https://img.shields.io/codeclimate/maintainability/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 – `created_at` and `updated_at` fields handled by ActiveRecord itself are a good example of such approach.
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! unless completed?
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 three methods explicitly, you can use a macro provided by the gem.
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
- This approach is very efficient when more than one field has to be handled that way.
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
- ```ruby
60
- has_events :complete, :archive
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
- ### Object
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 `email_confirmed?`, `confirm_email` and `confirm_email!` methods.
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](http://api.rubyonrails.org/classes/ActiveRecord/Enum.html)
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 = 'ActiveRecord::Events'
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 'standalone_migrations'
26
- StandaloneMigrations::Tasks.load_tasks
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
 
@@ -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
- define_method(naming.action) do
19
- touch(naming.field) if self[naming.field].blank?
20
- end
3
+ require 'active_record/events/version'
21
4
 
22
- define_method("#{naming.action}!") do
23
- touch(naming.field)
24
- end
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
- define_singleton_method(naming.inverse_scope) do
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