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