rearmed_rails 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
+ SHA1:
3
+ metadata.gz: a4ee0fb7927533d24c1c51d5b336ec748a2594f9
4
+ data.tar.gz: 137ddb07d7184f254f68948a2d897a4bebe20a69
5
+ SHA512:
6
+ metadata.gz: 9bbfa3d1aff55e2295264ac940174b29b1bc00f1508a43fa8fb0158d8fd91437e1857b17d66b508b2ee763581debd7952a8af9dfc398c94755270e42d4204765
7
+ data.tar.gz: a1985d414a13d6bdfec7379480fd4346b345c9e1793fad0a729f58e860a8f47a98b695f60cf766412a52f906da1872cf523cdfeda2eb6be145065192bd006ef4
data/CHANGELOG.md ADDED
@@ -0,0 +1,6 @@
1
+ CHANGELOG
2
+ ---------
3
+
4
+ - **1.0.0 - Feb 28, 2017**
5
+ - Gem Initial Release
6
+ - Gem was extracted from the `rearmed` gem. https://github.com/westonganger/rearmed-rb
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2017 Weston Ganger
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # Rearmed Rails
2
+ <a href='https://ko-fi.com/A5071NK' target='_blank'><img height='32' style='border:0px;height:32px;' src='https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a' border='0' alt='Buy Me a Coffee' /></a>
3
+
4
+ A collection of helpful methods and monkey patches for Rails
5
+
6
+ The difference between this library and others is that all monkey patching is performed in an opt-in way because you shouldnt be using methods you dont know about anyways.
7
+
8
+ ```ruby
9
+ # Gemfile
10
+
11
+ gem 'rearmed_rails'
12
+ ```
13
+
14
+ Run `rails g rearmed_rails:setup` to create a settings files in `config/initializers/rearmed_rails.rb` where you can opt-in to the monkey patches available in the library. Set these values to true if you want to enable the applicable monkey patch.
15
+
16
+ ```ruby
17
+ # config/initializers/rearmed.rb
18
+
19
+ RearmedRails.enabled_patches = {
20
+ rails: {
21
+ active_record: {
22
+ find_duplicates: false,
23
+ find_in_relation_batches: false,
24
+ find_or_create: false,
25
+ find_relation_each: false,
26
+ newest: false,
27
+ or: false,
28
+ pluck_to_hash: false,
29
+ pluck_to_struct: false,
30
+ reset_auto_increment: false,
31
+ reset_table: false
32
+ },
33
+ helpers: {
34
+ field_is_array: false,
35
+ link_to_confirm: false,
36
+ options_for_select_include_blank: false,
37
+ options_from_collection_for_select_include_blank: false
38
+ },
39
+ v3: {
40
+ all: false,
41
+ pluck: false,
42
+ update_columns: false
43
+ }
44
+ },
45
+ minitest: {
46
+ assert_changed: false,
47
+ assert_not_changed: false
48
+ }
49
+ }
50
+
51
+
52
+ require 'rearmed_rails/apply_patches'
53
+ ```
54
+
55
+
56
+ ## Rails
57
+
58
+ ### ActiveRecord
59
+
60
+ ```ruby
61
+ # This version of `or` behaves way nicer than the stupid one that was implemented in Rails 5
62
+ # it allows you to do what you need when you want to. This patch is for Rails 4, 5+
63
+ Post.where(name: 'foo').or.where(content: 'bar')
64
+ Post.where(name: 'foo').or.my_custom_scope
65
+ Post.where(name: 'foo').or(Post.where(content: 'bar'))
66
+ Post.where(name: 'foo').or(content: 'bar')
67
+
68
+ Post.pluck_to_hash(:name, :category, :id)
69
+ Post.pluck_to_struct(:name, :category, :id)
70
+
71
+ Post.find_or_create(name: 'foo', content: 'bar') # use this instead of the super confusing first_or_create method
72
+ Post.find_or_create!(name: 'foo', content: 'bar')
73
+
74
+ Post.find_duplicates # return active record relation of all records that have duplicates. By default it skips the primary_key, created_at, updated_at, & deleted_at columns
75
+ Post.find_duplicates(:name) # find duplicates based on the name attribute
76
+ Post.find_duplicates(:name, :category) # find duplicates based on the name & category attribute
77
+ Post.find_duplicates(self.column_names.reject{|x| ['id','created_at','updated_at','deleted_at'].include?(x)})
78
+
79
+ # It also can delete duplicates. Valid values for keep are :first & :last. Valid values for delete_method are :destroy & :delete. soft_delete is only used if you are using acts_as_paranoid on your model.
80
+ Post.find_duplicates(:name, :category, delete: true)
81
+ Post.find_duplicates(:name, :category, delete: {keep: :first, delete_method: :destroy, soft_delete: true}) # these are the default settings for delete: true
82
+
83
+ Post.newest # get the newest post, by default ordered by :created_at
84
+ Post.newest(:updated_at) # different sort order
85
+ Post.newest(:published_at, :created_at) # multiple columns to sort on
86
+
87
+ Post.reset_table # delete all records from table and reset autoincrement column (id), works with mysql/mariadb/postgresql/sqlite
88
+ # or with options
89
+ Post.reset_table(delete_method: :destroy) # to ensure all callbacks are fired
90
+
91
+ Post.reset_auto_increment # reset mysql/mariadb/postgresql/sqlite auto-increment column, if contains records then defaults to starting from next available number
92
+ # or with options
93
+ Post.reset_auto_increment(value: 1, column: :id) # column option is only relevant for postgresql
94
+
95
+ Post.find_in_relation_batches # this returns a relation instead of an array
96
+ Post.find_relation_each # this returns a relation instead of an array
97
+ ```
98
+
99
+ Note: All methods which involve deletion are compatible with Paranoia & ActsAsParanoid
100
+
101
+ ### Helpers
102
+
103
+ ```ruby
104
+ # field_is_array: works with field type tag, form_for, simple form, etc
105
+ = text_field_tag :name, is_array: true #=> <input type='text' name='name[]' />
106
+
107
+ # options_for_select_include_blank
108
+ options_for_select(@users.map{|x| [x.name, x.id]}, include_blank: true, selected: params[:user_id])
109
+
110
+ # options_from_collection_for_select_include_blank
111
+ options_from_collection_for_select(@users, 'id', 'name', include_blank: true, selected: params[:user_id])
112
+
113
+ # returns to rails 3 behaviour of allowing confirm attribute as well as data-confirm
114
+ = link_to 'Delete', post_path(post), method: :delete, confirm: "Are you sure you want to delete this post?"
115
+ ```
116
+
117
+ ### Rails 3.x Backports
118
+ ```ruby
119
+ Post.all # Now returns AR relation
120
+ Post.first.update_columns(a: 'foo', b: 'bar')
121
+ Post.pluck(:name, :id) # adds multi column pluck support ex. => [['first', 1], ['second', 2], ['third', 3]]
122
+ ```
123
+
124
+ ### Minitest Methods
125
+ ```ruby
126
+ assert_changed 'user.name' do
127
+ user.name = "Bob"
128
+ end
129
+
130
+ assert_not_changed -> { user.name } do
131
+ user.update(user_params)
132
+ end
133
+
134
+ assert_not_changed lambda{ user.name } do
135
+ user.update(user_params)
136
+ end
137
+ ```
138
+
139
+ # Contributing
140
+ If you want to request a new method please raise an issue and we will discuss the idea.
141
+
142
+
143
+ # Credits
144
+ Created by Weston Ganger - @westonganger
145
+
146
+ <a href='https://ko-fi.com/A5071NK' target='_blank'><img height='32' style='border:0px;height:32px;' src='https://az743702.vo.msecnd.net/cdn/kofi1.png?v=a' border='0' alt='Buy Me a Coffee' /></a>
data/Rakefile ADDED
@@ -0,0 +1,33 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/lib/rearmed_rails/version.rb')
2
+ require 'bundler/gem_tasks'
3
+
4
+ task :test do
5
+ require 'rake/testtask'
6
+ Rake::TestTask.new do |t|
7
+ t.libs << 'test'
8
+ t.test_files = FileList['test/**/tc_*.rb']
9
+ t.verbose = true
10
+ end
11
+ end
12
+
13
+ task :console do
14
+ require 'rearmed'
15
+ Rearmed.enabled_patches = {
16
+ array: true,
17
+ hash: true,
18
+ object: true,
19
+ string: true,
20
+ date: true,
21
+ enumerable: true,
22
+ rails_3: true,
23
+ rails_4: true,
24
+ rails: true,
25
+ minitest: true
26
+ }
27
+ require 'rearmed/apply_patches'
28
+
29
+ require 'irb'
30
+ binding.irb
31
+ end
32
+
33
+ task default: :test
@@ -0,0 +1,15 @@
1
+ require 'rails/generators'
2
+
3
+ module RearmedRails
4
+ class SetupGenerator < Rails::Generators::Base
5
+
6
+ def setup
7
+ create_file "config/initializers/rearmed_rails.rb", <<eos
8
+ RearmedRails.enabled_patches = #{File.read(File.join(File.dirname(__FILE__), '../../rearmed_rails/default_enabled_patches.hash'))}
9
+
10
+ require 'rearmed_rails/apply_patches'
11
+ eos
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ require 'rearmed_rails/monkey_patches/rails/active_record/base'
2
+ require 'rearmed_rails/monkey_patches/rails/active_record/batches'
3
+ require 'rearmed_rails/monkey_patches/rails/active_record/query_methods'
4
+ require 'rearmed_rails/monkey_patches/rails/helpers'
5
+ require 'rearmed_rails/monkey_patches/rails/v3'
6
+ require 'rearmed_rails/monkey_patches/minitest'
@@ -0,0 +1,31 @@
1
+ {
2
+ rails: {
3
+ active_record: {
4
+ find_duplicates: false,
5
+ find_in_relation_batches: false,
6
+ find_or_create: false,
7
+ find_relation_each: false,
8
+ newest: false,
9
+ or: false,
10
+ pluck_to_hash: false,
11
+ pluck_to_struct: false,
12
+ reset_auto_increment: false,
13
+ reset_table: false
14
+ },
15
+ helpers: {
16
+ field_is_array: false,
17
+ link_to_confirm: false,
18
+ options_for_select_include_blank: false,
19
+ options_from_collection_for_select_include_blank: false
20
+ },
21
+ v3: {
22
+ all: false,
23
+ pluck: false,
24
+ update_columns: false
25
+ },
26
+ minitest: {
27
+ assert_changed: false,
28
+ assert_not_changed: false
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,35 @@
1
+ if defined?(Minitest::Assertions)
2
+
3
+ enabled = RearmedRails.enabled_patches[:minitest] == true
4
+
5
+ Minitest::Assertions.module_eval do
6
+
7
+ if enabled || Rearmed.dig(Rearmed.enabled_patches, :minitest, :assert_changed)
8
+ def assert_changed(expression, &block)
9
+ if expression.respond_to?(:call)
10
+ e = expression
11
+ else
12
+ e = lambda{ block.binding.eval(expression) }
13
+ end
14
+ old = e.call
15
+ block.call
16
+ refute_equal old, e.call
17
+ end
18
+ end
19
+
20
+ if enabled || Rearmed.dig(Rearmed.enabled_patches, :minitest, :assert_not_changed)
21
+ def assert_not_changed(expression, &block)
22
+ if expression.respond_to?(:call)
23
+ e = expression
24
+ else
25
+ e = lambda{ block.binding.eval(expression) }
26
+ end
27
+ old = e.call
28
+ block.call
29
+ assert_equal old, e.call
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+ end
@@ -0,0 +1,216 @@
1
+ enabled = RearmedRails.enabled_patches[:rails] == true
2
+ enabled ||= RearmedRails.dig(RearmedRails.enabled_patches, :rails, :active_record) == true
3
+
4
+ if defined?(ActiveRecord)
5
+
6
+ ActiveRecord::Base.class_eval do
7
+ if enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :active_record, :newest)
8
+ def self.newest(*columns)
9
+ if columns.empty? || !columns.to_s.include?('created_at')
10
+ columns << 'created_at'
11
+ end
12
+
13
+ the_order = {}
14
+ columns.each do |x|
15
+ the_order[x.to_s] = :desc
16
+ end
17
+ self.order(the_order).limit(1).first
18
+ end
19
+ end
20
+
21
+ if enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :active_record, :reset_table)
22
+ def self.reset_table(opts={})
23
+ if opts[:delete_method] && opts[:delete_method].to_sym == :destroy
24
+ if self.try(:paranoid?)
25
+ self.unscoped.each do |x|
26
+ if x.respond_to?(:really_destroy!)
27
+ x.really_destroy!
28
+ else
29
+ x.destroy!
30
+ end
31
+ end
32
+ else
33
+ self.unscoped.destroy_all
34
+ end
35
+ end
36
+
37
+ case self.connection.adapter_name.downcase.to_sym
38
+ when :mysql2
39
+ if !opts[:delete_method] || opts[:delete_method].to_sym != :destroy
40
+ if defined?(ActsAsParanoid) && self.try(:paranoid?)
41
+ self.unscoped.delete_all!
42
+ else
43
+ self.unscoped.delete_all
44
+ end
45
+ end
46
+ self.connection.execute("ALTER TABLE #{self.table_name} AUTO_INCREMENT = 1")
47
+ when :postgresql
48
+ self.connection.execute("TRUNCATE TABLE #{self.table_name} RESTART IDENTITY")
49
+ when :sqlite3
50
+ self.connection.execute("DELETE FROM #{self.table_name}")
51
+ self.connection.execute("DELETE FROM sqlite_sequence WHERE NAME='#{self.table_name}'")
52
+ end
53
+ end
54
+ end
55
+
56
+ if enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :active_record, :reset_auto_increment)
57
+ def self.reset_auto_increment(opts={})
58
+ case self.connection.adapter_name.downcase.to_sym
59
+ when :mysql2
60
+ opts[:value] = 1 if opts[:value].blank?
61
+ self.connection.execute("ALTER TABLE #{self.table_name} AUTO_INCREMENT = #{opts[:value]}")
62
+ when :postgresql
63
+ opts[:value] = 1 if opts[:value].blank?
64
+ self.connection.execute("ALTER SEQUENCE #{self.table_name}_#{opts[:column].to_s || self.primary_key}_seq RESTART WITH #{opts[:value]}")
65
+ when :sqlite3
66
+ opts[:value] = 0 if opts[:value].blank?
67
+ self.connection.execute("UPDATE SQLITE_SEQUENCE SET SEQ=#{opts[:value]} WHERE NAME='#{self.table_name}'")
68
+ end
69
+ end
70
+ end
71
+
72
+ if enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :active_record, :find_duplicates)
73
+ def self.find_duplicates(*args)
74
+ options = {}
75
+
76
+ unless args.empty?
77
+ if args.last.is_a?(Hash)
78
+ options = args.pop
79
+ end
80
+
81
+ unless args.empty?
82
+ if args.count == 1 && args.first.is_a?(Array)
83
+ args = args.first
84
+ end
85
+
86
+ options[:columns] = args
87
+ end
88
+ end
89
+
90
+ options[:columns] ||= self.column_names.reject{|x| [self.primary_key, :created_at, :updated_at, :deleted_at].include?(x)}
91
+
92
+ duplicates = self.select("#{options[:columns].join(', ')}, COUNT(*)").group(options[:columns]).having("COUNT(*) > 1")
93
+
94
+ if options[:delete]
95
+ if options[:delete] == true
96
+ options[:delete] = {keep: :first, delete_method: :destroy, soft_delete: true}
97
+ else
98
+ options[:delete][:delete_method] ||= :destroy
99
+ options[:delete][:keep] ||= :first
100
+ if options[:delete][:soft_delete] != false
101
+ options[:delete][:soft_delete] = true
102
+ end
103
+ end
104
+
105
+ if options[:delete][:keep] == :last
106
+ duplicates.reverse!
107
+ end
108
+
109
+ used = []
110
+ duplicates.reject! do |x|
111
+ attrs = x.attributes.slice(*options[:columns].collect(&:to_s))
112
+
113
+ if used.include?(attrs)
114
+ return false
115
+ else
116
+ used.push attrs
117
+ return true
118
+ end
119
+ end
120
+ used = nil
121
+
122
+ if options[:delete][:delete_method].to_sym == :delete
123
+ duplicates = self.where(id: duplicates.collect(&:id))
124
+
125
+ if x.respond_to?(:delete_all!)
126
+ duplicates.delete_all!
127
+ else
128
+ duplicates.delete_all
129
+ end
130
+ else
131
+ duplicates.each do |x|
132
+ if !options[:delete][:soft_delete] && x.respond_to?(:really_destroy!)
133
+ x.really_destroy!
134
+ else
135
+ x.destroy!
136
+ end
137
+ end
138
+ end
139
+ return nil
140
+ else
141
+ return duplicates
142
+ end
143
+ end
144
+ end
145
+
146
+ if enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :active_record, :find_or_create)
147
+ def self.find_or_create(attrs={}, save_opts={})
148
+ unless self.where(attrs).limit(1).first
149
+ x = self.class.new(attrs)
150
+ x.save(save_opts)
151
+ return t
152
+ end
153
+ end
154
+
155
+ def self.find_or_create!(attrs={}, save_opts={})
156
+ unless self.where(attrs).limit(1).first
157
+ x = self.class.new(attrs)
158
+ x.save!(save_opts)
159
+ return t
160
+ end
161
+ end
162
+ end
163
+
164
+ if enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :active_record, :pluck_to_hash)
165
+ def self.pluck_to_hash(*keys)
166
+ hash_type = keys[-1].is_a?(Hash) ? keys.pop.fetch(:hash_type, HashWithIndifferentAccess) : HashWithIndifferentAccess
167
+ block_given = block_given?
168
+ keys, formatted_keys = format_keys(keys)
169
+ keys_one = keys.size == 1
170
+
171
+ pluck(*keys).map do |row|
172
+ value = hash_type[formatted_keys.zip(keys_one ? [row] : row)]
173
+ block_given ? yield(value) : value
174
+ end
175
+ end
176
+ end
177
+
178
+ if enabled || enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :active_record, :pluck_to_struct)
179
+ def self.pluck_to_struct(*keys)
180
+ struct_type = keys[-1].is_a?(Hash) ? keys.pop.fetch(:struct_type, Struct) : Struct
181
+ block_given = block_given?
182
+ keys, formatted_keys = format_keys(keys)
183
+ keys_one = keys.size == 1
184
+
185
+ struct = struct_type.new(*formatted_keys)
186
+ pluck(*keys).map do |row|
187
+ value = keys_one ? struct.new(*[row]) : struct.new(*row)
188
+ block_given ? yield(value) : value
189
+ end
190
+ end
191
+ end
192
+
193
+ private
194
+
195
+ if enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :active_record, :pluck_to_hash) || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :pluck_to_struct)
196
+ def self.format_keys(keys)
197
+ if keys.blank?
198
+ [column_names, column_names]
199
+ else
200
+ [
201
+ keys,
202
+ keys.map do |k|
203
+ case k
204
+ when String
205
+ k.split(/\bas\b/i)[-1].strip.to_sym
206
+ when Symbol
207
+ k
208
+ end
209
+ end
210
+ ]
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ end
@@ -0,0 +1,60 @@
1
+ enabled = RearmedRails.enabled_patches[:rails] == true
2
+ enabled ||= RearmedRails.dig(RearmedRails.enabled_patches, :rails, :active_record) == true
3
+
4
+ if defined?(ActiveRecord::Batches)
5
+
6
+ ActiveRecord::Batches.module_eval do
7
+ if enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :active_record, :find_in_relation_batches)
8
+ def find_in_relation_batches(options = {})
9
+ options.assert_valid_keys(:start, :batch_size)
10
+
11
+ relation = self
12
+ start = options[:start]
13
+ batch_size = options[:batch_size] || 1000
14
+
15
+ unless block_given?
16
+ return to_enum(:find_in_relation_batches, options) do
17
+ total = start ? where(table[primary_key].gteq(start)).size : size
18
+ (total - 1).div(batch_size) + 1
19
+ end
20
+ end
21
+
22
+ if logger && (arel.orders.present? || arel.taken.present?)
23
+ logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
24
+ end
25
+
26
+ relation = relation.reorder(batch_order).limit(batch_size)
27
+ #records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a
28
+ records = start ? relation.where(table[primary_key].gteq(start)) : relation
29
+
30
+ while records.any?
31
+ records_size = records.size
32
+ primary_key_offset = records.last.id
33
+ raise ActiveRecordError, "Primary key not included in the custom select clause" unless primary_key_offset
34
+
35
+ yield records
36
+
37
+ break if records_size < batch_size
38
+
39
+ records = relation.where(table[primary_key].gt(primary_key_offset))#.to_a
40
+ end
41
+ end
42
+ end
43
+
44
+ if enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :active_record, :find_relation_each)
45
+ def find_relation_each(options = {})
46
+ if block_given?
47
+ find_in_relation_batches(options) do |records|
48
+ records.each { |record| yield record }
49
+ end
50
+ else
51
+ enum_for :find_relation_each, options do
52
+ options[:start] ? where(table[primary_key].gteq(options[:start])).size : size
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,101 @@
1
+ enabled = RearmedRails.enabled_patches[:rails] == true
2
+ enabled ||= RearmedRails.dig(RearmedRails.enabled_patches, :rails, :active_record) == true
3
+
4
+ if defined?(ActiveRecord)
5
+
6
+ if enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :active_record, :or)
7
+
8
+ if Rails::VERSION::MAJOR > 4
9
+
10
+ module RearmedRails
11
+ class OrChain
12
+ def initialize(scope)
13
+ @scope = scope
14
+ end
15
+
16
+ def method_missing(method, *args, &block)
17
+ other = @scope.klass.unscoped do
18
+ @scope.klass.send(method, *args, &block)
19
+ end
20
+ return @scope.smart_or(other)
21
+ end
22
+ end
23
+ end
24
+
25
+ ActiveRecord::QueryMethods.class_eval do
26
+ def or(opts=nil, *rest)
27
+ if opts.nil?
28
+ return RearmedRails::OrChain.new(self)
29
+ else
30
+ other = opts.is_a?(ActiveRecord::Relation) ? opts : klass.unscoped.where(opts, rest)
31
+
32
+ self.where_clause = self.where_clause.or(other.where_clause)
33
+ self.having_clause = self.having_clause.or(other.having_clause)
34
+
35
+ return self
36
+ end
37
+ end
38
+ end
39
+
40
+ else # rails 4
41
+
42
+ module ActiveRecord
43
+ module Querying
44
+ delegate :or, to: :all
45
+ end
46
+
47
+ module QueryMethods
48
+ class OrChain
49
+ def initialize(scope)
50
+ @scope = scope
51
+ end
52
+
53
+ def method_missing(method, *args, &block)
54
+ right_relation = @scope.klass.unscoped do
55
+ @scope.klass.send(method, *args, &block)
56
+ end
57
+ @scope.or(right_relation)
58
+ end
59
+ end
60
+
61
+ def or(opts = :chain, *rest)
62
+ if opts == :chain
63
+ OrChain.new(self)
64
+ else
65
+ left = self
66
+ right = (ActiveRecord::Relation === opts) ? opts : klass.unscoped.where(opts, rest)
67
+
68
+ unless left.where_values.empty? || right.where_values.empty?
69
+ left.where_values = [left.where_ast.or(right.where_ast)]
70
+ right.where_values = []
71
+ end
72
+
73
+ left = left.merge(right)
74
+ end
75
+ end
76
+
77
+ private # Returns an Arel AST containing only where_values
78
+
79
+ def where_ast
80
+ arel_wheres = []
81
+
82
+ where_values.each do |where|
83
+ arel_wheres << (String === where ? Arel.sql(where) : where)
84
+ end
85
+
86
+ return Arel::Nodes::Grouping.new(Arel::Nodes::And.new(arel_wheres)) if arel_wheres.length >= 2
87
+
88
+ if Arel::Nodes::SqlLiteral === arel_wheres.first
89
+ Arel::Nodes::Grouping.new(arel_wheres.first)
90
+ else
91
+ arel_wheres.first
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ end
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,116 @@
1
+ enabled = RearmedRails.enabled_patches[:rails] == true
2
+ enabled ||= RearmedRails.dig(RearmedRails.enabled_patches, :rails, :helpers) == true
3
+
4
+ if defined?(ActionView::Helpers)
5
+
6
+ if enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :helpers, :link_to_confirm)
7
+ ActionView::Helpers::UrlHelper.module_eval do
8
+ def convert_options_to_data_attributes(options, html_options)
9
+ if html_options
10
+ html_options = html_options.stringify_keys
11
+ html_options['data-remote'] = 'true' if link_to_remote_options?(options) || link_to_remote_options?(html_options)
12
+
13
+ method = html_options.delete('method')
14
+ add_method_to_attributes!(html_options, method) if method
15
+
16
+ ### CUSTOM - behave like Rails 3.2
17
+ confirm = html_options.delete('confirm')
18
+ html_options['data-confirm'] = confirm if confirm
19
+
20
+ html_options
21
+ else
22
+ link_to_remote_options?(options) ? {'data-remote' => 'true'} : {}
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ if enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :helpers, :field_is_array)
29
+ module ActionView
30
+ module Helpers
31
+ module Tags
32
+ class Base
33
+ private
34
+
35
+ original_method = instance_method(:add_default_name_and_id)
36
+ define_method :add_default_name_and_id do |options|
37
+ original_method.bind(self).(options)
38
+
39
+ if options['is_array'] && options['name']
40
+ options['name'] = "#{options['name']}[]"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ module RearmedRails
50
+ module RailsHelpers
51
+ end
52
+ end
53
+
54
+ RearmedRails::RailsHelpers.module_eval do
55
+
56
+ if enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :helpers, :options_for_select_include_blank)
57
+ def options_for_select(container, selected = nil)
58
+ return container if String === container
59
+
60
+ if selected.is_a?(Hash)
61
+ include_blank = selected[:include_blank] || selected['include_blank']
62
+ end
63
+
64
+ selected, disabled = extract_selected_and_disabled(selected).map do |r|
65
+ Array(r).map(&:to_s)
66
+ end
67
+
68
+ options = []
69
+
70
+ if include_blank
71
+ include_blank = '' if include_blank == true
72
+ options.push(content_tag_string(:option, include_blank, {value: ''}))
73
+ end
74
+
75
+ container.each do |element|
76
+ html_attributes = option_html_attributes(element)
77
+ text, value = option_text_and_value(element).map(&:to_s)
78
+
79
+ html_attributes[:selected] ||= option_value_selected?(value, selected)
80
+ html_attributes[:disabled] ||= disabled && option_value_selected?(value, disabled)
81
+ html_attributes[:value] = value
82
+
83
+ options.push content_tag_string(:option, text, html_attributes)
84
+ end
85
+
86
+ options.join("\n").html_safe
87
+ end
88
+ end
89
+
90
+ if enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :helpers, :options_from_collection_for_select_include_blank)
91
+ def options_from_collection_for_select(collection, value_method, text_method, selected = nil)
92
+ options = collection.map do |element|
93
+ [value_for_collection(element, text_method), value_for_collection(element, value_method), option_html_attributes(element)]
94
+ end
95
+
96
+ if selected.is_a?(Hash)
97
+ include_blank = selected[:include_blank] || selected['include_blank']
98
+ end
99
+
100
+ selected, disabled = extract_selected_and_disabled(selected)
101
+
102
+ select_deselect = {
103
+ selected: extract_values_from_collection(collection, value_method, selected),
104
+ disabled: extract_values_from_collection(collection, value_method, disabled),
105
+ include_blank: include_blank
106
+ }
107
+
108
+ options_for_select(options, select_deselect)
109
+ end
110
+ end
111
+ end
112
+
113
+ ActiveSupport.on_load :action_view do
114
+ ActionView::Base.send(:include, RearmedRails::RailsHelpers)
115
+ end
116
+ end
@@ -0,0 +1,58 @@
1
+ enabled = RearmedRails.enabled_patches[:rails] == true
2
+ enabled ||= RearmedRails.dig(RearmedRails.enabled_patches, :rails, :v3) == true
3
+
4
+ if defined?(ActiveRecord) && ActiveRecord::VERSION::MAJOR < 4
5
+
6
+ if enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :v3, :all)
7
+ ActiveRecord::FinderMethods.module_eval do
8
+ def all(*args)
9
+ args.any? ? apply_finder_options(args.first) : self
10
+ end
11
+ end
12
+ end
13
+
14
+ if enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :v3, :pluck)
15
+ ActiveRecord::Relation.class_eval do
16
+ def pluck(*args)
17
+ args.map! do |column_name|
18
+ if column_name.is_a?(Symbol) && column_names.include?(column_name.to_s)
19
+ "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
20
+ else
21
+ column_name.to_s
22
+ end
23
+ end
24
+
25
+ relation = clone
26
+ relation.select_values = args
27
+ klass.connection.select_all(relation.arel).map! do |attributes|
28
+ initialized_attributes = klass.initialize_attributes(attributes)
29
+ attributes.map do |key, attr|
30
+ klass.type_cast_attribute(key, initialized_attributes)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ if enabled || RearmedRails.dig(RearmedRails.enabled_patches, :rails, :v3, :update_columns)
38
+ ActiveRecord::Persistence::ClassMethods.module_eval do
39
+ def update_columns(attributes)
40
+ raise ActiveRecordError, "cannot update a new record" if new_record?
41
+ raise ActiveRecordError, "cannot update a destroyed record" if destroyed?
42
+
43
+ attributes.each_key do |key|
44
+ raise ActiveRecordError, "#{key.to_s} is marked as readonly" if self.class.readonly_attributes.include?(key.to_s)
45
+ end
46
+
47
+ updated_count = self.class.unscoped.where(self.class.primary_key => id).update_all(attributes)
48
+
49
+ attributes.each do |k, v|
50
+ raw_write_attribute(k, v)
51
+ end
52
+
53
+ updated_count == 1
54
+ end
55
+ end
56
+ end
57
+
58
+ end
@@ -0,0 +1,3 @@
1
+ module RearmedRails
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,52 @@
1
+ require 'rearmed_rails/version'
2
+
3
+ module RearmedRails
4
+
5
+ @enabled_patches = eval(File.read(File.join(File.dirname(__FILE__), 'rearmed_rails/default_enabled_patches.hash')))
6
+
7
+ def self.enabled_patches=(val)
8
+ @enabled_patches = val
9
+ end
10
+
11
+ def self.enabled_patches
12
+ @enabled_patches
13
+ end
14
+
15
+ private
16
+
17
+ def self.dig(collection, *values)
18
+ current_val = nil
19
+ current_collection = collection
20
+ values.each_with_index do |val,i|
21
+ if i+1 == values.length
22
+ if (current_collection.is_a?(Array) && val.is_a?(Integer)) || (current_collection.is_a?(Hash) && ['String','Symbol'].include?(val.class.name))
23
+ current_val = current_collection[val]
24
+ else
25
+ current_val = nil
26
+ end
27
+ elsif current_collection.is_a?(Array)
28
+ if val.is_a?(Integer)
29
+ current_collection = current_collection[val]
30
+ next
31
+ else
32
+ current_val = nil
33
+ break
34
+ end
35
+ elsif current_collection.is_a?(Hash)
36
+ if ['Symbol','String'].include?(val.class.name)
37
+ current_collection = current_collection[val]
38
+ next
39
+ else
40
+ current_val = nil
41
+ break
42
+ end
43
+ else
44
+ current_val = nil
45
+ break
46
+ end
47
+ end
48
+
49
+ return current_val
50
+ end
51
+
52
+ end
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env ruby -w
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+
6
+ require 'yaml'
7
+ require 'minitest'
8
+
9
+ require 'rearmed_rails'
10
+
11
+ require 'minitest/autorun'
12
+
13
+ class TestRearmedRails < MiniTest::Test
14
+ def setup
15
+ Minitest::Assertions.module_eval do
16
+ alias_method :eql, :assert_equal
17
+ end
18
+
19
+ RearmedRails.enabled_patches = {
20
+ rails: true,
21
+ minitest: true
22
+ }
23
+ require 'rearmed_rails/apply_patches'
24
+ end
25
+
26
+ def test_minitest
27
+ str = 'first'
28
+ assert_changed "str" do
29
+ str = 'second'
30
+ end
31
+
32
+ str = 'first'
33
+ assert_changed ->{ str } do
34
+ str = 'second'
35
+ end
36
+
37
+ name = 'first'
38
+ assert_changed lambda{ name } do
39
+ name = 'second'
40
+ end
41
+
42
+ name = 'first'
43
+ assert_not_changed 'name' do
44
+ name = 'first'
45
+ end
46
+
47
+ name = 'first'
48
+ assert_not_changed ->{ name } do
49
+ name = 'first'
50
+ end
51
+
52
+ name = 'first'
53
+ assert_not_changed lambda{ name } do
54
+ name = 'first'
55
+ end
56
+ end
57
+
58
+ def test_general_rails
59
+ # THE MOST IMPORTANT TESTS HERE WOULD BE dedupe, reset_auto_increment, reset_table
60
+
61
+ #Post.pluck_to_hash(:name, :category, :id)
62
+ #Post.pluck_to_struct(:name, :category, :id)
63
+
64
+ #Post.find_or_create(name: 'foo', content: 'bar') # use this instead of the super confusing first_or_create method
65
+ #Post.find_or_create!(name: 'foo', content: 'bar')
66
+
67
+ #Post.find_duplicates # return active record relation of all records that have duplicates
68
+ #Post.find_duplicates(:name) # find duplicates based on the name attribute
69
+ #Post.find_duplicates([:name, :category]) # find duplicates based on the name & category attribute
70
+ #Post.find_duplicates(name: 'A Specific Name')
71
+
72
+ #Post.reset_table # delete all records from table and reset autoincrement column (id), works with mysql/mariadb/postgresql/sqlite
73
+ ## or with options
74
+ #Post.reset_table(delete_method: :destroy) # to ensure all callbacks are fired
75
+
76
+ #Post.reset_auto_increment # reset mysql/mariadb/postgresql/sqlite auto-increment column, if contains records then defaults to starting from next available number
77
+ ## or with options
78
+ #Post.reset_auto_increment(value: 1, column: :id) # column option is only relevant for postgresql
79
+
80
+ #Post.find_in_relation_batches # this returns a relation instead of an array
81
+ #Post.find_relation_each # this returns a relation instead of an array
82
+ end
83
+
84
+ def test_rails_3
85
+ #my_hash.compact
86
+ #my_hash.compact!
87
+ #Post.all # Now returns AR relation
88
+ #Post.first.update_columns(a: 'foo', b: 'bar')
89
+ #Post.pluck(:name, :id) # adds multi column pluck support ex. => [['first', 1], ['second', 2], ['third', 3]]
90
+ end
91
+
92
+ def test_rails_4
93
+ #Post.where(name: 'foo').or.where(content: 'bar')
94
+ #Post.where(name: 'foo').or.my_custom_scope
95
+ #Post.where(name: 'foo').or(Post.where(content: 'bar'))
96
+ #Post.where(name: 'foo).or(content: 'bar')
97
+
98
+ #= link_to 'Delete', post_path(post), method: :delete, confirm: "Are you sure you want to delete this post?"
99
+ # returns to rails 3 behaviour of allowing confirm attribute as well as data-confirm
100
+ end
101
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rearmed_rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Weston Ganger
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-02-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: A collection of helpful methods and monkey patches for Rails
56
+ email: westonganger@gmail.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - CHANGELOG.md
62
+ - LICENSE
63
+ - README.md
64
+ - Rakefile
65
+ - lib/generators/rearmed_rails/setup_generator.rb
66
+ - lib/rearmed_rails.rb
67
+ - lib/rearmed_rails/apply_patches.rb
68
+ - lib/rearmed_rails/default_enabled_patches.hash
69
+ - lib/rearmed_rails/monkey_patches/minitest.rb
70
+ - lib/rearmed_rails/monkey_patches/rails/active_record/base.rb
71
+ - lib/rearmed_rails/monkey_patches/rails/active_record/batches.rb
72
+ - lib/rearmed_rails/monkey_patches/rails/active_record/query_methods.rb
73
+ - lib/rearmed_rails/monkey_patches/rails/helpers.rb
74
+ - lib/rearmed_rails/monkey_patches/rails/v3.rb
75
+ - lib/rearmed_rails/version.rb
76
+ - test/tc_rearmed.rb
77
+ homepage: https://github.com/westonganger/rearmed_rails
78
+ licenses: []
79
+ metadata: {}
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 1.9.3
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 2.6.8
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: A collection of helpful methods and monkey patches for Rails
100
+ test_files:
101
+ - test/tc_rearmed.rb