rails_core_extensions 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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,27 @@
|
|
1
|
+
module RailsCoreExtensions
|
2
|
+
class PositionInitializer
|
3
|
+
def initialize(model_class, scope_name = nil, position_column = nil)
|
4
|
+
@model_class = model_class
|
5
|
+
@scope_name = scope_name
|
6
|
+
@position_column = position_column
|
7
|
+
@position_column ||= :position
|
8
|
+
end
|
9
|
+
|
10
|
+
def positionalize
|
11
|
+
groups.each do |objects|
|
12
|
+
objects.each.with_index do |object, index|
|
13
|
+
next if object.position == index + 1
|
14
|
+
scope = @model_class.where(id: object.id)
|
15
|
+
scope.update_all(@position_column => index + 1)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def groups
|
23
|
+
objects = @model_class.order(@position_column)
|
24
|
+
@scope_name ? objects.group_by(&@scope_name.to_sym).values : [objects]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module RailsCoreExtensions
|
2
|
+
class Sortable
|
3
|
+
# params is either:
|
4
|
+
# SIMPLE:
|
5
|
+
# model_body (where model is the model name)
|
6
|
+
# SCOPED:
|
7
|
+
# scope (name of the scope, e.g. category_id)
|
8
|
+
# category_id (or whatever the name of scope is)
|
9
|
+
# model_1_body (or whatever id of scope id)
|
10
|
+
def initialize(params, controller_name)
|
11
|
+
@params = params.symbolize_keys
|
12
|
+
@controller_name = controller_name
|
13
|
+
@klass = controller_name.classify.constantize
|
14
|
+
end
|
15
|
+
|
16
|
+
def sort
|
17
|
+
scope = @params[:scope].try(:to_sym)
|
18
|
+
|
19
|
+
param_key = @controller_name.singularize
|
20
|
+
param_key += "_#{@params[scope]}" if scope
|
21
|
+
|
22
|
+
params_collection = @params["#{param_key}_body".to_sym]
|
23
|
+
|
24
|
+
if params_collection.blank?
|
25
|
+
name = "#{scope.to_s.gsub('_id', '')}_#{@params[scope]}_body".to_sym
|
26
|
+
params_collection = @params[name]
|
27
|
+
end
|
28
|
+
|
29
|
+
collection = @klass.reorder(:position)
|
30
|
+
collection = collection.where(@params.slice(scope.to_sym)) if scope
|
31
|
+
|
32
|
+
sort_collection(collection, params_collection.map(&:to_i))
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def sort_collection(collection_old, collection_new_ids)
|
38
|
+
@klass.transaction do
|
39
|
+
collection_old.each.with_index do |object, index|
|
40
|
+
next if object.id == collection_new_ids[index]
|
41
|
+
|
42
|
+
new_position = collection_new_ids.index(object.id) + 1
|
43
|
+
update(object, new_position)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def update(object, new_position)
|
49
|
+
@klass.where(id: object.id).limit(1).update_all(position: new_position)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
namespace :fix do
|
2
|
+
desc 'Fix acts as list positions if they ever get out of sync for a model'
|
3
|
+
task :acts_as_list_positions => :environment do
|
4
|
+
raise ArgumentError, 'You must specify model with MODEL environment variable' if ENV['MODEL'].nil?
|
5
|
+
|
6
|
+
model_class = ENV['MODEL'].camelize.constantize
|
7
|
+
scope_name = ENV['SCOPE'].try(:to_sym)
|
8
|
+
position_column = ENV['POSITION'].try(:to_sym)
|
9
|
+
|
10
|
+
PositionInitializer.positionalize(model_class, scope_name, position_column)
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'rails_core_extensions/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'rails_core_extensions'
|
8
|
+
spec.version = RailsCoreExtensions::VERSION
|
9
|
+
spec.authors = ['Michael Noack', 'Allesandro Berardi']
|
10
|
+
spec.email = ['support@travellink.com.au']
|
11
|
+
spec.description = %q{Set of extensions to core rails libraries.}
|
12
|
+
spec.summary = %q{Set of extensions to core rails libraries.}
|
13
|
+
spec.homepage = 'http://github.com/sealink/rails_core_extensions'
|
14
|
+
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files`.split($/)
|
18
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_dependency 'activerecord', ['>= 2.3.0', '< 5.0.0']
|
23
|
+
spec.add_dependency 'actionpack', ['>= 2.3.0', '< 5.0.0']
|
24
|
+
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
26
|
+
spec.add_development_dependency 'rake'
|
27
|
+
spec.add_development_dependency 'rspec'
|
28
|
+
spec.add_development_dependency 'activerecord-nulldb-adapter'
|
29
|
+
spec.add_development_dependency 'simplecov'
|
30
|
+
spec.add_development_dependency 'simplecov-rcov'
|
31
|
+
spec.add_development_dependency 'coveralls'
|
32
|
+
spec.add_development_dependency 'sqlite3'
|
33
|
+
spec.add_development_dependency 'travis'
|
34
|
+
|
35
|
+
# breadcrumbs optional dependencies
|
36
|
+
spec.add_development_dependency 'make_resourceful'
|
37
|
+
spec.add_development_dependency 'inherited_resources'
|
38
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'rails_core_extensions/sortable'
|
4
|
+
|
5
|
+
connect_to_sqlite
|
6
|
+
|
7
|
+
describe RailsCoreExtensions::Sortable do
|
8
|
+
before do
|
9
|
+
Model = Class.new(ActiveRecord::Base) do
|
10
|
+
default_scope -> { order(:name) }
|
11
|
+
end
|
12
|
+
@one = Model.create!(name: 'One', position: 1, category_id: 1)
|
13
|
+
@two = Model.create!(name: 'Two', position: 2, category_id: 1)
|
14
|
+
@thr = Model.create!(name: 'Thr', position: 3, category_id: 2)
|
15
|
+
end
|
16
|
+
|
17
|
+
after do
|
18
|
+
Model.delete_all
|
19
|
+
Object.send(:remove_const, 'Model')
|
20
|
+
end
|
21
|
+
|
22
|
+
let(:params) { { model_body: [@one.id, @thr.id, @two.id] } }
|
23
|
+
subject { RailsCoreExtensions::Sortable.new(params, 'models') }
|
24
|
+
|
25
|
+
describe 'when unscoped' do
|
26
|
+
let(:scope) { Model.reorder(:position) }
|
27
|
+
specify { expect(scope.pluck(:name)).to eq %w(One Two Thr) }
|
28
|
+
it 'should correctly sort' do
|
29
|
+
subject.sort
|
30
|
+
expect(scope.pluck(:name)).to eq %w(One Thr Two)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe 'when scoped' do
|
35
|
+
let(:scope) { Model.where(category_id: 1).reorder(:position) }
|
36
|
+
specify { expect(scope.pluck(:name)).to eq %w(One Two) }
|
37
|
+
|
38
|
+
let(:params) { { category_id: 1, scope: :category_id, model_1_body: [@two.id, @one.id] } }
|
39
|
+
it 'should correctly sort' do
|
40
|
+
subject.sort
|
41
|
+
expect(scope.pluck(:name)).to eq %w(Two One)
|
42
|
+
end
|
43
|
+
|
44
|
+
describe 'when params scoped differently' do
|
45
|
+
let(:params) { { category_id: 1, scope: :category_id, category_1_body: [@two.id, @one.id] } }
|
46
|
+
it 'should correctly sort' do
|
47
|
+
subject.sort
|
48
|
+
expect(scope.pluck(:name)).to eq %w(Two One)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rails_core_extensions/action_view_extensions'
|
2
|
+
|
3
|
+
require 'action_view'
|
4
|
+
|
5
|
+
describe RailsCoreExtensions::ActionViewExtensions do
|
6
|
+
before do
|
7
|
+
class TestModel1
|
8
|
+
include ActionView::Helpers::FormTagHelper
|
9
|
+
include ActionView::Helpers::FormOptionsHelper
|
10
|
+
include RailsCoreExtensions::ActionViewExtensions
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
after { Object.send(:remove_const, 'TestModel1') }
|
15
|
+
|
16
|
+
subject { TestModel1.new }
|
17
|
+
|
18
|
+
context '#boolean_select_tag' do
|
19
|
+
it 'should generate and have selected element selected' do
|
20
|
+
expect(subject.boolean_select_tag('name', selected: '0')).to eq(
|
21
|
+
subject.select_tag('name', subject.options_for_select([['Yes', '1'], ['No', '0']], selected: '0'))
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
begin
|
2
|
+
require 'active_model'
|
3
|
+
require 'rails_core_extensions/active_model_extensions'
|
4
|
+
require 'spec_helper_model_base'
|
5
|
+
|
6
|
+
describe ActiveModelExtensions do
|
7
|
+
include NullDB::CustomNullifiedDatabase if defined?(NullDB)
|
8
|
+
|
9
|
+
before do
|
10
|
+
class ActiveModelExtensionsTestModel1 < ModelBase
|
11
|
+
include ActiveModelExtensions::Validations
|
12
|
+
|
13
|
+
attr_accessor :name
|
14
|
+
|
15
|
+
def initialize(options = {})
|
16
|
+
@name = options[:name]
|
17
|
+
end
|
18
|
+
|
19
|
+
validate :validate_required
|
20
|
+
|
21
|
+
def validate_required
|
22
|
+
CustomPresenceValidator.new(:attributes => lambda { ['name'] }).validate(self)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
after { Object.send(:remove_const, klass.name) }
|
28
|
+
|
29
|
+
let(:klass) { ActiveModelExtensionsTestModel1 }
|
30
|
+
|
31
|
+
context 'when model missing name' do
|
32
|
+
subject { klass.new }
|
33
|
+
it { should_not be_valid }
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'when model complete' do
|
37
|
+
subject { klass.new(:name => 'Valid') }
|
38
|
+
it { should be_valid }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe ActiveModelExtensions do
|
43
|
+
include NullDB::CustomNullifiedDatabase if defined?(NullDB)
|
44
|
+
|
45
|
+
before do
|
46
|
+
class ActiveModelExtensionsTestModel2 < ModelBase
|
47
|
+
include ActiveModelExtensions::Validations
|
48
|
+
|
49
|
+
attr_accessor :name
|
50
|
+
|
51
|
+
def initialize(options = {})
|
52
|
+
@name = options[:name]
|
53
|
+
end
|
54
|
+
|
55
|
+
validate_presence_by_custom_rules lambda { ['name'] }
|
56
|
+
end
|
57
|
+
|
58
|
+
class ActiveModelExtensionsTestModel3 < ModelBase
|
59
|
+
include ActiveModelExtensions::Validations
|
60
|
+
|
61
|
+
attr_accessor :name, :phone, :mobile
|
62
|
+
|
63
|
+
def initialize(options = {})
|
64
|
+
@name = options[:name]
|
65
|
+
@phone = options[:phone]
|
66
|
+
@mobile = options[:mobile]
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
validate_presence_by_custom_rules lambda { ['name', 'phone or mobile'] }
|
71
|
+
end
|
72
|
+
|
73
|
+
class ActiveModelExtensionsTestModelNil < ModelBase
|
74
|
+
include ActiveModelExtensions::Validations
|
75
|
+
def initialize(options = {})
|
76
|
+
end
|
77
|
+
validate_presence_by_custom_rules lambda { nil }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
after { Object.send(:remove_const, klass.name) }
|
82
|
+
|
83
|
+
let(:klass) { ActiveModelExtensionsTestModel2 }
|
84
|
+
|
85
|
+
context 'when model missing name' do
|
86
|
+
subject { klass.new }
|
87
|
+
it { should_not be_valid }
|
88
|
+
end
|
89
|
+
|
90
|
+
context 'when model complete' do
|
91
|
+
subject { klass.new(:name => 'Valid') }
|
92
|
+
it { should be_valid }
|
93
|
+
end
|
94
|
+
|
95
|
+
context 'with or case' do
|
96
|
+
let(:klass) { ActiveModelExtensionsTestModel3 }
|
97
|
+
|
98
|
+
context 'when model has none of conditions' do
|
99
|
+
subject { klass.new(:name => 'Valid') }
|
100
|
+
it { should_not be_valid }
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'when model has first of conditions' do
|
104
|
+
subject { klass.new(:name => 'Valid', :phone => '555-1234') }
|
105
|
+
it { should be_valid }
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'when model has second of conditions' do
|
109
|
+
subject { klass.new(:name => 'Valid', :mobile => '555-1234') }
|
110
|
+
it { should be_valid }
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'when model has all conditions' do
|
114
|
+
subject { klass.new(:name => 'Valid', :phone => '555-1234', :mobile => '555-4321') }
|
115
|
+
it { should be_valid }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'with nil requirements' do
|
120
|
+
let(:klass) { ActiveModelExtensionsTestModelNil }
|
121
|
+
|
122
|
+
context 'when model valid' do
|
123
|
+
subject { klass.new(:name => 'Valid') }
|
124
|
+
it { should be_valid }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
rescue LoadError # Spec doesn't run for rails 2
|
130
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
connect_to_sqlite
|
4
|
+
|
5
|
+
describe "optional_fields" do
|
6
|
+
before do
|
7
|
+
Model = Class.new(ActiveRecord::Base) do
|
8
|
+
optional_fields :name, :age, -> { [:age] }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
after { Object.send(:remove_const, 'Model') }
|
12
|
+
|
13
|
+
it 'should know what fields are optional' do
|
14
|
+
expect(Model).to be_age_enabled
|
15
|
+
expect(Model).to_not be_name_enabled
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should be able to change optional fields' do
|
19
|
+
Model.enabled_fields = [:age, :name]
|
20
|
+
expect(Model).to be_name_enabled
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "ActiveRecord::Base" do
|
25
|
+
before(:all) { Model = Class.new(ActiveRecord::Base) }
|
26
|
+
after (:all) { Object.send(:remove_const, 'Model') }
|
27
|
+
|
28
|
+
before do
|
29
|
+
@mock_model = double("mock model")
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should create a new record if new_or_update! is passed a hash without an :id" do
|
33
|
+
attributes = {:fake_column => 'nothing really'}
|
34
|
+
expect(Model).to receive(:new).with(attributes)
|
35
|
+
Model.new_or_update!(attributes)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should update record if new_or_update! is passed hash with :id" do
|
39
|
+
attributes = {:fake_column => 'nothing really', :id => 1}
|
40
|
+
expect(Model).to receive(:find) { @mock_model }
|
41
|
+
expect(@mock_model).to receive(:update_attributes!)
|
42
|
+
Model.new_or_update!(attributes)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
describe RailsCoreExtensions::ActionControllerSortable do
|
48
|
+
class NormalController < ActionController::Base
|
49
|
+
end
|
50
|
+
|
51
|
+
class SortableController < ActionController::Base
|
52
|
+
sortable
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should sort' do
|
56
|
+
# map(&:to_sym) for ruby 1.8 compatibility
|
57
|
+
expect(NormalController.new.methods.map(&:to_sym)).to_not include(:sort)
|
58
|
+
expect(SortableController.new.methods.map(&:to_sym)).to include(:sort)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe ActiveRecordExtensions do
|
63
|
+
class Parent < ActiveRecord::Base
|
64
|
+
has_many :children
|
65
|
+
def transfer_children_from(old_parent)
|
66
|
+
transfer_records(Child, [old_parent])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
class Child < ActiveRecord::Base
|
70
|
+
belongs_to :parent
|
71
|
+
end
|
72
|
+
|
73
|
+
let(:old) { Parent.create! }
|
74
|
+
let(:new) { Parent.create! }
|
75
|
+
|
76
|
+
before do
|
77
|
+
new.children.create!
|
78
|
+
old.children.create!
|
79
|
+
end
|
80
|
+
|
81
|
+
after do
|
82
|
+
Parent.delete_all
|
83
|
+
Child.delete_all
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'should transfer records' do
|
87
|
+
expect(new.children.size).to eq 1
|
88
|
+
expect(old.children.size).to eq 1
|
89
|
+
new.transfer_children_from(old)
|
90
|
+
expect(new.reload.children.size).to eq 2
|
91
|
+
expect(old.reload.children.size).to eq 0
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe ActiveRecordExtensions do
|
96
|
+
before(:all) do
|
97
|
+
Model = Class.new(ActiveRecord::Base) do
|
98
|
+
cache_all_attributes :by => 'name'
|
99
|
+
end
|
100
|
+
end
|
101
|
+
after (:all) { Object.send(:remove_const, 'Model') }
|
102
|
+
|
103
|
+
before do
|
104
|
+
allow(Model).to receive(:cache) { ActiveSupport::Cache::MemoryStore.new }
|
105
|
+
allow(Model).to receive(:should_cache?) { true }
|
106
|
+
end
|
107
|
+
|
108
|
+
after do
|
109
|
+
Model.delete_all
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'should cache all attributes' do
|
113
|
+
@first = Model.create!(:name => 'First')
|
114
|
+
@second = Model.create!(:name => 'Second')
|
115
|
+
|
116
|
+
expected = {'First' => @first.attributes, 'Second' => @second.attributes}
|
117
|
+
|
118
|
+
# Test underlying generate attributes hash method works
|
119
|
+
expect(Model.generate_attributes_hash).to eq expected
|
120
|
+
expect(Model.attribute_cache).to eq expected
|
121
|
+
|
122
|
+
# Test after save/destroy it updates
|
123
|
+
@first.destroy
|
124
|
+
expect(Model.attribute_cache).to eq 'Second' => @second.attributes
|
125
|
+
end
|
126
|
+
end
|