irie 1.0.0
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/README.md +657 -0
- data/Rakefile +21 -0
- data/lib/irie.rb +23 -0
- data/lib/irie/class_methods.rb +61 -0
- data/lib/irie/config.rb +131 -0
- data/lib/irie/configuration_error.rb +4 -0
- data/lib/irie/extensions/autorender_count.rb +17 -0
- data/lib/irie/extensions/conversion/nil_params.rb +27 -0
- data/lib/irie/extensions/count.rb +21 -0
- data/lib/irie/extensions/index_query.rb +48 -0
- data/lib/irie/extensions/limit.rb +25 -0
- data/lib/irie/extensions/offset.rb +25 -0
- data/lib/irie/extensions/order.rb +153 -0
- data/lib/irie/extensions/paging.rb +43 -0
- data/lib/irie/extensions/paging/autorender_page_count.rb +19 -0
- data/lib/irie/extensions/param_filters.rb +137 -0
- data/lib/irie/extensions/params_to_joins.rb +108 -0
- data/lib/irie/extensions/query_filter.rb +58 -0
- data/lib/irie/extensions/query_includes.rb +107 -0
- data/lib/irie/extensions/smart_layout.rb +20 -0
- data/lib/irie/param_aliases.rb +36 -0
- data/lib/irie/version.rb +3 -0
- metadata +124 -0
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'appraisal'
|
4
|
+
|
5
|
+
Bundler::GemHelper.install_tasks
|
6
|
+
|
7
|
+
Rake::TestTask.new do |t|
|
8
|
+
t.libs = ['lib','test']
|
9
|
+
t.test_files = Dir.glob(["test/**/*_test.rb"])
|
10
|
+
t.verbose = true
|
11
|
+
end
|
12
|
+
|
13
|
+
task :default => [:test]
|
14
|
+
task :spec => [:test]
|
15
|
+
|
16
|
+
desc "Setup Appraisal."
|
17
|
+
task 'appraisal:setup' do
|
18
|
+
Rake::Task['appraisal:cleanup'].invoke
|
19
|
+
Rake::Task['appraisal:gemfiles'].invoke
|
20
|
+
Rake::Task['appraisal:install'].invoke
|
21
|
+
end
|
data/lib/irie.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'irie/version'
|
2
|
+
require 'irie/configuration_error'
|
3
|
+
require 'irie/config'
|
4
|
+
require 'irie/class_methods'
|
5
|
+
require 'irie/param_aliases'
|
6
|
+
require 'irie/extensions/autorender_count'
|
7
|
+
require 'irie/extensions/params_to_joins'
|
8
|
+
require 'irie/extensions/conversion/nil_params'
|
9
|
+
require 'irie/extensions/count'
|
10
|
+
require 'irie/extensions/index_query'
|
11
|
+
require 'irie/extensions/limit'
|
12
|
+
require 'irie/extensions/offset'
|
13
|
+
require 'irie/extensions/order'
|
14
|
+
require 'irie/extensions/paging/autorender_page_count'
|
15
|
+
require 'irie/extensions/paging'
|
16
|
+
require 'irie/extensions/param_filters'
|
17
|
+
require 'irie/extensions/query_filter'
|
18
|
+
require 'irie/extensions/query_includes'
|
19
|
+
require 'irie/extensions/smart_layout'
|
20
|
+
|
21
|
+
class ::ActionController::Base
|
22
|
+
extend ::Irie::ClassMethods
|
23
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Irie
|
2
|
+
module ClassMethods
|
3
|
+
|
4
|
+
protected
|
5
|
+
|
6
|
+
def extensions(*extension_syms)
|
7
|
+
::Irie::CONTROLLER_OPTIONS.each do |key|
|
8
|
+
class_attribute key, instance_writer: true unless respond_to?(key)
|
9
|
+
# doing unless self.send(key.to_sym) to ensure parent override of defaults is kept
|
10
|
+
self.send("#{key}=".to_sym, ::Irie.send(key)) unless self.send(key.to_sym)
|
11
|
+
end
|
12
|
+
|
13
|
+
#raise "self.inherited_resources_defined_actions = #{inherited_resources_defined_actions.inspect}\n\ninstance_methods = #{self.instance_methods.collect(&:to_s).sort.join(', ')}\n\nself.autoincludes = #{self.autoincludes.inspect}" if $gary
|
14
|
+
|
15
|
+
self.autoincludes.keys.each do |action_sym|
|
16
|
+
if self.instance_methods.include?(action_sym.to_sym)
|
17
|
+
autoloading_extensions = self.autoincludes[action_sym]
|
18
|
+
if autoloading_extensions && autoloading_extensions.size > 0
|
19
|
+
extension_syms += autoloading_extensions
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
extensions! extension_syms
|
25
|
+
end
|
26
|
+
|
27
|
+
# Load specified extensions in the order defined by self.extension_include_order.
|
28
|
+
def extensions!(*extension_syms)
|
29
|
+
return extension_syms if extension_syms.length == 0
|
30
|
+
|
31
|
+
extension_syms = extension_syms.flatten.collect {|es| es.to_sym}.compact
|
32
|
+
|
33
|
+
if extension_syms.include?(:all)
|
34
|
+
ordered_extension_syms = self.extension_include_order.dup
|
35
|
+
else
|
36
|
+
extensions_without_defined_order = extension_syms.uniq - self.extension_include_order.uniq
|
37
|
+
if extensions_without_defined_order.length > 0
|
38
|
+
raise ::Irie::ConfigurationError.new "The following must be added to the self.extension_include_order array in Irie configuration: #{extensions_without_defined_order.collect(&:inspect).join(', ')}"
|
39
|
+
else
|
40
|
+
ordered_extension_syms = self.extension_include_order & extension_syms
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# load requested extensions
|
45
|
+
ordered_extension_syms.each do |arg_sym|
|
46
|
+
if module_class_name = self.available_extensions[arg_sym]
|
47
|
+
begin
|
48
|
+
logger.debug("Irie::ClassMethods.extensions! #{self} including #{module_class_name}") if ::Irie.debug?
|
49
|
+
include module_class_name.constantize
|
50
|
+
rescue NameError => e
|
51
|
+
raise ::Irie::ConfigurationError.new "Failed to constantize '#{module_class_name}' with extension key #{arg_sym.inspect} in self.available_extensions. Error: \n#{e.message}\n#{e.backtrace.join("\n")}"
|
52
|
+
end
|
53
|
+
else
|
54
|
+
raise ::Irie::ConfigurationError.new "#{arg_sym.inspect} isn't defined in self.available_extensions"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
extension_syms
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/irie/config.rb
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
module Irie
|
2
|
+
|
3
|
+
CONTROLLER_OPTIONS = [
|
4
|
+
:autoincludes,
|
5
|
+
:available_actions,
|
6
|
+
:available_extensions,
|
7
|
+
:can_filter_by_default_using,
|
8
|
+
:debug,
|
9
|
+
:function_param_names,
|
10
|
+
:id_is_primary_key_param,
|
11
|
+
:number_of_records_in_a_page,
|
12
|
+
:predicate_prefix,
|
13
|
+
:update_should_return_entity,
|
14
|
+
:extension_include_order
|
15
|
+
]
|
16
|
+
|
17
|
+
class << self
|
18
|
+
CONTROLLER_OPTIONS.each{|name|attr_accessor name; define_method("#{name}?") { !!public_send(name) } }
|
19
|
+
def configure(&blk); class_eval(&blk); end
|
20
|
+
|
21
|
+
# Adds to extension_include_order and extension_include_order, e.g.
|
22
|
+
# ::Irie.register_extension :boolean_params, '::Focal::Irie::BooleanParams'
|
23
|
+
# Is equivalent to:
|
24
|
+
# ::Irie.available_extensions[:boolean_params] = '::Focal::Irie::BooleanParams'
|
25
|
+
# ::Irie.extension_include_order << extension_sym
|
26
|
+
# Allowed options are `:include`, `:after`, and `:before`. Some examples:
|
27
|
+
# ::Irie.register_extension :boolean_params, '::Example::BooleanParams', include: :last # the default, so unnecessary
|
28
|
+
# ::Irie.register_extension :boolean_params, '::Example::BooleanParams', include: :first # includes module after all others registered at this point
|
29
|
+
# ::Irie.register_extension :boolean_params, '::Example::BooleanParams', after: :nil_params # includes after :nil_params
|
30
|
+
# ::Irie.register_extension :boolean_params, '::Example::BooleanParams', before: :nil_params # includes after :nil_params
|
31
|
+
def register_extension(extension_sym, extension_class_name, options = {})
|
32
|
+
raise ::Irie::ConfigurationError.new "Irie.register_extension must provide an extension symbol as the first argument" unless extension_sym
|
33
|
+
raise ::Irie::ConfigurationError.new "Irie.register_extension must provide an extension class name (string) as the second argument" unless extension_sym
|
34
|
+
raise ::Irie::ConfigurationError.new "Irie.register_extension can only provide a single option: :include, :after, or :before" if options.size > 1
|
35
|
+
initial_opts = options.dup
|
36
|
+
include_opt, after_opt, before_opt = *[:include, :after, :before].collect{|opt_name| options.delete(opt_name)}
|
37
|
+
include_opt = :last unless include_opt || after_opt || before_opt
|
38
|
+
raise ::Irie::ConfigurationError.new "Irie.register_extension unrecognized options: #{options.inspect}" if options.size > 0
|
39
|
+
|
40
|
+
::Irie.extension_include_order.delete(extension_sym)
|
41
|
+
|
42
|
+
before_or_after_opt_value = before_opt || after_opt
|
43
|
+
if include_opt == :first
|
44
|
+
::Irie.extension_include_order.unshift extension_sym
|
45
|
+
elsif include_opt == :last
|
46
|
+
::Irie.extension_include_order << extension_sym
|
47
|
+
elsif before_or_after_opt_value
|
48
|
+
ind = ::Irie.extension_include_order.index(before_or_after_opt_value)
|
49
|
+
raise ::Irie::ConfigurationError.new "Irie.register_extension cannot insert #{before_opt ? 'before' : 'after'} #{before_or_after_opt_value.inspect}, because #{before_or_after_opt_value.inspect} was not yet registered. A possible workaround for deferred registration may be to require the code that does the prerequisite registration."
|
50
|
+
::Irie.extension_include_order.insert(ind + (after_opt ? 1 : 0), extension_sym)
|
51
|
+
else
|
52
|
+
raise ::Irie::ConfigurationError.new "Irie.register_extension unsupported options: #{initial_opts.inspect}"
|
53
|
+
end
|
54
|
+
|
55
|
+
::Irie.available_extensions[extension_sym] = extension_class_name
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
::Irie.configure do
|
62
|
+
|
63
|
+
# Default for :using in can_filter_by.
|
64
|
+
self.can_filter_by_default_using = [:eq]
|
65
|
+
|
66
|
+
# Use one or more alternate request parameter names for functions, e.g.
|
67
|
+
# `self.function_param_names = {distinct: :very_distinct, limit: [:limit, :limita]}`
|
68
|
+
self.function_param_names = {}
|
69
|
+
|
70
|
+
# Delimiter for ARel predicate in the request parameter name.
|
71
|
+
self.predicate_prefix = '.'
|
72
|
+
|
73
|
+
# You'd set this to false if id is used for something else other than primary key.
|
74
|
+
self.id_is_primary_key_param = true
|
75
|
+
|
76
|
+
# Used when paging is enabled.
|
77
|
+
self.number_of_records_in_a_page = 15
|
78
|
+
|
79
|
+
# When you include the defined action module, it includes the associated modules.
|
80
|
+
# If value or value array contains symbol it will look up symbol in
|
81
|
+
# ::Irie.available_extensions in the controller (which is defaulted to
|
82
|
+
# `::Irie.available_extensions`). If value is String will assume String is the
|
83
|
+
# fully-qualified module name to include, e.g. `index: '::My::Module'`, If constant,
|
84
|
+
# it will just include constant (module), e.g. `index: ::My::Module`.
|
85
|
+
self.autoincludes = {
|
86
|
+
create: [:smart_layout, :query_includes],
|
87
|
+
destroy: [:smart_layout, :query_includes],
|
88
|
+
edit: [:smart_layout, :query_includes],
|
89
|
+
index: [:smart_layout, :index_query, :order, :param_filters, :params_to_joins, :query_filter, :query_includes],
|
90
|
+
new: [:smart_layout],
|
91
|
+
show: [:smart_layout, :query_includes],
|
92
|
+
update: [:smart_layout, :query_includes]
|
93
|
+
}
|
94
|
+
|
95
|
+
# This ensures the correct order of includes via the extensions method. It bears no
|
96
|
+
# relevance on whether the include is included or not. Since many includes call
|
97
|
+
# super in their methods, the order may seem partially reversed, but this is the actual
|
98
|
+
# include order. If extensions are not listed here, they will not be included by
|
99
|
+
# the extensions method.
|
100
|
+
self.extension_include_order = [
|
101
|
+
:smart_layout,
|
102
|
+
:autorender_page_count,
|
103
|
+
:autorender_count,
|
104
|
+
:count,
|
105
|
+
:paging,
|
106
|
+
:order,
|
107
|
+
:offset,
|
108
|
+
:limit,
|
109
|
+
:param_filters,
|
110
|
+
:query_filter,
|
111
|
+
:params_to_joins,
|
112
|
+
:query_includes,
|
113
|
+
:index_query,
|
114
|
+
:nil_params
|
115
|
+
]
|
116
|
+
|
117
|
+
# By default, it sets the instance variable, but does not return entity if request
|
118
|
+
# update, e.g. in JSON format.
|
119
|
+
self.update_should_return_entity = false
|
120
|
+
|
121
|
+
# Extensions to actions that you can implement in the controller via
|
122
|
+
# `include_extensions`, e.g. `include_extensions :count, :paging`
|
123
|
+
# Each is added as each file is required when the gem is loaded, so for a full list,
|
124
|
+
# check `::Irie.available_extensions` in rails console.
|
125
|
+
# You shouldn't have to worry about configuring this typically.
|
126
|
+
self.available_extensions = {}
|
127
|
+
|
128
|
+
# If true, will logger.debug in instance methods to help with execution tracing at
|
129
|
+
# runtime.
|
130
|
+
self.debug = false
|
131
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Irie
|
2
|
+
module Extensions
|
3
|
+
# Standard rendering of index page count in all formats except html so you don't need views for them.
|
4
|
+
module AutorenderCount
|
5
|
+
extend ::ActiveSupport::Concern
|
6
|
+
::Irie.available_extensions[:autorender_count] = '::' + AutorenderCount.name
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
def autorender_count(options={}, &block)
|
11
|
+
logger.debug("Irie::Extensions::AutorenderCount.autorender_count") if ::Irie.debug?
|
12
|
+
render request.format.symbol => { count: @count }, status: 200, layout: false
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Irie
|
2
|
+
module Extensions
|
3
|
+
module Conversion
|
4
|
+
# Converts the following filter request param values to nil: 'NULL', 'null', 'nil'
|
5
|
+
module NilParams
|
6
|
+
extend ::ActiveSupport::Concern
|
7
|
+
::Irie.available_extensions[:nil_params] = '::' + NilParams.name
|
8
|
+
|
9
|
+
NILS = ['NULL'.freeze, 'null'.freeze, 'nil'.freeze].to_set
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
# Converts request param value(s) 'NULL', 'null', and 'nil' to nil.
|
14
|
+
def convert_param(param_name, param_value_or_values)
|
15
|
+
logger.debug("Irie::Extensions::Conversion::NilParams.convert_param(#{param_name.inspect}, #{param_value_or_values.inspect})") if ::Irie.debug?
|
16
|
+
param_value_or_values = super if defined?(super)
|
17
|
+
if param_value_or_values.is_a? Array
|
18
|
+
param_value_or_values.map{|v| v && NILS.include?(v) ? nil : v }
|
19
|
+
else
|
20
|
+
param_value_or_values && NILS.include?(param_value_or_values) ? nil : param_value_or_values
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Irie
|
2
|
+
module Extensions
|
3
|
+
# Allowing setting `@count` with the count of the records in the index query.
|
4
|
+
module Count
|
5
|
+
extend ::ActiveSupport::Concern
|
6
|
+
::Irie.available_extensions[:count] = '::' + Count.name
|
7
|
+
|
8
|
+
included do
|
9
|
+
include ::Irie::ParamAliases
|
10
|
+
end
|
11
|
+
|
12
|
+
def index(options={}, &block)
|
13
|
+
logger.debug("Irie::Extensions::Count.index") if ::Irie.debug?
|
14
|
+
return super(options, &block) unless aliased_param_present?(:count)
|
15
|
+
@count = collection.count
|
16
|
+
return respond_to?(:autorender_count, true) ? autorender_count(options, &block) : super(options, &block)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Irie
|
2
|
+
module Extensions
|
3
|
+
# Allows use of a lambda for the index query.
|
4
|
+
module IndexQuery
|
5
|
+
extend ::ActiveSupport::Concern
|
6
|
+
::Irie.available_extensions[:index_query] = '::' + IndexQuery.name
|
7
|
+
|
8
|
+
included do
|
9
|
+
include ::Irie::ParamAliases
|
10
|
+
|
11
|
+
class_attribute(:custom_index_query, instance_writer: true) unless self.respond_to? :custom_index_query
|
12
|
+
|
13
|
+
self.custom_index_query ||= nil
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
# Specify a custom query/additional filtering of the collection, e.g.
|
21
|
+
# index_query ->(q) { q.where(:status_code => 'green') }
|
22
|
+
# You could also completely overwrite the collection which would lead
|
23
|
+
# to certain peril, as you would need to then ensure all filters
|
24
|
+
# are included in the correct order to be executed after the query.
|
25
|
+
def index_query(query)
|
26
|
+
self.custom_index_query = query
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
|
32
|
+
def collection
|
33
|
+
logger.debug("Irie::Extensions::IndexQuery.collection") if ::Irie.debug?
|
34
|
+
object = super
|
35
|
+
if self.custom_index_query
|
36
|
+
# convert to relation if model class because proc expects a relation
|
37
|
+
object = object.all unless object.is_a?(ActiveRecord::Relation)
|
38
|
+
a = object.to_s
|
39
|
+
object = self.custom_index_query.call(object)
|
40
|
+
end
|
41
|
+
|
42
|
+
logger.debug("Irie::Extensions::IndexQuery.collection: relation.to_sql so far: #{object.to_sql}") if ::Irie.debug? && object.respond_to?(:to_sql)
|
43
|
+
|
44
|
+
set_collection_ivar object
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Irie
|
2
|
+
module Extensions
|
3
|
+
# Allows limiting of the number of records returned by the index query.
|
4
|
+
module Limit
|
5
|
+
extend ::ActiveSupport::Concern
|
6
|
+
::Irie.available_extensions[:limit] = '::' + Limit.name
|
7
|
+
|
8
|
+
included do
|
9
|
+
include ::Irie::ParamAliases
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def collection
|
15
|
+
logger.debug("Irie::Extensions::Limit.collection") if ::Irie.debug?
|
16
|
+
object = super
|
17
|
+
aliased_params(:limit).each {|param_value| object = object.limit(param_value)}
|
18
|
+
|
19
|
+
logger.debug("Irie::Extensions::Limit.collection: relation.to_sql so far: #{object.to_sql}") if ::Irie.debug? && object.respond_to?(:to_sql)
|
20
|
+
|
21
|
+
set_collection_ivar object
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Irie
|
2
|
+
module Extensions
|
3
|
+
# Allowing offsetting (skipping) records that would be returned by the index query.
|
4
|
+
module Offset
|
5
|
+
extend ::ActiveSupport::Concern
|
6
|
+
::Irie.available_extensions[:offset] = '::' + Offset.name
|
7
|
+
|
8
|
+
included do
|
9
|
+
include ::Irie::ParamAliases
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def collection
|
15
|
+
logger.debug("Irie::Extensions::Offset.collection") if ::Irie.debug?
|
16
|
+
object = super
|
17
|
+
aliased_params(:offset).each {|param_value| object = object.offset(param_value)}
|
18
|
+
|
19
|
+
logger.debug("Irie::Extensions::Offset.collection: relation.to_sql so far: #{object.to_sql}") if ::Irie.debug? && object.respond_to?(:to_sql)
|
20
|
+
|
21
|
+
set_collection_ivar object
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module Irie
|
2
|
+
module Extensions
|
3
|
+
# Allows setting of attributes that can be used for ordering, and allows default ordering to be set.
|
4
|
+
module Order
|
5
|
+
extend ::ActiveSupport::Concern
|
6
|
+
::Irie.available_extensions[:order] = '::' + Order.name
|
7
|
+
|
8
|
+
included do
|
9
|
+
include ::Irie::ParamAliases
|
10
|
+
|
11
|
+
class_attribute(:can_be_ordered_by, instance_writer: true) unless self.respond_to? :can_be_ordered_by
|
12
|
+
class_attribute(:default_ordered_by, instance_writer: true) unless self.respond_to? :default_ordered_by
|
13
|
+
|
14
|
+
self.can_be_ordered_by ||= []
|
15
|
+
self.default_ordered_by ||= {}
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
|
20
|
+
protected
|
21
|
+
|
22
|
+
# A whitelist of orderable attributes.
|
23
|
+
#
|
24
|
+
# If no options are provided or the :using option is provided, defines attributes that are orderable through the operation(s) already defined in can_filter_by_default_using, or can specify attributes:
|
25
|
+
# can_order_by :attr_name_1, :attr_name_2
|
26
|
+
# So that you could call: http://.../foobars?order=attr_name_1,-attr_name_2
|
27
|
+
# to order by attr_name_1 asc then attr_name_2 desc.
|
28
|
+
#
|
29
|
+
# When :through is specified, it will take the array supplied to through as 0 to many model names following by an attribute name. It will follow through
|
30
|
+
# each association until it gets to the attribute to filter by that via ARel joins, e.g. if the model Foobar has an association to :foo, and on the Foo model there is an assocation
|
31
|
+
# to :bar, and you want to order by bar.name (foobar.foo.bar.name):
|
32
|
+
# can_order_by :my_param_name, through: {foo: {bar: :name}}
|
33
|
+
def can_order_by(*args)
|
34
|
+
options = args.extract_options!
|
35
|
+
|
36
|
+
opt_through = options.delete(:through)
|
37
|
+
raise ::Irie::ConfigurationError.new "options #{options.inspect} not supported by can_order_by" if options.present?
|
38
|
+
|
39
|
+
self.can_be_ordered_by = self.can_be_ordered_by.deep_dup
|
40
|
+
|
41
|
+
args.each do |arg|
|
42
|
+
# store as strings because we have to do a string comparison later to avoid req param symbol attack
|
43
|
+
self.can_be_ordered_by << arg.to_s unless self.can_be_ordered_by.include?(arg.to_s)
|
44
|
+
end
|
45
|
+
|
46
|
+
if opt_through
|
47
|
+
raise ::Irie::ConfigurationError.new "Must use extension :params_to_joins to use can_order_by :through" unless ancestors.include?(::Irie::Extensions::ParamsToJoins)
|
48
|
+
args.each do |through_key|
|
49
|
+
# note: handles cloning, etc.
|
50
|
+
self.define_params(through_key => opt_through)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Takes an string, symbol, array, hash to indicate order. If not a hash, assumes is ascending. Is cumulative and order defines order of sorting, e.g:
|
56
|
+
# #would order by foo_color attribute ascending
|
57
|
+
# default_order_by :foo_color
|
58
|
+
# or
|
59
|
+
# default_order_by foo_date: :asc, bar_date: :desc
|
60
|
+
# or you could be completely insane and do:
|
61
|
+
# default_order_by {foo_date: :asc}, :foo_color, 'foo_name', another_date: :asc, bar_date: :desc
|
62
|
+
def default_order_by(*args)
|
63
|
+
options = args.extract_options!
|
64
|
+
|
65
|
+
self.default_ordered_by = self.default_ordered_by.deep_dup
|
66
|
+
|
67
|
+
# hash is ordered in recent versions of Ruby we support
|
68
|
+
args.flatten.each do |item|
|
69
|
+
case item
|
70
|
+
when Hash
|
71
|
+
# merge hash converting keys to string
|
72
|
+
item.each {|param_name, direction| self.default_ordered_by[param_name.to_s] = direction}
|
73
|
+
when String, Symbol
|
74
|
+
self.default_ordered_by[item.to_s] = :asc
|
75
|
+
else
|
76
|
+
raise ::Irie::ConfigurationError.new "Can't default_order_by #{item}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# merge hash converting keys to string
|
81
|
+
options.each {|param_name, direction| self.default_ordered_by[param_name.to_s] = direction}
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
protected
|
86
|
+
|
87
|
+
def collection
|
88
|
+
logger.debug("Irie::Extensions::Order.collection") if ::Irie.debug?
|
89
|
+
object = super
|
90
|
+
|
91
|
+
already_ordered_by = []
|
92
|
+
aliased_params(:order).collect{|p| p.split(',')}.flatten.collect(&:strip).each do |split_param_name|
|
93
|
+
|
94
|
+
# not using convert_param here.
|
95
|
+
# (these should be attribute names, not attribute values.)
|
96
|
+
|
97
|
+
# remove optional preceding - or + to act as directional
|
98
|
+
direction = :asc
|
99
|
+
if split_param_name[0] == '-'
|
100
|
+
direction = :desc
|
101
|
+
split_param_name = split_param_name.reverse.chomp('-').reverse
|
102
|
+
elsif split_param_name[0] == '+'
|
103
|
+
split_param_name = split_param_name.reverse.chomp('+').reverse
|
104
|
+
end
|
105
|
+
|
106
|
+
# support for named_params/:through renaming of param name
|
107
|
+
attr_sym = attr_sym_for_param(split_param_name)
|
108
|
+
|
109
|
+
# order of logic here is important:
|
110
|
+
# do not to_sym the partial param value until passes whitelist to avoid symbol attack.
|
111
|
+
# be sure to pass in the same param name as the default param it is trying to override,
|
112
|
+
# if there is one.
|
113
|
+
if self.can_be_ordered_by.include?(split_param_name) && !already_ordered_by.include?(attr_sym)
|
114
|
+
join_to_apply = join_for_param(split_param_name)
|
115
|
+
object = object.joins(join_to_apply) if join_to_apply
|
116
|
+
arel_table_column = get_arel_table(split_param_name)[attr_sym]
|
117
|
+
raise ::Irie::ConfigurationError.new "can_order_by/define_params config problem: could not find arel table/column for param name #{split_param_name.inspect} and/or attr_sym #{attr_sym.inspect}" unless arel_table_column
|
118
|
+
#TODO: is there a better way? not sure how else to order on joined table columns- no example
|
119
|
+
sql_fragment = "#{arel_table_column.relation.name}.#{arel_table_column.name}#{direction == :desc ? ' DESC' : ''}"
|
120
|
+
# Important note! the behavior of multiple `order`'s' got reversed between Rails 4.0.0 and 4.0.1:
|
121
|
+
# http://weblog.rubyonrails.org/2013/11/1/Rails-4-0-1-has-been-released/
|
122
|
+
object = object.order(sql_fragment)
|
123
|
+
already_ordered_by << attr_sym
|
124
|
+
end
|
125
|
+
|
126
|
+
set_collection_ivar object
|
127
|
+
|
128
|
+
object
|
129
|
+
end
|
130
|
+
|
131
|
+
self.default_ordered_by.each do |split_param_name, direction|
|
132
|
+
unless already_ordered_by.include?(split_param_name)
|
133
|
+
attr_sym = attr_sym_for_param(split_param_name)
|
134
|
+
join_to_apply = join_for_param(split_param_name)
|
135
|
+
object = object.joins(join_to_apply) if join_to_apply
|
136
|
+
arel_table_column = get_arel_table(split_param_name)[attr_sym]
|
137
|
+
raise ::Irie::ConfigurationError.new "default_order_by/define_params config problem: could not find arel table/column for param name #{split_param_name.inspect} and/or attr_sym #{attr_sym.inspect}" unless arel_table_column
|
138
|
+
#TODO: is there a better way? not sure how else to order on joined table columns- no example
|
139
|
+
sql_fragment = "#{arel_table_column.relation.name}.#{arel_table_column.name}#{direction == :desc ? ' DESC' : ''}"
|
140
|
+
object = object.order(sql_fragment)
|
141
|
+
already_ordered_by << attr_sym
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
logger.debug("Irie::Extensions::Order.collection: relation.to_sql so far: #{object.to_sql}") if ::Irie.debug? && object.respond_to?(:to_sql)
|
146
|
+
|
147
|
+
set_collection_ivar object
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
|