puffer 0.0.29 → 0.0.30
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.
- data/Gemfile +1 -0
- data/Gemfile.lock +56 -52
- data/README.md +51 -43
- data/VERSION +1 -1
- data/app/assets/javascripts/puffer/application.js +3 -1
- data/app/assets/stylesheets/puffer/application.css +2 -0
- data/app/assets/stylesheets/puffer/puffer.css +5 -9
- data/app/components/base_component.rb +0 -1
- data/app/components/boolean/index.html.erb +1 -1
- data/app/components/boolean_component.rb +1 -1
- data/app/components/references_many/index.html.erb +1 -0
- data/app/components/references_many_component.rb +8 -0
- data/app/components/references_one_component.rb +6 -1
- data/app/components/select/filter.html.erb +2 -0
- data/app/components/select/form.html.erb +1 -1
- data/app/components/select_component.rb +13 -2
- data/app/controllers/admin/puffer_users_controller.rb +5 -1
- data/app/controllers/admin/sessions_controller.rb +4 -1
- data/app/controllers/puffer/base.rb +1 -1
- data/app/controllers/puffer/sessions_base.rb +3 -0
- data/app/controllers/puffer/tree_base.rb +4 -4
- data/app/helpers/puffer_helper.rb +16 -15
- data/app/models/puffer/puffer_user.rb +11 -10
- data/app/models/puffer_user.rb +14 -2
- data/app/views/layouts/puffer.html.erb +16 -17
- data/app/views/layouts/puffer_base.html.erb +1 -2
- data/app/views/layouts/puffer_dashboard.html.erb +2 -2
- data/app/views/puffer/base/edit.html.erb +12 -3
- data/app/views/puffer/base/new.html.erb +12 -3
- data/app/views/puffer/dashboard_base/index.html.erb +5 -5
- data/lib/generators/puffer/component/component_generator.rb +0 -1
- data/lib/generators/puffer/controller/controller_generator.rb +1 -1
- data/lib/generators/puffer/controller/templates/controller.rb +8 -0
- data/lib/puffer.rb +77 -13
- data/lib/puffer/component.rb +14 -9
- data/lib/puffer/controller/auth.rb +41 -2
- data/lib/puffer/controller/config.rb +1 -1
- data/lib/puffer/controller/mutate.rb +15 -6
- data/lib/puffer/extensions/core.rb +4 -0
- data/lib/puffer/extensions/directive_processor.rb +36 -0
- data/lib/puffer/extensions/form.rb +1 -1
- data/lib/puffer/extensions/mapper.rb +74 -149
- data/lib/puffer/field.rb +2 -4
- data/lib/puffer/field_set.rb +3 -15
- data/lib/puffer/filters.rb +105 -0
- data/lib/puffer/orm_adapter/active_record.rb +33 -0
- data/lib/puffer/orm_adapter/base.rb +4 -0
- data/lib/puffer/orm_adapter/mongoid.rb +29 -0
- data/lib/puffer/resource.rb +63 -62
- data/lib/puffer/resource/node.rb +71 -0
- data/lib/puffer/resource/routing.rb +4 -4
- data/lib/puffer/resource/tree.rb +36 -0
- data/puffer.gemspec +28 -8
- data/spec/dummy/app/controllers/admin/posts_controller.rb +1 -1
- data/spec/dummy/app/controllers/admin/users_controller.rb +1 -0
- data/spec/dummy/app/controllers/application_controller.rb +4 -0
- data/spec/dummy/app/controllers/orms/active_record_orm_primals_controller.rb +36 -0
- data/spec/dummy/app/controllers/orms/mongoid_orm_primals_controller.rb +42 -0
- data/spec/dummy/app/models/active_record_orm.rb +5 -0
- data/spec/dummy/app/models/active_record_orm/primal.rb +2 -0
- data/spec/dummy/app/models/{mongoid_test.rb → mongoid_orm.rb} +1 -1
- data/spec/dummy/app/models/mongoid_orm/primal.rb +46 -0
- data/spec/dummy/config/environments/development.rb +3 -0
- data/spec/dummy/config/routes.rb +2 -1
- data/spec/dummy/db/migrate/20110930183902_create_active_record_orm_primals.rb +17 -0
- data/spec/dummy/db/schema.rb +15 -1
- data/spec/fabricators/active_record_orm/primal.rb +13 -0
- data/spec/fabricators/mongoid_orm/primal.rb +12 -0
- data/spec/generators/puffer/component/component_generator_spec.rb +32 -0
- data/spec/generators/puffer/controller/controller_generator_spec.rb +31 -0
- data/spec/helpers/puffer_helper_spec.rb +62 -0
- data/spec/lib/filters_spec.rb +21 -0
- data/spec/lib/orm_adapter/active_record_spec.rb +33 -0
- data/spec/lib/orm_adapter/base_shared.rb +97 -0
- data/spec/lib/orm_adapter/mongoid_spec.rb +46 -0
- data/spec/lib/resource/routing_spec.rb +5 -5
- data/spec/lib/resource/tree_spec.rb +31 -0
- data/spec/lib/resource_spec.rb +36 -37
- data/spec/spec_helper.rb +3 -2
- metadata +71 -43
- data/app/views/puffer/base/_form.html.erb +0 -11
- data/lib/generators/puffer/component/templates/component_spec.rb +0 -19
- data/lib/puffer/resource/scoping.rb +0 -19
- data/spec/dummy/app/controllers/orms/mongoid_tests_controller.rb +0 -19
- data/spec/lib/params_spec.rb +0 -120
data/lib/puffer/field.rb
CHANGED
@@ -45,9 +45,7 @@ module Puffer
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def custom_type
|
48
|
-
|
49
|
-
return :password if name =~ /password/
|
50
|
-
return reflection.macro if reflection
|
48
|
+
Puffer.field_type_for self
|
51
49
|
end
|
52
50
|
|
53
51
|
def reflection
|
@@ -59,7 +57,7 @@ module Puffer
|
|
59
57
|
end
|
60
58
|
|
61
59
|
def component_class
|
62
|
-
@component_class ||= Puffer
|
60
|
+
@component_class ||= Puffer.component_for self
|
63
61
|
end
|
64
62
|
|
65
63
|
def component
|
data/lib/puffer/field_set.rb
CHANGED
@@ -13,25 +13,13 @@ module Puffer
|
|
13
13
|
last
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
17
|
-
|
18
|
-
end
|
19
|
-
|
20
|
-
def searches query
|
21
|
-
searchable.map { |f| "#{f.query_column} like '%#{query}%'" if f.query_column.present? }.compact.join(' or ') if query
|
22
|
-
end
|
23
|
-
|
24
|
-
def boolean
|
25
|
-
@boolean ||= reject { |f| f.type != :boolean }
|
26
|
-
end
|
27
|
-
|
28
|
-
def includes
|
29
|
-
@includes ||= map {|f| f.path unless f.native?}.compact.to_includes
|
16
|
+
def columns
|
17
|
+
select {|f| f.column}.to_fieldset
|
30
18
|
end
|
31
19
|
|
32
20
|
def [] key
|
33
21
|
if key.is_a?(String) || key.is_a?(Symbol)
|
34
|
-
detect {|f| f.
|
22
|
+
detect {|f| f.field_name == key.to_s}
|
35
23
|
else
|
36
24
|
super
|
37
25
|
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Puffer
|
2
|
+
class Filters
|
3
|
+
include ActiveModel::Conversion
|
4
|
+
extend ActiveModel::Naming
|
5
|
+
extend ActiveModel::Translation
|
6
|
+
include ActiveModel::AttributeMethods
|
7
|
+
|
8
|
+
delegate :model_name, :to => 'self.class'
|
9
|
+
|
10
|
+
def initialize attributes = {}
|
11
|
+
attributes ||= {}
|
12
|
+
attributes.each do |(key, value)|
|
13
|
+
send("#{key}=", value) if respond_to?("#{key}=")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def read_attribute name
|
18
|
+
attributes[name]
|
19
|
+
end
|
20
|
+
|
21
|
+
def write_attribute name, value
|
22
|
+
attributes[name] = value.presence if attributes.key?(name)
|
23
|
+
end
|
24
|
+
|
25
|
+
def persisted?
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
def any?
|
30
|
+
attributes.keys.any? { |attribute| !send(attribute).nil? }
|
31
|
+
end
|
32
|
+
|
33
|
+
def conditions
|
34
|
+
attributes.except('puffer_search', 'puffer_order').keys.reduce({}) do |res, attribute|
|
35
|
+
value = send(attribute)
|
36
|
+
|
37
|
+
unless value.nil?
|
38
|
+
value = case value
|
39
|
+
when 'puffer_nil' then nil
|
40
|
+
when 'puffer_blank' then ''
|
41
|
+
else value
|
42
|
+
end
|
43
|
+
res[attribute] = value
|
44
|
+
end
|
45
|
+
|
46
|
+
res
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def search
|
51
|
+
puffer_search
|
52
|
+
end
|
53
|
+
|
54
|
+
def order
|
55
|
+
puffer_order
|
56
|
+
end
|
57
|
+
|
58
|
+
module ClassMethods
|
59
|
+
def model_name
|
60
|
+
@model_name ||= begin
|
61
|
+
name = super
|
62
|
+
name.instance_variable_set :@param_key, 'filters'
|
63
|
+
name
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def controller_filters controller
|
68
|
+
scope = "#{controller}Filters".split('::')[0..-2].join('::').constantize rescue Kernel
|
69
|
+
name = "#{controller}Filters".demodulize
|
70
|
+
|
71
|
+
if scope.const_defined?(name)
|
72
|
+
scope.const_get(name)
|
73
|
+
else
|
74
|
+
attributes_from_controller = controller.index_fields.reduce({}) do |res, field|
|
75
|
+
res[field.field_name] = nil
|
76
|
+
res
|
77
|
+
end
|
78
|
+
|
79
|
+
attributes_from_controller.merge!('puffer_search' => nil, 'puffer_order' => nil)
|
80
|
+
|
81
|
+
klass = Class.new(self)
|
82
|
+
|
83
|
+
attributes_from_controller.keys.each do |name|
|
84
|
+
klass.send :define_method, name do
|
85
|
+
read_attribute name
|
86
|
+
end
|
87
|
+
klass.send :define_method, "#{name}=" do |value|
|
88
|
+
write_attribute name, value
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
klass.send :define_method, :attributes do
|
93
|
+
attributes_from_controller
|
94
|
+
end
|
95
|
+
|
96
|
+
|
97
|
+
scope.const_set(name, klass)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
extend ClassMethods
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
@@ -8,6 +8,39 @@ module Puffer
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
+
def filter scope, fields, options = {}
|
12
|
+
fields = fields.columns
|
13
|
+
conditions, order = extract_conditions_and_order!(options)
|
14
|
+
|
15
|
+
conditions_fields = fields.select {|f| conditions.keys.include?(f.field_name)}.to_fieldset
|
16
|
+
search_fields = fields.select {|f| !conditions_fields.include?(f) && search_types.include?(f.column_type)}
|
17
|
+
all_fields = conditions_fields + search_fields
|
18
|
+
|
19
|
+
conditions = conditions.reduce({}) do |res, (name, value)|
|
20
|
+
field = conditions_fields[name]
|
21
|
+
res[field.query_column] = value if field
|
22
|
+
res
|
23
|
+
end
|
24
|
+
|
25
|
+
order = order.map {|field, direction| "#{field} #{direction}"}.join(', ')
|
26
|
+
|
27
|
+
scope.includes(includes(all_fields)).where(searches(search_fields, options[:search])).where(conditions).order(order)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def search_types
|
33
|
+
[:text, :string, :integer, :decimal, :float]
|
34
|
+
end
|
35
|
+
|
36
|
+
def includes fields
|
37
|
+
fields.map {|f| f.path unless f.native?}.compact.to_includes
|
38
|
+
end
|
39
|
+
|
40
|
+
def searches fields, query
|
41
|
+
[fields.map {|f| "#{f.query_column} like ?"}.compact.join(' or '), *(Array.wrap("%#{query}%") * fields.count)] if query.present?
|
42
|
+
end
|
43
|
+
|
11
44
|
end
|
12
45
|
end
|
13
46
|
end
|
@@ -8,6 +8,35 @@ module Puffer
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
+
def filter scope, fields, options = {}
|
12
|
+
fields = fields.columns
|
13
|
+
conditions, order = extract_conditions_and_order!(options)
|
14
|
+
|
15
|
+
conditions_fields = fields.select {|f| conditions.keys.include?(f.field_name)}.to_fieldset
|
16
|
+
search_fields = fields.select {|f| !conditions_fields.include?(f) && search_types.include?(f.column_type)}
|
17
|
+
all_fields = conditions_fields + search_fields
|
18
|
+
|
19
|
+
conditions = conditions.reduce({}) do |res, (name, value)|
|
20
|
+
field = conditions_fields[name]
|
21
|
+
res[field.name] = value if field
|
22
|
+
res
|
23
|
+
end
|
24
|
+
|
25
|
+
scope = scope.any_of(searches(search_fields, options[:search])) if options[:search].present?
|
26
|
+
scope.where(conditions).order(order)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def search_types
|
32
|
+
[:string, :integer, :big_decimal, :float, :"bson/object_id", :symbol]
|
33
|
+
end
|
34
|
+
|
35
|
+
def searches fields, query
|
36
|
+
regexp = /#{Regexp.escape(query)}/i
|
37
|
+
fields.map {|field| {field.name => regexp}}
|
38
|
+
end
|
39
|
+
|
11
40
|
end
|
12
41
|
end
|
13
42
|
end
|
data/lib/puffer/resource.rb
CHANGED
@@ -9,57 +9,25 @@ module Puffer
|
|
9
9
|
class Resource
|
10
10
|
|
11
11
|
include Routing
|
12
|
-
include Scoping
|
13
12
|
|
14
|
-
attr_reader :
|
15
|
-
delegate :
|
13
|
+
attr_reader :resource_node, :scope, :params, :controller_instance
|
14
|
+
delegate :controller, :name, :plural?, :to => :resource_node, :allow_nil => true
|
15
|
+
delegate :model, :to => :controller, :allow_nil => true
|
16
|
+
delegate :env, :request, :to => :controller_instance, :allow_nil => true
|
16
17
|
|
17
|
-
def initialize params,
|
18
|
+
def initialize params, controller_instance = nil
|
18
19
|
params = ActiveSupport::HashWithIndifferentAccess.new.deep_merge params
|
19
|
-
@action = params.delete :action
|
20
|
-
@controller_class = "#{params.delete :controller}_controller".camelize.constantize
|
21
|
-
@controller_name = controller_class.controller_name
|
22
|
-
@namespace = controller_class.namespace
|
23
|
-
@model_name = controller_class.model_name if controller_class.puffer?
|
24
|
-
@model = controller_class.model if controller_class.puffer?
|
25
|
-
@params = params
|
26
|
-
@controller = controller
|
27
|
-
end
|
28
20
|
|
29
|
-
|
30
|
-
params[:
|
21
|
+
@resource_node = params[:puffer]
|
22
|
+
@scope = swallow_nil{@resource_node.scope} || params[:puffer_scope]
|
23
|
+
@params = params
|
24
|
+
@controller_instance = controller_instance
|
31
25
|
end
|
32
26
|
|
33
27
|
def human
|
34
28
|
model.model_name.human
|
35
29
|
end
|
36
30
|
|
37
|
-
def parent
|
38
|
-
@parent ||= begin
|
39
|
-
parent_ancestors = params[:ancestors].dup rescue []
|
40
|
-
parent_name = parent_ancestors.pop
|
41
|
-
if parent_name
|
42
|
-
parent_params = ActiveSupport::HashWithIndifferentAccess.new({
|
43
|
-
:controller => "#{namespace}/#{parent_name.to_s.pluralize}",
|
44
|
-
:action => 'index',
|
45
|
-
:plural => parent_name.plural?,
|
46
|
-
:ancestors => parent_ancestors,
|
47
|
-
:children => []
|
48
|
-
})
|
49
|
-
|
50
|
-
parent_ancestors.each do |ancestor|
|
51
|
-
key = ancestor.to_s.singularize.foreign_key
|
52
|
-
parent_params.merge! key => params[key] if params[key]
|
53
|
-
end
|
54
|
-
parent_params.merge! :id => params[parent_name.to_s.singularize.foreign_key]
|
55
|
-
|
56
|
-
self.class.new parent_params, controller
|
57
|
-
else
|
58
|
-
nil
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
31
|
def ancestors
|
64
32
|
@ancestors ||= begin
|
65
33
|
ancestors = []
|
@@ -75,40 +43,73 @@ module Puffer
|
|
75
43
|
@root ||= (ancestors.first || self)
|
76
44
|
end
|
77
45
|
|
78
|
-
def
|
79
|
-
@
|
80
|
-
|
81
|
-
:
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
46
|
+
def parent
|
47
|
+
@parent ||= begin
|
48
|
+
if resource_node.parent
|
49
|
+
parent_params = ActiveSupport::HashWithIndifferentAccess.new({:puffer => resource_node.parent})
|
50
|
+
|
51
|
+
resource_node.ancestors[0..-2].each do |ancestor|
|
52
|
+
key = ancestor.to_s.singularize.foreign_key
|
53
|
+
parent_params[key] = params[key] if params[key]
|
54
|
+
key = key.gsub(/_id$/, '_member')
|
55
|
+
parent_params[key] = params[key] if params[key]
|
56
|
+
end
|
57
|
+
|
58
|
+
key = resource_node.parent.to_s.singularize.foreign_key
|
59
|
+
parent_params[:id] = params[key] if params[key]
|
60
|
+
key = key.gsub(/_id$/, '_member')
|
61
|
+
parent_params[:member] = params[key] if params[key]
|
62
|
+
|
63
|
+
self.class.new parent_params, controller_instance
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def children
|
69
|
+
@children ||= resource_node.children.map do |child_node|
|
70
|
+
child_params = ActiveSupport::HashWithIndifferentAccess.new({:puffer => child_node})
|
71
|
+
|
72
|
+
resource_node.ancestors.each do |ancestor|
|
89
73
|
key = ancestor.to_s.singularize.foreign_key
|
90
|
-
child_params
|
74
|
+
child_params[key] = params[key] if params[key]
|
75
|
+
key = key.gsub(/_id$/, '_member')
|
76
|
+
child_params[key] = params[key] if params[key]
|
91
77
|
end
|
92
|
-
child_params.merge! controller_name.singularize.foreign_key => params[:id] if params[:id]
|
93
78
|
|
94
|
-
|
79
|
+
key = resource_node.to_s.singularize.foreign_key
|
80
|
+
child_params[key] = params[:id] if params[:id]
|
81
|
+
key = key.gsub(/_id$/, '_member')
|
82
|
+
child_params[key] = params[:member] if params[:member]
|
83
|
+
|
84
|
+
self.class.new child_params, controller_instance
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def children_hash
|
89
|
+
@children_hash ||= children.inject(ActiveSupport::HashWithIndifferentAccess.new) do |result, child|
|
90
|
+
result[child.name] = child
|
91
|
+
result
|
95
92
|
end
|
96
93
|
end
|
97
94
|
|
98
95
|
def collection_scope
|
99
|
-
parent ? parent.member.send(
|
96
|
+
parent ? parent.member.send(name) : model
|
100
97
|
end
|
101
98
|
|
102
99
|
def collection
|
103
|
-
|
100
|
+
model.to_adapter.filter collection_scope, controller.index_fields,
|
101
|
+
:conditions => controller_instance.puffer_filters.conditions,
|
102
|
+
:search => controller_instance.puffer_filters.search,
|
103
|
+
:order => controller_instance.puffer_filters.order
|
104
104
|
end
|
105
105
|
|
106
106
|
def member
|
107
|
+
return params[:member] if params[:member]
|
107
108
|
if parent
|
108
109
|
if plural?
|
109
|
-
parent.member.send(
|
110
|
+
parent.member.send(name).find params[:id] if params[:id]
|
110
111
|
else
|
111
|
-
parent.member.send(
|
112
|
+
parent.member.send(name)
|
112
113
|
end
|
113
114
|
else
|
114
115
|
model.find params[:id] if params[:id]
|
@@ -118,9 +119,9 @@ module Puffer
|
|
118
119
|
def new_member
|
119
120
|
if parent
|
120
121
|
if plural?
|
121
|
-
parent.member.send(
|
122
|
+
parent.member.send(name).new attributes
|
122
123
|
else
|
123
|
-
parent.member.send("build_#{
|
124
|
+
parent.member.send("build_#{name}", attributes)
|
124
125
|
end
|
125
126
|
else
|
126
127
|
model.new attributes
|
@@ -128,7 +129,7 @@ module Puffer
|
|
128
129
|
end
|
129
130
|
|
130
131
|
def attributes
|
131
|
-
params[
|
132
|
+
params[name.to_s.singularize]
|
132
133
|
end
|
133
134
|
|
134
135
|
def method_missing method, *args, &block
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Puffer
|
2
|
+
class Resource
|
3
|
+
class Node
|
4
|
+
attr_accessor :parent, :children, :options
|
5
|
+
|
6
|
+
def initialize *args
|
7
|
+
@options = args.extract_options!
|
8
|
+
@parent = args.first
|
9
|
+
@children = []
|
10
|
+
@parent.children.push self if parent
|
11
|
+
end
|
12
|
+
|
13
|
+
def name
|
14
|
+
options[:name]
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
name.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def scope
|
22
|
+
options[:scope]
|
23
|
+
end
|
24
|
+
|
25
|
+
def controller
|
26
|
+
options[:controller]
|
27
|
+
end
|
28
|
+
|
29
|
+
def group
|
30
|
+
controller.configuration.group
|
31
|
+
end
|
32
|
+
|
33
|
+
def singular
|
34
|
+
name.to_s.singularize
|
35
|
+
end
|
36
|
+
|
37
|
+
def singular?
|
38
|
+
options[:singular]
|
39
|
+
end
|
40
|
+
|
41
|
+
def plural
|
42
|
+
name.to_s.pluralize
|
43
|
+
end
|
44
|
+
|
45
|
+
def plural?
|
46
|
+
!singular?
|
47
|
+
end
|
48
|
+
|
49
|
+
def root?
|
50
|
+
parent.nil?
|
51
|
+
end
|
52
|
+
|
53
|
+
def ancestors
|
54
|
+
ancestors = []
|
55
|
+
resource = self
|
56
|
+
while resource = resource.parent do
|
57
|
+
ancestors.unshift resource
|
58
|
+
end
|
59
|
+
ancestors
|
60
|
+
end
|
61
|
+
|
62
|
+
def url_string
|
63
|
+
[scope, name, (:index if singular == plural)].compact.map(&:to_s).join('_')
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_struct
|
67
|
+
{:scope => scope, :current => name, :children => children.map(&:name), :ancestors => ancestors.map(&:name)}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|