ta_has_event 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 951ce881bec2caac969973595dd1345226ed2213bd35b66223facb8377229bdb
4
+ data.tar.gz: 655feb6da1c124ef7fb156a91191580b4777eb34562e15338f01da9175648ec7
5
+ SHA512:
6
+ metadata.gz: ab7f97b14543cec599bc5a4118e97fe8dae1b5c03d53c86b3046c1c37f0d2a76a097b555be01881ac0deb862df263b0a73ec77da2abb7a515be3fbae4bcb232a
7
+ data.tar.gz: b16fad751df98adab0dffd1448b6b7375299c33dee89a6d709b821e29c2f828e4355f82c7a1f7620e7b2fd2e2b88d276572095d83896ed0f293d695105ef86e3
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2015 Bartosz Pieńkowski
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,214 @@
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
+
3
+ An ActiveRecord extension providing convenience methods for timestamp management.
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
+
13
+ ## Installation
14
+
15
+ Add the following line to your application's Gemfile:
16
+
17
+ ```ruby
18
+ gem 'active_record-events'
19
+ ```
20
+
21
+ Install the gem with Bundler:
22
+
23
+ ```
24
+ $ bundle install
25
+ ```
26
+
27
+ Or do it manually by running:
28
+
29
+ ```
30
+ $ gem install active_record-events
31
+ ```
32
+
33
+ ## Usage
34
+
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.
37
+ This gem allows you to manage custom timestamp fields in the exact same manner.
38
+
39
+ ### Example
40
+
41
+ Consider a `Task` model with a `completed_at` field and the following methods:
42
+
43
+ ```ruby
44
+ class Task < ActiveRecord::Base
45
+ def completed?
46
+ completed_at.present?
47
+ end
48
+
49
+ def not_completed?
50
+ !completed?
51
+ end
52
+
53
+ def complete
54
+ complete! if not_completed?
55
+ end
56
+
57
+ def complete!
58
+ touch(:completed_at)
59
+ end
60
+
61
+ def self.complete_all
62
+ touch_all(:completed_at)
63
+ end
64
+ end
65
+ ```
66
+
67
+ Instead of defining all of these methods by hand, you can use the `has_event` macro provided by the gem.
68
+
69
+ ```ruby
70
+ class Task < ActiveRecord::Base
71
+ has_event :complete
72
+ end
73
+ ```
74
+
75
+ As a result, the methods will be generated automatically.
76
+
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.*
79
+
80
+ ### Scopes
81
+
82
+ In addition, the macro defines two scope methods – one for retrieving objects with a recorded timestamp and one for those without it, for example:
83
+
84
+ ```ruby
85
+ scope :not_completed, -> { where(completed_at: nil) }
86
+ scope :completed, -> { where.not(completed_at: nil) }
87
+ ```
88
+
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
123
+
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.
125
+ In order to keep method names grammatically correct, you can specify an object using the `object` option.
126
+
127
+ ```ruby
128
+ class User < ActiveRecord::Base
129
+ has_event :confirm, object: :email
130
+ end
131
+ ```
132
+
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)
211
+
212
+ ## See also
213
+
214
+ - [ActiveRecord::Enum](https://api.rubyonrails.org/classes/ActiveRecord/Enum.html)
data/Rakefile ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env rake
2
+
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+
9
+ begin
10
+ require 'rdoc/task'
11
+ rescue LoadError
12
+ require 'rdoc/rdoc'
13
+ require 'rake/rdoctask'
14
+ RDoc::Task = Rake::RDocTask
15
+ end
16
+
17
+ RDoc::Task.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'ActiveRecord::Events'
20
+ rdoc.options << '--line-numbers'
21
+ rdoc.rdoc_files.include('README.rdoc')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
24
+
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'
51
+
52
+ Bundler::GemHelper.install_tasks
53
+
54
+ require 'rspec/core'
55
+ require 'rspec/core/rake_task'
56
+
57
+ desc 'Run all specs in spec directory (excluding plugin specs)'
58
+ RSpec::Core::RakeTask.new(spec: 'db:test:prepare')
59
+
60
+ task default: :spec
@@ -0,0 +1,23 @@
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
+ naming = method_factory.naming
15
+ attribute naming.field, naming.field_database_type, index: true
16
+ attribute naming.boolean_attribute, :boolean, index: true, default: -> { false }
17
+
18
+ include method_factory.instance_methods
19
+ extend method_factory.class_methods
20
+ end
21
+ end
22
+ end
23
+ 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,121 @@
1
+ require 'active_record/events/naming'
2
+
3
+ module ActiveRecord
4
+ module Events
5
+ class MethodFactory
6
+ attr_reader :naming
7
+
8
+ def initialize(event_name, options)
9
+ @options = options
10
+ @naming = Naming.new(event_name, options)
11
+ end
12
+
13
+ def instance_methods
14
+ Module.new.tap do |module_|
15
+ field_type = naming.field_database_type
16
+
17
+ define_predicate_method(module_, naming)
18
+ define_inverse_predicate_method(module_, naming)
19
+
20
+ define_action_method(module_, naming)
21
+ define_inverse_action_method(module_, naming)
22
+ define_safe_action_method(module_, naming)
23
+ define_inverse_safe_action_method(module_, naming)
24
+ define_boolean_attribute_method(module_, naming)
25
+ end
26
+ end
27
+
28
+ def class_methods
29
+ Module.new.tap do |module_|
30
+ define_collective_action_method(module_, naming)
31
+
32
+ unless options[:skip_scopes]
33
+ define_scope_method(module_, naming)
34
+ define_inverse_scope_method(module_, naming)
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :options
42
+
43
+ def define_predicate_method(module_, naming)
44
+ module_.send(:define_method, naming.predicate) do
45
+ self[naming.field].present?
46
+ end
47
+ end
48
+
49
+ def define_inverse_predicate_method(module_, naming)
50
+ module_.send(:define_method, naming.inverse_predicate) do
51
+ !__send__(naming.predicate)
52
+ end
53
+ end
54
+
55
+ def define_action_method(module_, naming)
56
+ module_.send(:define_method, naming.action) do
57
+ __send__("#{naming.boolean_attribute}=", true)
58
+ end
59
+ end
60
+
61
+ def define_inverse_action_method(module_, naming)
62
+ module_.send(:define_method, naming.inverse_action) do
63
+ __send__("#{naming.boolean_attribute}=", false)
64
+ end
65
+ end
66
+
67
+ def define_safe_action_method(module_, naming)
68
+ module_.send(:define_method, naming.safe_action) do
69
+ if __send__(naming.inverse_predicate)
70
+ __send__(naming.action)
71
+ save!
72
+ end
73
+ end
74
+ end
75
+
76
+ def define_inverse_safe_action_method(module_, naming)
77
+ module_.send(:define_method, naming.inverse_safe_action) do
78
+ if __send__(naming.inverse_predicate)
79
+ __send__(naming.inverse_action)
80
+ save!
81
+ end
82
+ end
83
+ end
84
+
85
+ # naming.boolean_attribute=
86
+ # is_object_completed=
87
+ def define_boolean_attribute_method(module_, naming)
88
+ module_.send(:define_method, "#{naming.boolean_attribute}=") do |val|
89
+ if val == true && self[naming.field].blank?
90
+ self[naming.field] = current_time_from_proper_timezone
91
+ elsif val == false && self[naming.field].present?
92
+ self[naming.field] = nil
93
+ end
94
+ super(val)
95
+ end
96
+ end
97
+
98
+ def define_collective_action_method(module_, naming)
99
+ module_.send(:define_method, naming.collective_action) do
100
+ if respond_to?(:touch_all)
101
+ touch_all(naming.field)
102
+ else
103
+ update_all(naming.field => Time.current)
104
+ end
105
+ end
106
+ end
107
+
108
+ def define_scope_method(module_, naming)
109
+ module_.send(:define_method, naming.scope) do
110
+ where(arel_table[naming.field].not_eq(nil))
111
+ end
112
+ end
113
+
114
+ def define_inverse_scope_method(module_, naming)
115
+ module_.send(:define_method, naming.inverse_scope) do
116
+ where(arel_table[naming.field].eq(nil))
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,85 @@
1
+ require 'verbs'
2
+
3
+ module ActiveRecord
4
+ module Events
5
+ class Naming
6
+ def initialize(infinitive, options = {})
7
+ @infinitive = infinitive
8
+ @object = options[:object].presence
9
+ @field_name = options[:field_name].to_s
10
+ @field_type = options[:field_type].try(:to_sym)
11
+ end
12
+
13
+ def field
14
+ return field_name if field_name.present?
15
+
16
+ suffix = field_type == :date ? 'on' : 'at'
17
+
18
+ concatenate(object, past_participle, suffix)
19
+ end
20
+
21
+ def field_database_type
22
+ field_type || :datetime
23
+ end
24
+
25
+ def boolean_attribute
26
+ concatenate('is', object, past_participle)
27
+ end
28
+
29
+ def predicate
30
+ concatenate(object, past_participle) + '?'
31
+ end
32
+
33
+ def inverse_predicate
34
+ concatenate(object, 'not', past_participle) + '?'
35
+ end
36
+
37
+ def action
38
+ concatenate(infinitive, object)
39
+ end
40
+
41
+ def inverse_action
42
+ concatenate('not', infinitive, object)
43
+ end
44
+
45
+ def safe_action
46
+ concatenate(infinitive, object) + '!'
47
+ end
48
+
49
+ def inverse_safe_action
50
+ concatenate('not', infinitive, object) + '!'
51
+ end
52
+
53
+ def collective_action
54
+ concatenate(infinitive, 'all', plural_object)
55
+ end
56
+
57
+ def scope
58
+ concatenate(object, past_participle)
59
+ end
60
+
61
+ def inverse_scope
62
+ concatenate(object, 'not', past_participle)
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :infinitive
68
+ attr_reader :object
69
+ attr_reader :field_name
70
+ attr_reader :field_type
71
+
72
+ def concatenate(*parts)
73
+ parts.compact.join('_')
74
+ end
75
+
76
+ def past_participle
77
+ infinitive.verb.conjugate(tense: :past, aspect: :perfective)
78
+ end
79
+
80
+ def plural_object
81
+ object.to_s.pluralize if object.present?
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRecord
2
+ module Events
3
+ VERSION = '1.0.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,13 @@
1
+ require 'active_support'
2
+
3
+ require 'active_record/events/version'
4
+
5
+ require 'active_record/events/naming'
6
+ require 'active_record/events/method_factory'
7
+ require 'active_record/events/extension'
8
+
9
+ require 'active_record/events/macro'
10
+
11
+ ActiveSupport.on_load(:active_record) do
12
+ extend ActiveRecord::Events::Extension
13
+ 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