benhoskings-hammock 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|