benhoskings-hammock 0.2.4
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/LICENSE +24 -0
- data/Manifest.txt +42 -0
- data/README.rdoc +105 -0
- data/Rakefile +27 -0
- data/lib/hammock.rb +25 -0
- data/lib/hammock/ajaxinate.rb +152 -0
- data/lib/hammock/callbacks.rb +107 -0
- data/lib/hammock/canned_scopes.rb +121 -0
- data/lib/hammock/constants.rb +7 -0
- data/lib/hammock/controller_attributes.rb +66 -0
- data/lib/hammock/export_scope.rb +74 -0
- data/lib/hammock/hamlink_to.rb +47 -0
- data/lib/hammock/javascript_buffer.rb +63 -0
- data/lib/hammock/logging.rb +98 -0
- data/lib/hammock/model_attributes.rb +38 -0
- data/lib/hammock/model_logging.rb +30 -0
- data/lib/hammock/monkey_patches/action_pack.rb +32 -0
- data/lib/hammock/monkey_patches/active_record.rb +227 -0
- data/lib/hammock/monkey_patches/array.rb +73 -0
- data/lib/hammock/monkey_patches/hash.rb +49 -0
- data/lib/hammock/monkey_patches/logger.rb +28 -0
- data/lib/hammock/monkey_patches/module.rb +27 -0
- data/lib/hammock/monkey_patches/numeric.rb +25 -0
- data/lib/hammock/monkey_patches/object.rb +61 -0
- data/lib/hammock/monkey_patches/route_set.rb +200 -0
- data/lib/hammock/monkey_patches/string.rb +197 -0
- data/lib/hammock/overrides.rb +32 -0
- data/lib/hammock/resource_mapping_hooks.rb +28 -0
- data/lib/hammock/resource_retrieval.rb +115 -0
- data/lib/hammock/restful_actions.rb +170 -0
- data/lib/hammock/restful_rendering.rb +114 -0
- data/lib/hammock/restful_support.rb +167 -0
- data/lib/hammock/route_drawing_hooks.rb +22 -0
- data/lib/hammock/route_for.rb +58 -0
- data/lib/hammock/scope.rb +120 -0
- data/lib/hammock/suggest.rb +36 -0
- data/lib/hammock/utils.rb +42 -0
- data/misc/scaffold.txt +83 -0
- data/misc/template.rb +17 -0
- data/tasks/hammock_tasks.rake +5 -0
- data/test/hammock_test.rb +8 -0
- metadata +129 -0
@@ -0,0 +1,167 @@
|
|
1
|
+
module Hammock
|
2
|
+
module RestfulSupport
|
3
|
+
def self.included base # :nodoc:
|
4
|
+
base.send :include, InstanceMethods
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
|
7
|
+
base.class_eval {
|
8
|
+
before_modify :set_editing
|
9
|
+
# TODO Investigate the usefulness of this.
|
10
|
+
# before_destroy :set_editing
|
11
|
+
before_create :set_creator_id_if_appropriate
|
12
|
+
helper_method :mdl, :mdl_name, :editing?, :nested_within?, :partial_exists?
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
module InstanceMethods
|
21
|
+
private
|
22
|
+
|
23
|
+
# The model this controller operates on. Defined as the singularized controller name. For example, for +GelatinousBlobsController+, this will return the +GelatinousBlob+ class.
|
24
|
+
def mdl
|
25
|
+
@hammock_cached_mdl ||= Object.const_get self.class.to_s.sub('Controller', '').classify
|
26
|
+
end
|
27
|
+
# The lowercase name of the model this controller operates on. For example, for +GelatinousBlobsController+, this will return "gelatinous_blob".
|
28
|
+
def mdl_name
|
29
|
+
@hammock_cached_mdl_name ||= self.class.to_s.sub('Controller', '').singularize.underscore
|
30
|
+
end
|
31
|
+
|
32
|
+
def current_route
|
33
|
+
@hammock_cached_current_route ||= route_for(action_name.to_sym, *current_nested_records.push(@entity)) unless @entity.nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns true if the current action represents an edit on +record+.
|
37
|
+
#
|
38
|
+
# For example, consider the route <tt>/articles/3/comments/31/edit</tt>, which fires <tt>CommentsController#edit</tt>. The nested route handler would assign <tt>@comment</tt> and <tt>@article</tt> to the appropriate records, and then the following would be observed:
|
39
|
+
# editing?(@comment) #=> true
|
40
|
+
# editing?(@article) #=> false
|
41
|
+
def editing? record
|
42
|
+
record == @editing
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns <tt>params[key]</tt>, defaulting to an empty Hash if <tt>params[key]</tt> can't receive :[].
|
46
|
+
#
|
47
|
+
# This is useful for concise nested parameter access. For example, if <tt>params[:account]</tt> is nil:
|
48
|
+
# params[:account][:email] #=> NoMethodError: undefined method `[]' for nil:NilClass
|
49
|
+
# params_for(:account)[:email] #=> nil
|
50
|
+
def params_for key
|
51
|
+
params[key] || {}
|
52
|
+
end
|
53
|
+
|
54
|
+
def assign_entity record_or_records
|
55
|
+
@entity = if record_or_records.nil?
|
56
|
+
# Fail
|
57
|
+
elsif record_or_records.is_a? ActiveRecord::Base
|
58
|
+
instance_variable_set "@#{mdl_name}", (@record = record_or_records)
|
59
|
+
elsif record_or_records.is_a? Ambition::Context
|
60
|
+
# log "Unkicked query: #{record_or_records.to_s}"
|
61
|
+
instance_variable_set "@#{mdl_name.pluralize}", (@records = record_or_records)
|
62
|
+
elsif record_or_records.is_a? Array
|
63
|
+
instance_variable_set "@#{mdl_name.pluralize}", (@records = record_or_records)
|
64
|
+
else
|
65
|
+
raise "Unknown record(s) type #{record_or_records.class}."
|
66
|
+
end
|
67
|
+
|
68
|
+
if assign_nestable_resources
|
69
|
+
@entity
|
70
|
+
else
|
71
|
+
escort :not_found
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def make_new_record resource = mdl
|
78
|
+
resource.new_with(params_for(resource.symbolize))
|
79
|
+
end
|
80
|
+
|
81
|
+
def assign_createable
|
82
|
+
assign_entity make_createable
|
83
|
+
end
|
84
|
+
|
85
|
+
def make_createable resource = mdl
|
86
|
+
if !(new_record = make_new_record(resource))
|
87
|
+
log "Couldn't create a new #{resource.base_model} with the given nesting level and parameters."
|
88
|
+
elsif !new_record.createable_by?(@current_account)
|
89
|
+
log "#{requester_name} can't create #{new_record.resource_name}."
|
90
|
+
else
|
91
|
+
new_record
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def assign_nestable_resources
|
96
|
+
@current_nested_records, @current_nested_resources = [], []
|
97
|
+
params.symbolize_keys.dragnet(*nestable_resources.keys).all? {|param_name,column_name|
|
98
|
+
constant_name = param_name.to_s.sub(/_id$/, '').camelize
|
99
|
+
constant = Object.const_get constant_name rescue nil
|
100
|
+
|
101
|
+
if constant.nil?
|
102
|
+
log "'#{constant_name}' is not available for #{param_name}."
|
103
|
+
elsif (record = constant.find_by_id(params[param_name])).nil?
|
104
|
+
log "#{constant}<#{params[param_name]}> not found."
|
105
|
+
else
|
106
|
+
@current_nested_records << record
|
107
|
+
@current_nested_resources << record.class
|
108
|
+
@record.send "#{nestable_resources[param_name]}=", params[param_name] unless @record.nil?
|
109
|
+
# log "Assigning @#{constant.name.underscore} with #{record.inspect}."
|
110
|
+
instance_variable_set "@#{constant_name.underscore}", record
|
111
|
+
end
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
def current_nested_records
|
116
|
+
@current_nested_records.nil? ? [] : @current_nested_records.dup
|
117
|
+
end
|
118
|
+
|
119
|
+
def current_nested_resources
|
120
|
+
@current_nested_resources.nil? ? [] : @current_nested_resources.dup
|
121
|
+
end
|
122
|
+
|
123
|
+
def nested_within? record_or_resource
|
124
|
+
if record_or_resource.is_a? ActiveRecord::Base
|
125
|
+
@current_nested_records.include? record_or_resource
|
126
|
+
else
|
127
|
+
@current_nested_resources.include? record_or_resource
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def safe_verb_and_implication?
|
132
|
+
request.get? && !action_name.to_s.in?(Hammock::Constants::ImpliedUnsafeActions)
|
133
|
+
end
|
134
|
+
|
135
|
+
def set_editing
|
136
|
+
@editing = @record
|
137
|
+
end
|
138
|
+
|
139
|
+
def set_creator_id_if_appropriate
|
140
|
+
@record.creator_id = @current_account.id if @record.respond_to?(:creator_id=)
|
141
|
+
end
|
142
|
+
|
143
|
+
def partial_exists? name, extension = nil
|
144
|
+
partial_name, ctrler_name = name.split('/', 2).reverse
|
145
|
+
!Dir.glob(File.join(
|
146
|
+
RAILS_ROOT,
|
147
|
+
'app/views',
|
148
|
+
ctrler_name || '',
|
149
|
+
"_#{partial_name}.html.#{extension || '*'}"
|
150
|
+
)).empty?
|
151
|
+
end
|
152
|
+
|
153
|
+
def redirect_back_or opts = {}, *parameters_for_method_reference
|
154
|
+
if request.referer.blank?
|
155
|
+
redirect_to opts, *parameters_for_method_reference
|
156
|
+
else
|
157
|
+
redirect_to request.referer
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def rendered_or_redirected?
|
162
|
+
@performed_render || @performed_redirect
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Hammock
|
2
|
+
module RouteDrawingHooks
|
3
|
+
MixInto = ActionController::Routing::RouteSet
|
4
|
+
|
5
|
+
def self.included base # :nodoc:
|
6
|
+
base.send :include, Methods
|
7
|
+
|
8
|
+
base.class_eval {
|
9
|
+
alias_method_chain_once :draw, :hammock_route_map_init
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
module Methods
|
14
|
+
|
15
|
+
def draw_with_hammock_route_map_init &block
|
16
|
+
ActionController::Routing::Routes.send :initialize_hammock_route_map
|
17
|
+
draw_without_hammock_route_map_init &block
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Hammock
|
2
|
+
module RouteFor
|
3
|
+
def self.included base # :nodoc:
|
4
|
+
base.send :include, InstanceMethods
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
|
7
|
+
base.class_eval {
|
8
|
+
helper_method :path_for, :nested_path_for, :route_for, :nested_route_for
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
end
|
14
|
+
|
15
|
+
module InstanceMethods
|
16
|
+
private
|
17
|
+
|
18
|
+
def path_for *args
|
19
|
+
route_for(*args).path
|
20
|
+
end
|
21
|
+
|
22
|
+
def nested_path_for *args
|
23
|
+
nested_route_for(*args).path
|
24
|
+
end
|
25
|
+
|
26
|
+
def route_for *args
|
27
|
+
opts = args.extract_options!
|
28
|
+
verb = args.shift if args.first.is_a?(Symbol)
|
29
|
+
|
30
|
+
ActionController::Routing::Routes.route_map.for verb_for(verb, args.last), args, opts
|
31
|
+
end
|
32
|
+
|
33
|
+
def verb_for requested_verb, record
|
34
|
+
requested_verb = :show if requested_verb.blank?
|
35
|
+
|
36
|
+
if (:show == requested_verb) && record.is_a?(Class)
|
37
|
+
:index
|
38
|
+
elsif :modify == requested_verb
|
39
|
+
record.new_record? ? :new : :edit
|
40
|
+
elsif :save == requested_verb
|
41
|
+
record.new_record? ? :create : :update
|
42
|
+
else
|
43
|
+
requested_verb
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def nested_route_for *resources
|
48
|
+
resources.delete_if &:nil?
|
49
|
+
requested_verb = resources.shift if resources.first.is_a?(Symbol)
|
50
|
+
args = @current_nested_records.dup.concat(resources)
|
51
|
+
|
52
|
+
args.unshift(requested_verb) unless requested_verb.nil?
|
53
|
+
route_for *args
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Hammock
|
2
|
+
module Scope
|
3
|
+
def self.included base # :nodoc:
|
4
|
+
base.send :include, InstanceMethods
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
|
7
|
+
base.class_eval {
|
8
|
+
helper_method :can_verb_entity?
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
end
|
14
|
+
|
15
|
+
module InstanceMethods
|
16
|
+
private
|
17
|
+
|
18
|
+
def can_verb_entity? verb, entity
|
19
|
+
if entity.is_a? ActiveRecord::Base
|
20
|
+
can_verb_record? verb, entity
|
21
|
+
else
|
22
|
+
can_verb_resource? verb, entity
|
23
|
+
end == :ok
|
24
|
+
end
|
25
|
+
|
26
|
+
def can_verb_resource? verb, resource
|
27
|
+
raise "The verb at #{call_point} must be supplied as a Symbol." unless verb.nil? || verb.is_a?(Symbol)
|
28
|
+
route = route_for verb, resource
|
29
|
+
if route.safe? && !resource.indexable_by(@current_account)
|
30
|
+
log "#{requester_name} can't index #{resource.name.pluralize}. #{describe_call_point 4}"
|
31
|
+
:not_found
|
32
|
+
elsif !route.safe? && !make_createable(resource)
|
33
|
+
log "#{requester_name} can't #{verb} #{resource.name.pluralize}. #{describe_call_point 4}"
|
34
|
+
:read_only
|
35
|
+
else
|
36
|
+
# log "#{requester_name} can #{verb} #{resource.name.pluralize}."
|
37
|
+
:ok
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def can_verb_record? verb, record
|
42
|
+
raise "The verb at #{call_point} must be supplied as a Symbol." unless verb.nil? || verb.is_a?(Symbol)
|
43
|
+
route = route_for verb, record
|
44
|
+
if route.verb.in?(:save, :create) && record.new_record?
|
45
|
+
if !record.createable_by?(@current_account)
|
46
|
+
log "#{requester_name} can't create a #{record.class} with #{record.attributes.inspect}. #{describe_call_point 4}"
|
47
|
+
:unauthed
|
48
|
+
else
|
49
|
+
:ok
|
50
|
+
end
|
51
|
+
else
|
52
|
+
if !record.readable_by?(@current_account)
|
53
|
+
log "#{requester_name} can't see #{record.class}<#{record.id}>. #{describe_call_point 4}"
|
54
|
+
:not_found
|
55
|
+
elsif !route.safe? && !record.writeable_by?(@current_account)
|
56
|
+
log "#{requester_name} can't #{verb} #{record.class}<#{record.id}>. #{describe_call_point 4}"
|
57
|
+
:read_only
|
58
|
+
else
|
59
|
+
# log "#{requester_name} can #{verb} #{record.class}<#{record.id}>."
|
60
|
+
:ok
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def verb_scope
|
66
|
+
if @current_account && (scope_name = account_verb_scope?)
|
67
|
+
# log "got an account_verb_scope #{scope_name}."
|
68
|
+
mdl.send scope_name, @current_account
|
69
|
+
elsif !(scope_name = public_verb_scope?)
|
70
|
+
log "No #{@current_account.nil? ? 'public' : 'account'} #{scope_name_for_action} scope available for #{mdl}.#{' May be available after login.' if account_verb_scope?}"
|
71
|
+
nil
|
72
|
+
else
|
73
|
+
# log "got a #{scope_name} public_verb_scope."
|
74
|
+
mdl.send scope_name
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def nest_scope
|
79
|
+
params.symbolize_keys.dragnet(*nestable_resources.keys).inject(mdl.ambition_context) {|acc,(k,v)|
|
80
|
+
# TODO this would be more ductile if it used AR assocs instead of explicit FK
|
81
|
+
eval "acc.select {|r| r.#{nestable_resources[k]} == v }"
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
def current_scope
|
86
|
+
if (resultant_scope = nest_scope.chain(verb_scope)).nil?
|
87
|
+
nil
|
88
|
+
else
|
89
|
+
resultant_scope = resultant_scope.chain(custom_scope) unless custom_scope.nil?
|
90
|
+
resultant_scope.sort_by &mdl.sorter
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def scope_name_for_action
|
98
|
+
if 'index' == action_name
|
99
|
+
'index'
|
100
|
+
elsif safe_verb_and_implication?
|
101
|
+
'read'
|
102
|
+
else
|
103
|
+
'write'
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def requester_name
|
108
|
+
@current_account.nil? ? 'Anonymous' : "#{@current_account.class}<#{@current_account.id}>"
|
109
|
+
end
|
110
|
+
|
111
|
+
def account_verb_scope?
|
112
|
+
mdl.has_account_scope? scope_name_for_action
|
113
|
+
end
|
114
|
+
def public_verb_scope?
|
115
|
+
mdl.has_public_scope? scope_name_for_action
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Hammock
|
2
|
+
module Suggest
|
3
|
+
MixInto = ActiveRecord::Base
|
4
|
+
|
5
|
+
def self.included base # :nodoc:
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
def suggest given_fields, queries, limit = 15
|
13
|
+
if (fields = given_fields & columns.map(&:name)).length != given_fields.length
|
14
|
+
log "Invalid columns #{(given_fields - fields).inspect}."
|
15
|
+
else
|
16
|
+
find(:all,
|
17
|
+
:limit => limit,
|
18
|
+
:order => fields.map {|f| "#{f} ASC" }.join(', '),
|
19
|
+
:conditions => [
|
20
|
+
fields.map {|f|
|
21
|
+
([ "LOWER(#{table_name}.#{f}) LIKE ?" ] * queries.length).join(' AND ')
|
22
|
+
}.map {|clause|
|
23
|
+
"(#{clause})"
|
24
|
+
}.join(' OR ')
|
25
|
+
].concat(queries.map{|q| "%#{q}%" } * fields.length)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
module InstanceMethods
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Hammock
|
2
|
+
module Utils
|
3
|
+
def self.included base # :nodoc:
|
4
|
+
base.send :include, Methods
|
5
|
+
base.send :extend, Methods
|
6
|
+
end
|
7
|
+
|
8
|
+
module Methods
|
9
|
+
private
|
10
|
+
|
11
|
+
require 'pathname'
|
12
|
+
def rails_root
|
13
|
+
@hammock_cached_rails_root ||= Pathname(RAILS_ROOT).realpath.to_s
|
14
|
+
end
|
15
|
+
|
16
|
+
def rails_env
|
17
|
+
ENV['RAILS_ENV'] || 'development'
|
18
|
+
end
|
19
|
+
|
20
|
+
def development?
|
21
|
+
'development' == rails_env
|
22
|
+
end
|
23
|
+
|
24
|
+
def production?
|
25
|
+
'production' == rails_env
|
26
|
+
end
|
27
|
+
|
28
|
+
def sqlite?
|
29
|
+
'SQLite' == connection.adapter_name
|
30
|
+
end
|
31
|
+
|
32
|
+
def describe_call_point offset = 0
|
33
|
+
"(called from #{call_point offset + 1})"
|
34
|
+
end
|
35
|
+
|
36
|
+
def call_point offset = 0
|
37
|
+
caller[offset + 1].strip.gsub(rails_root, '').gsub(/\:in\ .*$/, '')
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|