active_record-events 1.1.0 → 4.0.0

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