rails_core_extensions 0.0.1
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 +7 -0
- data/.gitignore +4 -0
- data/.hound.yml +2 -0
- data/.rspec +2 -0
- data/.ruby-style.yml +233 -0
- data/.travis.yml +14 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +62 -0
- data/Rakefile +12 -0
- data/gemfiles/rails3.gemfile +13 -0
- data/gemfiles/rails4.gemfile +14 -0
- data/lib/rails_core_extensions.rb +41 -0
- data/lib/rails_core_extensions/action_controller_sortable.rb +22 -0
- data/lib/rails_core_extensions/action_view_currency_extensions.rb +26 -0
- data/lib/rails_core_extensions/action_view_extensions.rb +62 -0
- data/lib/rails_core_extensions/action_view_has_many_extensions.rb +32 -0
- data/lib/rails_core_extensions/activatable.rb +38 -0
- data/lib/rails_core_extensions/active_model_extensions.rb +47 -0
- data/lib/rails_core_extensions/active_record_cache_all_attributes.rb +43 -0
- data/lib/rails_core_extensions/active_record_cloning.rb +81 -0
- data/lib/rails_core_extensions/active_record_extensions.rb +228 -0
- data/lib/rails_core_extensions/active_record_liquid_extensions.rb +32 -0
- data/lib/rails_core_extensions/active_support_concern.rb +134 -0
- data/lib/rails_core_extensions/breadcrumb.rb +170 -0
- data/lib/rails_core_extensions/caches_action_without_host.rb +17 -0
- data/lib/rails_core_extensions/concurrency.rb +152 -0
- data/lib/rails_core_extensions/position_initializer.rb +27 -0
- data/lib/rails_core_extensions/railtie.rb +7 -0
- data/lib/rails_core_extensions/sortable.rb +52 -0
- data/lib/rails_core_extensions/tasks/position_initializer.rake +12 -0
- data/lib/rails_core_extensions/time_with_zone.rb +16 -0
- data/lib/rails_core_extensions/version.rb +3 -0
- data/rails_core_extensions.gemspec +38 -0
- data/spec/action_controller_sortable_spec.rb +52 -0
- data/spec/action_view_extensions_spec.rb +25 -0
- data/spec/active_model_extensions_spec.rb +130 -0
- data/spec/active_record_extensions_spec.rb +126 -0
- data/spec/breadcrumb_spec.rb +85 -0
- data/spec/concurrency_spec.rb +110 -0
- data/spec/position_initializer_spec.rb +48 -0
- data/spec/schema.rb +17 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/spec_helper_model_base.rb +37 -0
- data/spec/support/coverage_loader.rb +26 -0
- metadata +294 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
module RailsCoreExtensions
|
2
|
+
module ActiveRecordLiquidExtensions
|
3
|
+
def self.included(base)
|
4
|
+
base.extend ClassMethods
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def validates_liquid(field)
|
10
|
+
field = field.to_sym
|
11
|
+
before_validation do |record|
|
12
|
+
begin
|
13
|
+
Liquid::Template.parse(record.send(field))
|
14
|
+
rescue Liquid::SyntaxError => e
|
15
|
+
record.errors.add(field, "Liquid Syntax Error: #{e}")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def liquid_field(field)
|
21
|
+
class_eval <<-CODE
|
22
|
+
def parsed_#{field}
|
23
|
+
Liquid::Template.parse(#{field})
|
24
|
+
end
|
25
|
+
|
26
|
+
def render_#{field}(*args)
|
27
|
+
parsed_#{field}.render!(*args)
|
28
|
+
end
|
29
|
+
CODE
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module ActiveSupport
|
2
|
+
# A typical module looks like this:
|
3
|
+
#
|
4
|
+
# module M
|
5
|
+
# def self.included(base)
|
6
|
+
# base.extend ClassMethods
|
7
|
+
# base.send(:include, InstanceMethods)
|
8
|
+
# scope :disabled, where(:disabled => true)
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# module ClassMethods
|
12
|
+
# ...
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# module InstanceMethods
|
16
|
+
# ...
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# By using <tt>ActiveSupport::Concern</tt> the above module could instead be written as:
|
21
|
+
#
|
22
|
+
# require 'active_support/concern'
|
23
|
+
#
|
24
|
+
# module M
|
25
|
+
# extend ActiveSupport::Concern
|
26
|
+
#
|
27
|
+
# included do
|
28
|
+
# scope :disabled, where(:disabled => true)
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# module ClassMethods
|
32
|
+
# ...
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# module InstanceMethods
|
36
|
+
# ...
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# Moreover, it gracefully handles module dependencies. Given a +Foo+ module and a +Bar+
|
41
|
+
# module which depends on the former, we would typically write the following:
|
42
|
+
#
|
43
|
+
# module Foo
|
44
|
+
# def self.included(base)
|
45
|
+
# base.class_eval do
|
46
|
+
# def self.method_injected_by_foo
|
47
|
+
# ...
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
# end
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# module Bar
|
54
|
+
# def self.included(base)
|
55
|
+
# base.method_injected_by_foo
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# class Host
|
60
|
+
# include Foo # We need to include this dependency for Bar
|
61
|
+
# include Bar # Bar is the module that Host really needs
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We could try to hide
|
65
|
+
# these from +Host+ directly including +Foo+ in +Bar+:
|
66
|
+
#
|
67
|
+
# module Bar
|
68
|
+
# include Foo
|
69
|
+
# def self.included(base)
|
70
|
+
# base.method_injected_by_foo
|
71
|
+
# end
|
72
|
+
# end
|
73
|
+
#
|
74
|
+
# class Host
|
75
|
+
# include Bar
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt> is the +Bar+ module,
|
79
|
+
# not the +Host+ class. With <tt>ActiveSupport::Concern</tt>, module dependencies are properly resolved:
|
80
|
+
#
|
81
|
+
# require 'active_support/concern'
|
82
|
+
#
|
83
|
+
# module Foo
|
84
|
+
# extend ActiveSupport::Concern
|
85
|
+
# included do
|
86
|
+
# class_eval do
|
87
|
+
# def self.method_injected_by_foo
|
88
|
+
# ...
|
89
|
+
# end
|
90
|
+
# end
|
91
|
+
# end
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# module Bar
|
95
|
+
# extend ActiveSupport::Concern
|
96
|
+
# include Foo
|
97
|
+
#
|
98
|
+
# included do
|
99
|
+
# self.method_injected_by_foo
|
100
|
+
# end
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# class Host
|
104
|
+
# include Bar # works, Bar takes care now of its dependencies
|
105
|
+
# end
|
106
|
+
#
|
107
|
+
module Concern
|
108
|
+
def self.extended(base)
|
109
|
+
base.instance_variable_set("@_dependencies", [])
|
110
|
+
end
|
111
|
+
|
112
|
+
def append_features(base)
|
113
|
+
if base.instance_variable_defined?("@_dependencies")
|
114
|
+
base.instance_variable_get("@_dependencies") << self
|
115
|
+
return false
|
116
|
+
else
|
117
|
+
return false if base < self
|
118
|
+
@_dependencies.each { |dep| base.send(:include, dep) }
|
119
|
+
super
|
120
|
+
base.extend const_get("ClassMethods") if const_defined?("ClassMethods")
|
121
|
+
base.send :include, const_get("InstanceMethods") if const_defined?("InstanceMethods")
|
122
|
+
base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block")
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def included(base = nil, &block)
|
127
|
+
if base.nil?
|
128
|
+
@_included_block = block
|
129
|
+
else
|
130
|
+
super
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
module RailsCoreExtensions
|
2
|
+
|
3
|
+
module Breadcrumb
|
4
|
+
|
5
|
+
def breadcrumbs(object_or_nested_array = nil)
|
6
|
+
breadcrumbs_builder_for(object_or_nested_array).breadcrumbs
|
7
|
+
end
|
8
|
+
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def breadcrumbs_builder_for(object_or_nested_array)
|
13
|
+
BreadcrumbsBuilder.new(self, object_or_nested_array)
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
class BreadcrumbsBuilder
|
18
|
+
|
19
|
+
attr_reader :view, :object_or_nested_array, :path
|
20
|
+
|
21
|
+
def initialize(view, object_or_nested_array = nil)
|
22
|
+
@view = view
|
23
|
+
@object_or_nested_array = object_or_nested_array || path_builder.nested_array
|
24
|
+
@path = path_builder.collection_url
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
def breadcrumbs
|
29
|
+
view.content_tag :ul, :class =>'breadcrumb' do
|
30
|
+
(breadcrumb_for_parent + breadcrumb_for_collection + breadcrumb_for_action).html_safe
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
def breadcrumb_for_object_link
|
36
|
+
breadcrumb_for link_to_object
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def breadcrumb_for_parent
|
43
|
+
parent = path_builder.parent
|
44
|
+
return ''.html_safe unless parent
|
45
|
+
BreadcrumbsBuilder.new(view, parent).breadcrumb_for_object_link
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def breadcrumb_for_collection
|
50
|
+
breadcrumb_for view.link_to(object.class.table_name.titleize, path)
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
def breadcrumb_for_action
|
55
|
+
case action
|
56
|
+
when 'new' then breadcrumb_for('New', :class => 'active')
|
57
|
+
when 'edit' then breadcrumb_for_object + breadcrumb_for('Edit', :class => 'active')
|
58
|
+
else breadcrumb_for_object_name
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def breadcrumb_for_object
|
64
|
+
if can_show?
|
65
|
+
breadcrumb_for_object_link
|
66
|
+
else
|
67
|
+
breadcrumb_for_object_name
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
def breadcrumb_for(content, options = {})
|
73
|
+
view.content_tag :li, content, options
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def breadcrumb_for_object_name
|
78
|
+
breadcrumb_for object_name.html_safe
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
def link_to_object
|
83
|
+
view.link_to object_name.html_safe, object_or_nested_array
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
def can_show?
|
88
|
+
view.controller.respond_to?(:show)
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
def action
|
93
|
+
return 'new' if object.new_record?
|
94
|
+
view.params[:action]
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
def object_name
|
99
|
+
if object.respond_to?(:name) && object.name.present?
|
100
|
+
object.name
|
101
|
+
else
|
102
|
+
object.to_s
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
def object
|
108
|
+
@object ||= Array(object_or_nested_array).last
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
def path_builder
|
113
|
+
@path_builder ||= if inherited_resources?
|
114
|
+
InheritedResourcesPathBuilder.new(view)
|
115
|
+
else
|
116
|
+
PathBuilder.new(view)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
def inherited_resources?
|
122
|
+
defined?(InheritedResources) && view.controller.responder == InheritedResources::Responder
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
|
128
|
+
PathBuilder = Struct.new(:view) do
|
129
|
+
def nested_array
|
130
|
+
(namespaces + [parent] + [view.current_object]).compact
|
131
|
+
end
|
132
|
+
|
133
|
+
def collection_url
|
134
|
+
view.objects_path
|
135
|
+
end
|
136
|
+
|
137
|
+
def parent
|
138
|
+
view.parent_object
|
139
|
+
end
|
140
|
+
|
141
|
+
def namespaces
|
142
|
+
return [] unless view.respond_to?(:namespaces)
|
143
|
+
Array(view.namespaces)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
|
148
|
+
InheritedResourcesPathBuilder = Struct.new(:view) do
|
149
|
+
def nested_array
|
150
|
+
if view.respond_to? :calculate_nested_array
|
151
|
+
view.calculate_nested_array
|
152
|
+
else
|
153
|
+
[parent, view.resource].compact
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def collection_url
|
158
|
+
view.collection_url
|
159
|
+
end
|
160
|
+
|
161
|
+
def parent
|
162
|
+
return view.parent.becomes(view.parent.class.base_class) if view.respond_to?(:parent)
|
163
|
+
view.parent_object
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
ActionView::Base.send(:include, RailsCoreExtensions::Breadcrumb) if defined?(ActionView::Base)
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module CachesActionWithoutHost
|
2
|
+
|
3
|
+
def self.included(controller)
|
4
|
+
controller.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def caches_action_without_host(*args)
|
9
|
+
options = args.extract_options!
|
10
|
+
options ||= {}
|
11
|
+
options[:cache_path] ||= proc{|c| c.url_for(c.params).split(/\//, 4).last}
|
12
|
+
args << options
|
13
|
+
caches_action(*args)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module Concurrency
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
module ClassMethods
|
5
|
+
|
6
|
+
def concurrency_safe(*methods)
|
7
|
+
options = methods.extract_options!
|
8
|
+
methods.each do |method|
|
9
|
+
add_concurrency_check(method, options)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
def concurrency_safe_method_locked?(method)
|
15
|
+
concurrency_cache.read(concurrency_safe_method_cache_name(method)) == 'locked'
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def concurrency_cache
|
20
|
+
@concurrency_cache ||= ::Rails.cache
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def concurrency_cache=(cache)
|
25
|
+
[:read,:write,:delete].each do |method|
|
26
|
+
raise ConcurrencyCacheException, "#{cache} does not implement #{method}" unless cache.respond_to?(method)
|
27
|
+
end
|
28
|
+
@concurrency_cache = cache
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def add_concurrency_check(method, options = {})
|
35
|
+
method_definition = <<-DEFINITION
|
36
|
+
def #{method}_with_concurrency_lock(*args)
|
37
|
+
if concurrency_safe_method_locked?(:#{method})
|
38
|
+
raise ConcurrentCallException.new(self,:#{method}), "#{self.name}.#{method} is already running"
|
39
|
+
end
|
40
|
+
lock_concurrency_safe_method(:#{method})
|
41
|
+
return_value = nil
|
42
|
+
begin
|
43
|
+
return_value = #{method}_without_concurrency_lock(*args)
|
44
|
+
ensure
|
45
|
+
unlock_concurrency_safe_method(:#{method})
|
46
|
+
end
|
47
|
+
return_value
|
48
|
+
end
|
49
|
+
|
50
|
+
alias_method_chain :#{method}, :concurrency_lock
|
51
|
+
DEFINITION
|
52
|
+
|
53
|
+
if method_type(method, options[:type]) == 'class'
|
54
|
+
method_definition = <<-DEFINITION
|
55
|
+
class << self
|
56
|
+
#{method_definition}
|
57
|
+
end
|
58
|
+
DEFINITION
|
59
|
+
end
|
60
|
+
|
61
|
+
module_eval method_definition
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
def lock_concurrency_safe_method(method)
|
66
|
+
concurrency_cache.write(concurrency_safe_method_cache_name(method), 'locked')
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
def unlock_concurrency_safe_method(method)
|
71
|
+
concurrency_cache.delete(concurrency_safe_method_cache_name(method))
|
72
|
+
end
|
73
|
+
|
74
|
+
|
75
|
+
def concurrency_safe_method_cache_name(method)
|
76
|
+
"#{self.name.underscore}_concurrency_safe_class_method_#{method}"
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
def method_type(method, type = nil)
|
81
|
+
types = method_types(method, type)
|
82
|
+
raise AmbiguousMethodException.new(self, method), "#{method} for #{self.name} is ambiguous. Please specify the type (instance/class) option" if types.count == 2
|
83
|
+
raise NoMethodException.new(self, method), "#{method} is not not a valid method for #{self.name}." if types.blank?
|
84
|
+
types.first
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
def method_types(method, type = nil)
|
89
|
+
['class', 'instance'].select do |mt|
|
90
|
+
(type.blank? || type.to_s == mt) && self.send("#{mt}_method?", method)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
def class_method?(method)
|
96
|
+
self.respond_to?(method, true)
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
def instance_method?(method)
|
101
|
+
(self.instance_methods + self.private_instance_methods).map(&:to_s).include?(method.to_s)
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
module InstanceMethods
|
108
|
+
|
109
|
+
def concurrency_cache
|
110
|
+
self.class.concurrency_cache
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
def concurrency_safe_method_locked?(method)
|
115
|
+
concurrency_cache.read(concurrency_safe_method_cache_name(method)) == 'locked'
|
116
|
+
end
|
117
|
+
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def lock_concurrency_safe_method(method)
|
122
|
+
concurrency_cache.write(concurrency_safe_method_cache_name(method), 'locked')
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
def unlock_concurrency_safe_method(method)
|
127
|
+
concurrency_cache.delete(concurrency_safe_method_cache_name(method))
|
128
|
+
end
|
129
|
+
|
130
|
+
|
131
|
+
def concurrency_safe_method_cache_name(method)
|
132
|
+
"#{self.class.name.underscore}_concurrency_safe_instance_method_#{method}"
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
class ConcurrencyException < ::Exception; end
|
139
|
+
class ConcurrencyCacheException < ConcurrencyException; end
|
140
|
+
class MethodException < ConcurrencyException
|
141
|
+
attr_reader :object, :method
|
142
|
+
|
143
|
+
def initialize(object, method)
|
144
|
+
@object = object
|
145
|
+
@method = method
|
146
|
+
end
|
147
|
+
end
|
148
|
+
class ConcurrentCallException < MethodException; end
|
149
|
+
class NoMethodException < MethodException; end
|
150
|
+
class AmbiguousMethodException < MethodException; end
|
151
|
+
|
152
|
+
end
|