active_record-events 1.1.0 → 4.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 CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- Njk0YzdlMmY0YzRiMmYxNzdjMmE3OTMyODUwNjIzNjgwZmEzOTZiMg==
5
- data.tar.gz: !binary |-
6
- NDNiMmViY2U5NTNmZmY5ZjQ3NzdkMTY5Mjg4YTlmMTJmMTI3NjI3Yw==
2
+ SHA256:
3
+ metadata.gz: 5039a0306f7a2a2c89a4e6cea36d6995153caeaf8e2941ac12baea60e351f9bd
4
+ data.tar.gz: ffaa64e1917f5824b67e9a9b72f6edff09a25b05149aff624c08974e8ab264b5
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- OTlhYzQ1ODkwNjg1ZjNkYjhjZTY3ZTgwZWIyYjgxYTgzNDM1NmJlNmExODEy
10
- ZTU1MTYxOTllMDAwNmUwOTM0MTVkM2FlODZlN2ZjNzViOTYzNmFmOTk2NTEw
11
- Y2JlNzEzZGUyN2EyMTk4M2Q2YWZiNTQ2ODI5NjAzYWY1ZWMwNjY=
12
- data.tar.gz: !binary |-
13
- YWZmMzdhYjNmODJlMmY5ZDExOGE1ODBjNmQ3NTc4YTJmOWZlMjIwODU0ZTMz
14
- MmJiZjU1ZTJlYjQyOWI4NDA3N2U2NzZjYWFkNDAxNTIyYTE0MDEyMWExOWQz
15
- YTA0MmE4MGQzNzNjMWQwNjQzZGQzMzJmZTE3YzBmNDBjMzdlMDg=
6
+ metadata.gz: ce136868ee5545c3a13d28a766b43adb868c3b6f4e92574f006eb7338d1247982fceeff0aca60f39ad395b90bfb002a65f964f2180914dde566397e79676918b
7
+ data.tar.gz: 9acbffdd536499d00fb481d3eb16d60798fb34e6cfdefb0daf5c6f1400e5b3cb750d5e50892dea62c897c673d0f2e14221173ea1cba486cccc77f6b6ffe4055a
File without changes
data/README.md CHANGED
@@ -1,4 +1,4 @@
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
 
@@ -24,9 +24,12 @@ $ 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 – `created_at` and `updated_at` fields handled by ActiveRecord itself are a good example of such approach.
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
@@ -35,17 +38,25 @@ class Task < ActiveRecord::Base
35
38
  completed_at.present?
36
39
  end
37
40
 
41
+ def not_completed?
42
+ !completed?
43
+ end
44
+
38
45
  def complete
39
- complete! unless completed?
46
+ complete! if not_completed?
40
47
  end
41
48
 
42
49
  def complete!
43
50
  touch(:completed_at)
44
51
  end
52
+
53
+ def self.complete_all
54
+ touch_all(:completed_at)
55
+ end
45
56
  end
46
57
  ```
47
58
 
48
- Instead of defining these three methods explicitly, you can use a macro provided by the gem.
59
+ Instead of defining all of these methods by hand, you can use the `has_event` macro provided by the gem.
49
60
 
50
61
  ```ruby
51
62
  class Task < ActiveRecord::Base
@@ -53,12 +64,10 @@ class Task < ActiveRecord::Base
53
64
  end
54
65
  ```
55
66
 
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.
67
+ As a result, the methods will be generated automatically.
58
68
 
59
- ```ruby
60
- has_events :complete, :archive
61
- ```
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.*
62
71
 
63
72
  ### Scopes
64
73
 
@@ -69,19 +78,128 @@ scope :not_completed, -> { where(completed_at: nil) }
69
78
  scope :completed, -> { where.not(completed_at: nil) }
70
79
  ```
71
80
 
72
- ### Subject
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
+ ### Date fields
97
+
98
+ 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:
99
+
100
+ ```ruby
101
+ has_event :complete, field_type: :date
102
+ ```
103
+
104
+ ### Custom field name
105
+
106
+ 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.
107
+
108
+ ```ruby
109
+ has_event :complete, field_name: :completion_time
110
+ ```
111
+
112
+ Note that the `field_name` option takes precedence over the `field_type` option.
113
+
114
+ ### Specifying an object
73
115
 
