rails_core_extensions 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.hound.yml +2 -0
  4. data/.rspec +2 -0
  5. data/.ruby-style.yml +233 -0
  6. data/.travis.yml +14 -0
  7. data/Gemfile +2 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +62 -0
  10. data/Rakefile +12 -0
  11. data/gemfiles/rails3.gemfile +13 -0
  12. data/gemfiles/rails4.gemfile +14 -0
  13. data/lib/rails_core_extensions.rb +41 -0
  14. data/lib/rails_core_extensions/action_controller_sortable.rb +22 -0
  15. data/lib/rails_core_extensions/action_view_currency_extensions.rb +26 -0
  16. data/lib/rails_core_extensions/action_view_extensions.rb +62 -0
  17. data/lib/rails_core_extensions/action_view_has_many_extensions.rb +32 -0
  18. data/lib/rails_core_extensions/activatable.rb +38 -0
  19. data/lib/rails_core_extensions/active_model_extensions.rb +47 -0
  20. data/lib/rails_core_extensions/active_record_cache_all_attributes.rb +43 -0
  21. data/lib/rails_core_extensions/active_record_cloning.rb +81 -0
  22. data/lib/rails_core_extensions/active_record_extensions.rb +228 -0
  23. data/lib/rails_core_extensions/active_record_liquid_extensions.rb +32 -0
  24. data/lib/rails_core_extensions/active_support_concern.rb +134 -0
  25. data/lib/rails_core_extensions/breadcrumb.rb +170 -0
  26. data/lib/rails_core_extensions/caches_action_without_host.rb +17 -0
  27. data/lib/rails_core_extensions/concurrency.rb +152 -0
  28. data/lib/rails_core_extensions/position_initializer.rb +27 -0
  29. data/lib/rails_core_extensions/railtie.rb +7 -0
  30. data/lib/rails_core_extensions/sortable.rb +52 -0
  31. data/lib/rails_core_extensions/tasks/position_initializer.rake +12 -0
  32. data/lib/rails_core_extensions/time_with_zone.rb +16 -0
  33. data/lib/rails_core_extensions/version.rb +3 -0
  34. data/rails_core_extensions.gemspec +38 -0
  35. data/spec/action_controller_sortable_spec.rb +52 -0
  36. data/spec/action_view_extensions_spec.rb +25 -0
  37. data/spec/active_model_extensions_spec.rb +130 -0
  38. data/spec/active_record_extensions_spec.rb +126 -0
  39. data/spec/breadcrumb_spec.rb +85 -0
  40. data/spec/concurrency_spec.rb +110 -0
  41. data/spec/position_initializer_spec.rb +48 -0
  42. data/spec/schema.rb +17 -0
  43. data/spec/spec_helper.rb +31 -0
  44. data/spec/spec_helper_model_base.rb +37 -0
  45. data/spec/support/coverage_loader.rb +26 -0
  46. metadata +294 -0