74
116
  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
- In order to keep method names grammatically correct, you can specify a subject using the `subject` option.
117
+ In order to keep method names grammatically correct, you can specify an object using the `object` option.
76
118
 
77
119
  ```ruby
78
120
  class User < ActiveRecord::Base
79
- has_event :confirm, subject: :email
121
+ has_event :confirm, object: :email
80
122
  end
81
123
  ```
82
124
 
83
- This will generate `email_confirmed?`, `confirm_email` and `confirm_email!` methods.
125
+ This will generate the following methods:
126
+
127
+ - `email_not_confirmed?`
128
+ - `email_confirmed?`
129
+ - `confirm_email`
130
+ - `confirm_email!`
131
+ - `confirm_all_emails` (class method)
132
+
133
+ As well as these two scopes:
134
+
135
+ - `email_confirmed`
136
+ - `email_not_confirmed`
137
+
138
+ ### Using a Rails generator
139
+
140
+ If you want to quickly add a new event, you can make use of a Rails generator provided by the gem:
141
+
142
+ ```
143
+ $ rails generate active_record:event task complete
144
+ ```
145
+
146
+ It will create a necessary migration and insert a `has_event` statement into the model class.
147
+
148
+ ```ruby
149
+ # db/migrate/XXX_add_completed_at_to_tasks.rb
150
+
151
+ class AddCompletedAtToTasks < ActiveRecord::Migration[6.0]
152
+ def change
153
+ add_column :tasks, :completed_at, :datetime
154
+ end
155
+ end
156
+ ```
157
+
158
+ ```ruby
159
+ # app/models/task.rb
160
+
161
+ class Task < ActiveRecord::Base
162
+ has_event :complete
163
+ end
164
+ ```
165
+
166
+ All of the macro options are supported by the generator and can be passed via the command line.
167
+ For instance:
168
+
169
+ ```
170
+ $ rails generate active_record:event user confirm --object=email --skip-scopes
171
+ ```
172
+
173
+ For more information, run the generator with the `--help` option.
174
+
175
+ ### Overriding methods
176
+
177
+ 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.
178
+ This applies to instance methods as well as class methods.
179
+ In both cases, the `super` keyword invokes the original method.
180
+
181
+ ```ruby
182
+ class Task < ActiveRecord::Base
183
+ has_event :complete
184
+
185
+ def complete!
186
+ super
187
+ logger.info("Task #{id} has been completed")
188
+ end
189
+
190
+ def self.complete_all
191
+ super
192
+ logger.info('All tasks have been completed')
193
+ end
194
+ end
195
+ ```
196
+
197
+ ## Contributors
198
+
199
+ - [Bartosz Pieńkowski](https://github.com/pienkowb)
200
+ - [Tomasz Skupiński](https://github.com/tskupinski)
201
+ - [Oskar Janusz](https://github.com/oskaror)
84
202
 
85
203
  ## See also
86
204
 
87
- - [ActiveRecord::Enum](http://api.rubyonrails.org/classes/ActiveRecord/Enum.html)
205
+ - [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,84 @@
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
+ touch(naming.field)
52
+ end
53
+ end
54
+
55
+ def define_safe_action_method(module_, naming)
56
+ module_.send(:define_method, naming.safe_action) do
57
+ __send__(naming.action) if __send__(naming.inverse_predicate)
58
+ end
59
+ end
60
+
61
+ def define_collective_action_method(module_, naming)
62
+ module_.send(:define_method, naming.collective_action) do
63
+ if respond_to?(:touch_all)
64
+ touch_all(naming.field)
65
+ else
66
+ update_all(naming.field => Time.current)
67
+ end
68
+ end
69
+ end
70
+
71
+ def define_scope_method(module_, naming)
72
+ module_.send(:define_method, naming.scope) do
73
+ where(arel_table[naming.field].not_eq(nil))
74
+ end
75
+ end
76
+
77
+ def define_inverse_scope_method(module_, naming)
78
+ module_.send(:define_method, naming.inverse_scope) do
79
+ where(arel_table[naming.field].eq(nil))
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end