@@ -0,0 +1,22 @@
1
+ module RailsCoreExtensions
2
+ module ActionControllerSortable
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def sortable
9
+ include RailsCoreExtensions::ActionControllerSortable::InstanceMethods
10
+ end
11
+ end
12
+
13
+ module InstanceMethods
14
+ def sort
15
+ RailsCoreExtensions::Sortable.new(params, controller_name).sort
16
+
17
+ render :update do |page|
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,26 @@
1
+ module ActionView
2
+ module Helpers
3
+ module FormTagHelper
4
+ # Create currency selector tag -- see select_tag for options
5
+ def currency_select_tag(name, current_value, options={})
6
+ selectable_options = []
7
+ selectable_options << ["--", nil] if options[:include_blank]
8
+ selectable_options += EnabledCurrency.all.map(&:iso_code) unless EnabledCurrency.all.empty?
9
+ select_tag(name, options_for_select(selectable_options, current_value), options)
10
+ end
11
+ end
12
+
13
+ module DateHelper
14
+ def currency_select(object_name, method, options = {})
15
+ value = options[:object] || EnabledCurrency.base_currency
16
+ currency_select_tag("#{object_name}[#{method}]", value, options.merge(:id => "#{object_name}_#{method}"))
17
+ end
18
+ end
19
+
20
+ class FormBuilder
21
+ def currency_select(method, options = {})
22
+ @template.currency_select(@object_name, method, options.merge(:object => @object.send(method)))
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,62 @@
1
+ module RailsCoreExtensions
2
+ module ActionViewExtensions
3
+ def textilize(content)
4
+ super(h(content)).html_safe
5
+ end
6
+
7
+ # Will recursively parse a Hash and any sub hashes to be normal hashes
8
+ # This is useful when exporting hashes to non ruby systems, which don't understand HashWithIndifferentAccess
9
+ def hashify(element)
10
+ if element.is_a? Hash
11
+ element = element.to_hash if element.is_a?(HashWithIndifferentAccess)
12
+ element.each_pair do |key, value|
13
+ element[key] = hashify(value)
14
+ end
15
+ else
16
+ # Must hashify enumerables encase their sub items are hashes
17
+ # Can't enumerate through string as it returns strings, which are also enumerable (stack level too deep)
18
+ if element.respond_to?(:each) && !element.is_a?(String)
19
+ element.map{ |sub| hashify(sub) }
20
+ else
21
+ element
22
+ end
23
+ end
24
+ end
25
+
26
+ # Generates a tooltip with given text
27
+ # text is textilized before display
28
+ def tooltip(hover_element_id, text, title='')
29
+ content = "<div style='width: 25em'>#{textilize(text)}</div>"
30
+ "<script>" +
31
+ "new Tip('#{hover_element_id}', '#{escape_javascript(content)}',"+
32
+ "{title : '#{escape_javascript title}', className: 'silver_smaller_div',"+
33
+ "showOn: 'mouseover', hideOn: { event: 'mouseout' }, fixed: false});"+
34
+ "</script>"
35
+ end
36
+
37
+ def expandable_list_for(objects, show = 4)
38
+ first, others = objects[0..(show-1)], objects[show..(objects.size-1)]
39
+ first.each do |o|
40
+ yield o
41
+ end
42
+ if others
43
+ content_tag 'div', :id => 'others', :style => 'display: none' do
44
+ others.each do |o|
45
+ yield o
46
+ end
47
+ end
48
+ "#{others.size} Others - ".html_safe + link_to_function("Show/Hide", "$('others').toggle()")
49
+ end
50
+ end
51
+
52
+ def boolean_select_tag(name, *args)
53
+ options = args.extract_options!
54
+ options ||= {}
55
+ opts = [['Yes', '1'], ['No', '0']]
56
+ opts = [blank_option] + opts if options[:include_blank]
57
+ select_tag name, options_for_select(opts, options[:selected])
58
+ end
59
+ end
60
+ end
61
+
62
+ ActionView::Base.send(:include, RailsCoreExtensions::ActionViewExtensions) if defined?(ActionView::Base)
@@ -0,0 +1,32 @@
1
+ module RailsCoreExtensions
2
+ module HasManyExtensions
3
+ module Tags
4
+ def hm_check_box(object_name, method, options = {})
5
+ object = options.delete(:object)
6
+ parent = options.delete(:parent) || instance_variable_get(object_name)
7
+ objects = options.delete(:objects) || parent.send(method)
8
+ check_box_tag("#{object_name}[#{method}][]", object, objects.include?(object), options.merge(:id => "#{object_name}_#{method}"))
9
+ end
10
+
11
+ def hm_empty_array(object_name, method)
12
+ hidden_field_tag "#{object_name}[#{method}][]"
13
+ end
14
+ end
15
+
16
+ module FormBuilder
17
+ def hm_empty_array(method)
18
+ @habtm_fields ||= {}
19
+ @habtm_fields[method] = @object.send(method)
20
+ @template.hm_empty_array(@object_name, method)
21
+ end
22
+
23
+ def hm_check_box(method, object, options = {})
24
+ empty = (hm_empty_array(method) unless @habtm_fields && @habtm_fields[method])
25
+ (empty || '').html_safe + @template.hm_check_box(@object_name, method, options.merge(:object => object, :parent => @object, :objects => @habtm_fields[method]))
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ ActionView::Base.send(:include, RailsCoreExtensions::HasManyExtensions::Tags) if defined?(ActionView::Base)
32
+ ActionView::Helpers::FormBuilder.send(:include, RailsCoreExtensions::HasManyExtensions::FormBuilder) if defined?(ActionView::Base)
@@ -0,0 +1,38 @@
1
+ module Activatable
2
+
3
+ def self.included(controller)
4
+ controller.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+
9
+ def activatable
10
+ include Activatable::InstanceMethods
11
+ end
12
+
13
+ end
14
+
15
+ module InstanceMethods
16
+
17
+ def activate(success_block = nil)
18
+ resource.active = params[:active].presence || false
19
+ action = resource.active ? 'activate' : 'inactivate'
20
+
21
+ resource.save!
22
+
23
+ success_block ||= -> {
24
+ flash[:success] = "#{resource} #{action}d"
25
+ redirect_to(collection_path)
26
+ }
27
+
28
+ success_block.call
29
+
30
+ rescue ActiveRecord::ActiveRecordError => e
31
+ resource.errors.add(:base, "Failed to #{action}: " + e.message)
32
+ flash[:error] = resource.errors.full_messages.to_sentence
33
+ redirect_to(collection_path)
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,47 @@
1
+ module ActiveModelExtensions
2
+ module Validations
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ # Validates the presence of the required fields identified in a rule-string.
8
+ #
9
+ # Similar to validates_presence_of macro, but is an INSTANCE method.
10
+ # This allows it to vary depending on custom settings.
11
+ #
12
+ # Example:
13
+ # validate_required_fields "field1,field2 or field4"
14
+ #
15
+
16
+ # The string is a CSV of required field rules, where each field rule is:
17
+ # - the name of a required field
18
+ # - OR a set of required field names spearated by 'or' (where only ONE is required)
19
+ #
20
+ class CustomPresenceValidator < ActiveModel::Validator
21
+ def validate(record)
22
+ required_fields = Array.wrap(@options[:attributes]).first.call || []
23
+ return if required_fields.empty?
24
+
25
+ required_fields.flatten.each do |required_field|
26
+ if required_field.include? ' or '
27
+ fields = required_field.split(' or ')
28
+ if fields.all? { |field| record.send(field).to_s.blank? }
29
+ record.errors.add(:base, "One of %s is required" % fields.map(&:humanize).to_sentence)
30
+ end
31
+ else
32
+ if record.send(required_field).to_s.blank?
33
+ record.errors.add(required_field, "is required")
34
+ end
35
+ end
36
+ end
37
+
38
+ end
39
+ end
40
+
41
+ module ClassMethods
42
+ def validate_presence_by_custom_rules(*with)
43
+ validates_with CustomPresenceValidator, _merge_attributes(with)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,43 @@
1
+ module ActiveRecordCacheAllAttributes
2
+ def self.included(base)
3
+ base.extend ClassMethods
4
+ end
5
+
6
+ module InstanceMethods
7
+ def clear_attribute_cache
8
+ self.class.cache.delete("#{self.class.name}.attribute_cache") if self.class.should_cache?
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ def cache
14
+ Rails.cache
15
+ end
16
+
17
+ def attribute_cache
18
+ cache_key = "#{self.name}.attribute_cache"
19
+ if self.should_cache?
20
+ cache.read(cache_key) || self.generate_cache(cache_key)
21
+ else
22
+ self.generate_attributes_hash
23
+ end
24
+ end
25
+
26
+ def generate_attributes_hash
27
+ scope = self
28
+ scope = scope.ordered if respond_to?(:ordered)
29
+ Hash[scope.all.map { |o| [o.send(self.cache_attributes_by), o.attributes] }]
30
+ end
31
+
32
+ def generate_cache(cache_key)
33
+ if (cache_value = generate_attributes_hash)
34
+ cache.write(cache_key, cache_value)
35
+ end
36
+ cache_value
37
+ end
38
+
39
+ def should_cache?
40
+ Rails.configuration.action_controller.perform_caching
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,81 @@
1
+ module ActiveRecordCloning
2
+
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ module ClassMethods
8
+
9
+ def inherited(subclass)
10
+ super
11
+ subclass.cloned_attributes_hash = cloned_attributes_hash
12
+ end
13
+
14
+ def attributes_included_in_cloning
15
+ cloned_attributes_hash[:include].dup
16
+ end
17
+
18
+ def attributes_excluded_from_cloning
19
+ cloned_attributes_hash[:exclude].dup
20
+ end
21
+
22
+ def clones_attributes(*attributes)
23
+ cloned_attributes_hash[:include] = attributes.map(&:to_sym)
24
+ end
25
+
26
+ def clones_attributes_except(*attributes)
27
+ cloned_attributes_hash[:exclude] = attributes.map(&:to_sym)
28
+ end
29
+
30
+ protected
31
+
32
+ def cloned_attributes_hash
33
+ @cloned_attributes ||= {:include => [], :exclude => []}
34
+ end
35
+
36
+ def cloned_attributes_hash=(attributes_hash)
37
+ @cloned_attributes = attributes_hash
38
+ end
39
+
40
+ end
41
+
42
+ module InstanceMethods
43
+
44
+ def self.included(base)
45
+ base.class_eval %q{
46
+ alias_method :base_clone_attributes, :clone_attributes
47
+ def clone_attributes(reader_method = :read_attribute, attributes = {})
48
+ allowed = cloned_attributes
49
+ base_clone_attributes(reader_method, attributes).delete_if { |k,v| !allowed.include?(k.to_sym) }
50
+ end
51
+ }
52
+ end
53
+
54
+ def clone_excluding(excludes=[])
55
+ method = ActiveRecord::Base.instance_methods(false).include?(:clone) ? :clone : :dup
56
+ cloned = send(method)
57
+
58
+ excludes ||= []
59
+ excludes = [excludes] unless excludes.is_a?(Enumerable)
60
+
61
+ excludes.each do |excluded_attr|
62
+ attr_writer = (excluded_attr.to_s + '=').to_sym
63
+ cloned.send attr_writer, nil
64
+ end
65
+
66
+ cloned
67
+ end
68
+
69
+ private
70
+
71
+ def cloned_attributes
72
+ included_attributes = if self.class.attributes_included_in_cloning.empty?
73
+ attribute_names.map(&:to_sym)
74
+ else
75
+ attribute_names.map(&:to_sym) & self.class.attributes_included_in_cloning
76
+ end
77
+ included_attributes - self.class.attributes_excluded_from_cloning
78
+ end
79
+ end
80
+
81
+ end
@@ -0,0 +1,228 @@
1
+ module ActiveRecordExtensions
2
+ def self.included(base)
3
+ base.extend ClassMethods
4
+ end
5
+
6
+ module ClassMethods
7
+ # Like establish_connection but postfixes the key with the rails environment
8
+ # e.g. database('hello') in development will look for the database
9
+ # which in config/database.yml is called hello_development
10
+ def database(key)
11
+ establish_connection("#{key}_#{Rails.env}")
12
+ end
13
+
14
+ def cache_all_attributes(options = {})
15
+ method = options[:by] || 'id'
16
+ class_eval <<-CACHE
17
+ after_save :clear_attribute_cache
18
+ after_destroy :clear_attribute_cache
19
+ cattr_accessor :cache_attributes_by
20
+ self.cache_attributes_by = '#{method}'
21
+ CACHE
22
+ extend ActiveRecordCacheAllAttributes::ClassMethods
23
+ include ActiveRecordCacheAllAttributes::InstanceMethods
24
+ end
25
+
26
+ # Create a new object from the attributes passed in
27
+ # OR update an existing
28
+ #
29
+ # If an :id attribute is present it will assume it's an existing record, and needs update
30
+ def new_or_update!(hash={}, options = {:hard_update => true})
31
+ hash.symbolize_keys!
32
+ if hash[:id].blank?
33
+ self.new(hash)
34
+ else
35
+ rec = self.find(hash[:id])
36
+ if options[:hard_update]
37
+ rec.update_attributes!(hash.except(:id))
38
+ else
39
+ rec.update_attributes(hash.except(:id))
40
+ end
41
+ rec
42
+ end
43
+ end
44
+
45
+ def enum(field, values, options = {})
46
+ const_set("#{field.to_s.upcase}_OPTIONS", values)
47
+
48
+ select_options = if values.is_a?(Array)
49
+ values.map.with_index{|v, i| [v.to_s.humanize, i]}
50
+ elsif values.is_a?(Hash)
51
+ values.values.map.with_index{|v, i| [v, i]}
52
+ end
53
+ const_set("#{field.to_s.upcase}_SELECT_OPTIONS", select_options)
54
+
55
+ values.each.with_index do |value, i|
56
+ const_set("#{field.to_s.upcase}_#{value.to_s.upcase}", i)
57
+ method_name = options[:short_name] ? "#{value}?" : "#{field}_#{value}?"
58
+ class_eval <<-ENUM
59
+ def #{method_name}
60
+ #{field} == #{i}
61
+ end
62
+ ENUM
63
+ end
64
+ class_eval <<-ENUM
65
+ def #{field}_name
66
+ #{field.to_s.upcase}_OPTIONS[#{field}]
67
+ end
68
+ ENUM
69
+ end
70
+
71
+ def optional_fields(*possible_fields)
72
+ @optional_fields_loader = possible_fields.pop if possible_fields.last.is_a?(Proc)
73
+
74
+ class << self
75
+ def enabled_fields
76
+ @enabled_fields || @optional_fields_loader.try(:call)
77
+ end
78
+
79
+ def enabled_fields=(fields)
80
+ @enabled_fields = fields
81
+ end
82
+ end
83
+
84
+ possible_fields.each do |field|
85
+ instance_eval <<-EVAL
86
+ def #{field}_enabled?
87
+ enabled_fields.include?(:#{field})
88
+ end
89
+ EVAL
90
+ end
91
+ end
92
+
93
+
94
+ def position_helpers_for(*collections)
95
+ collections.each do |collection|
96
+ class_eval <<-EVAL
97
+ after_save do |record|
98
+ record.rebalance_#{collection.to_s.singularize}_positions!
99
+ end
100
+
101
+ def assign_#{collection.to_s.singularize}_position(object)
102
+ object.position = (#{collection}.last.try(:position) || 0) + 1 unless object.position
103
+ end
104
+
105
+ def rebalance_#{collection.to_s.singularize}_positions!(object = nil)
106
+ reload
107
+ #{collection}.sort_by(&:position).each_with_index do |o, index|
108
+ if o.position != (index + 1)
109
+ o.update_attribute(:position, index + 1)
110
+ end
111
+ end
112
+ end
113
+ EVAL
114
+ end
115
+ end
116
+
117
+
118
+ # Validates presence of -- but works on parent within accepts_nested_attributes
119
+ #
120
+ def validates_presence_of_parent(foreign_key)
121
+ after_save do |record|
122
+ unless record.send(foreign_key)
123
+ record.errors.add_on_blank(foreign_key)
124
+ raise ActiveRecord::ActiveRecordError, record.errors.full_messages.to_sentence
125
+ end
126
+ end
127
+ end
128
+
129
+
130
+ # Run a block, being respectful of connection pools
131
+ #
132
+ # Useful for when you're not in the standard rails request process,
133
+ # since normally rails will take, then clear you're connection in the
134
+ # processing of the request.
135
+ #
136
+ # If you don't do this in, say, a command line task with threads, then
137
+ # you'll run out of connections after 5 x Threads are run simultaneously...
138
+ def with_connection_pooling
139
+ # Verify active connections and remove and disconnect connections
140
+ # associated with stale threads.
141
+ ActiveRecord::Base.verify_active_connections!
142
+
143
+ yield
144
+
145
+ # This code checks in the connection being used by the current thread back
146
+ # into the connection pool. It repeats this if you are using multiple
147
+ # connection pools. It will not disconnect the connection.
148
+ #
149
+ # Returns any connections in use by the current thread back to the pool,
150
+ # and also returns connections to the pool cached by threads that are no
151
+ # longer alive.
152
+ ActiveRecord::Base.clear_active_connections!
153
+
154
+ end
155
+
156
+
157
+ def translate(key, options = {})
158
+ klass = self
159
+ klass = klass.superclass while klass.superclass != ActiveRecord::Base
160
+ I18n.translate key, options.merge(:scope => klass.name.tableize.singularize)
161
+ end
162
+
163
+
164
+ def t(key, options = {})
165
+ self.translate(key, options)
166
+ end
167
+
168
+ end
169
+
170
+
171
+ module InstanceMethods
172
+
173
+
174
+ def all_errors
175
+ errors_hash = {}
176
+ self.errors.each do |attr, msg|
177
+ (errors_hash[attr] ||= []) << if self.respond_to?(attr) && (record_attr = self.send(attr)).is_a?(ActiveRecord::Base)
178
+ record_attr.all_errors
179
+ else
180
+ msg
181
+ end
182
+ end
183
+ errors_hash
184
+ end
185
+
186
+
187
+ def to_drop
188
+ @drop_class ||= (self.class.name+'Drop').constantize
189
+ @drop_class.new(self)
190
+ end
191
+ alias_method :to_liquid, :to_drop
192
+
193
+
194
+ # A unique id - even if you are unsaved!
195
+ def unique_id
196
+ id || @generated_dom_id || (@generated_dom_id = Time.now.to_f.to_s.gsub('.', '_'))
197
+ end
198
+
199
+
200
+ #getting audits
201
+ def audit_log
202
+ return (self.methods.include?('audits') ? self.audits : [])
203
+ end
204
+
205
+
206
+
207
+ private
208
+
209
+ def t(key, options = {})
210
+ self.class.translate(key, options)
211
+ end
212
+
213
+
214
+ def transfer_records(klass, objects, options = {})
215
+ record_ids = objects.flat_map { |o|
216
+ o.send(klass.name.underscore + '_ids')
217
+ }
218
+ unless record_ids.empty?
219
+ options[:foreign_key] ||= self.class.name.underscore + '_id'
220
+ update_options = options.except(:foreign_key)
221
+ update_options[options[:foreign_key]] = id
222
+ klass.where(id: record_ids).update_all(update_options)
223
+ end
224
+ end
225
+
226
+ end
227
+
228
+ end