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,32 @@
|
|
1
|
+
module Hammock
|
2
|
+
module Overrides
|
3
|
+
def self.included base # :nodoc:
|
4
|
+
base.send :include, InstanceMethods
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module InstanceMethods
|
12
|
+
private
|
13
|
+
|
14
|
+
def custom_scope
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def postsave_render result
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def postsave_redirect
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def postdestroy_redirect
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Hammock
|
2
|
+
module ResourceMappingHooks
|
3
|
+
MixInto = ActionController::Resources
|
4
|
+
|
5
|
+
def self.included base # :nodoc:
|
6
|
+
base.send :include, Methods
|
7
|
+
|
8
|
+
base.class_eval {
|
9
|
+
alias_method_chain_once :map_resource, :hammock_route_map
|
10
|
+
alias_method_chain_once :map_singleton_resource, :hammock_route_map
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
module Methods
|
15
|
+
|
16
|
+
def map_resource_with_hammock_route_map entity, options = {}, &block
|
17
|
+
ActionController::Routing::Routes.route_map.add entity, options
|
18
|
+
map_resource_without_hammock_route_map entity, options, &block
|
19
|
+
end
|
20
|
+
|
21
|
+
def map_singleton_resource_with_hammock_route_map entity, options = {}, &block
|
22
|
+
ActionController::Routing::Routes.route_map.add_singleton entity, options
|
23
|
+
map_singleton_resource_without_hammock_route_map entity, options, &block
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
module Hammock
|
2
|
+
module ResourceRetrieval
|
3
|
+
def self.included base # :nodoc:
|
4
|
+
base.send :include, InstanceMethods
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module InstanceMethods
|
12
|
+
private
|
13
|
+
|
14
|
+
def find_deleted_record
|
15
|
+
find_record :find_with_deleted
|
16
|
+
end
|
17
|
+
|
18
|
+
def find_record finder = :find
|
19
|
+
result = if !callback(:before_find)
|
20
|
+
# callbacks failed
|
21
|
+
elsif (record = retrieve_record(finder)).nil?
|
22
|
+
log "#{mdl}<#{params[:id]}> doesn't exist within #{requester_name.possessive} #{action_name} scope."
|
23
|
+
:not_found
|
24
|
+
elsif :ok != (verbability = can_verb_record?(action_name.to_sym, record))
|
25
|
+
verbability
|
26
|
+
elsif !callback(:during_find, record)
|
27
|
+
# callbacks failed
|
28
|
+
else
|
29
|
+
:ok
|
30
|
+
end
|
31
|
+
|
32
|
+
if :ok != result
|
33
|
+
escort(result)
|
34
|
+
else
|
35
|
+
assign_entity record
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def retrieve_resource
|
40
|
+
if (scope = current_scope).nil?
|
41
|
+
escort :unauthed
|
42
|
+
else
|
43
|
+
assign_entity scope
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def retrieve_record finder
|
48
|
+
if (scope = current_scope).nil?
|
49
|
+
|
50
|
+
else
|
51
|
+
record = scope.send finder, :first, :conditions => {find_column_name => params[:id]}
|
52
|
+
record || required_callback(:after_failed_find)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def escort reason
|
57
|
+
if rendered_or_redirected?
|
58
|
+
# lol
|
59
|
+
elsif request.xhr?
|
60
|
+
# TODO bad request might only be appropriate for invalid requests, as opposed to just an auth failure.
|
61
|
+
escort_for_bad_request
|
62
|
+
elsif :relogin == reason
|
63
|
+
reset_session # TODO: instead of a full reset, this should just set unauthed so we can remember who the user was without granting them their creds.
|
64
|
+
redirect_to returning_login_path
|
65
|
+
elsif :readonly == reason
|
66
|
+
escort_for_read_only
|
67
|
+
elsif :unauthed == reason
|
68
|
+
escort_for_403
|
69
|
+
elsif @current_account.nil? && account_verb_scope?
|
70
|
+
escort_for_login
|
71
|
+
else
|
72
|
+
escort_for_404
|
73
|
+
end
|
74
|
+
false
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def returning_login_path
|
81
|
+
session[:path_after_login] = request.request_uri
|
82
|
+
login_path
|
83
|
+
end
|
84
|
+
|
85
|
+
def escort_for_bad_request
|
86
|
+
log
|
87
|
+
render :nothing => true, :status => 400
|
88
|
+
end
|
89
|
+
def escort_for_read_only
|
90
|
+
log
|
91
|
+
redirect_to :action => :show
|
92
|
+
end
|
93
|
+
def escort_for_login
|
94
|
+
log
|
95
|
+
# render :partial => 'login/account', :status => 401 # unauthorized
|
96
|
+
redirect_to returning_login_path
|
97
|
+
end
|
98
|
+
def escort_for_404
|
99
|
+
if partial_exists? 'shared/status_404'
|
100
|
+
render :partial => 'shared/status_404', :layout => true
|
101
|
+
else
|
102
|
+
render :file => File.join(RAILS_ROOT, 'public/404.html'), :status => 404
|
103
|
+
end
|
104
|
+
end
|
105
|
+
def escort_for_403
|
106
|
+
if partial_exists? 'shared/status_404'
|
107
|
+
render :partial => 'shared/status_404', :layout => true
|
108
|
+
else
|
109
|
+
render :file => File.join(RAILS_ROOT, 'public/404.html'), :status => 403
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
module Hammock
|
2
|
+
module RestfulActions
|
3
|
+
def self.included base # :nodoc:
|
4
|
+
base.send :include, InstanceMethods
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module InstanceMethods
|
12
|
+
|
13
|
+
# The +index+ action. (GET, safe, idempotent)
|
14
|
+
#
|
15
|
+
# Lists the current resource's records that are visible within the current index scope, defined by +index_scope+ and +index_scope_for+ on the current model.
|
16
|
+
def index
|
17
|
+
if tasks_for_index
|
18
|
+
respond_to do |format|
|
19
|
+
format.html
|
20
|
+
format.xml { render :xml => @records.kick }
|
21
|
+
format.json { render :json => @records.kick }
|
22
|
+
format.yaml { render :text => @records.kick.to_yaml }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# The +new+ action. (GET, safe, idempotent)
|
28
|
+
#
|
29
|
+
# Renders a form containing the required fields to create a new record.
|
30
|
+
#
|
31
|
+
# This action is available within the same scope as +create+, since it is only useful if a subsequent +create+ would be successful.
|
32
|
+
def new
|
33
|
+
if !tasks_for_new
|
34
|
+
escort :unauthed
|
35
|
+
else
|
36
|
+
render_for_safe_actions
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# The +create+ action. (POST, unsafe, non-idempotent)
|
41
|
+
#
|
42
|
+
# Creates a new record with the supplied attributes. TODO
|
43
|
+
def create
|
44
|
+
if !find_record_on_create && !assign_createable
|
45
|
+
escort :unauthed
|
46
|
+
else
|
47
|
+
render_or_redirect_after save_record
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# The +show+ action. (GET, safe, idempotent)
|
52
|
+
#
|
53
|
+
# Displays the specified record if it is within the current read scope, defined by +read_scope+ and +read_scope_for+ on the current model.
|
54
|
+
def show
|
55
|
+
if find_record
|
56
|
+
render_for_safe_actions if callback(:before_show)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# The +edit+ action. (GET, safe, idempotent)
|
61
|
+
#
|
62
|
+
# Renders a form containing the fields populated with the current record's attributes.
|
63
|
+
#
|
64
|
+
# This action is available within the same scope as +update+, since it is only useful if a subsequent +update+ would be successful.
|
65
|
+
def edit
|
66
|
+
if find_record
|
67
|
+
render_for_safe_actions if callback(:before_modify) and callback(:before_edit)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# The +update+ action. (PUT, unsafe, idempotent)
|
72
|
+
#
|
73
|
+
# Updates the specified record with the supplied attributes if it is within the current write scope, defined by +write_scope+ and +write_scope_for+ on the current model.
|
74
|
+
def update
|
75
|
+
if find_record
|
76
|
+
# If params[:attribute] is given, update only that attribute. We mass-assign either way to filter through attr_accessible.
|
77
|
+
@record.attributes = if (attribute_name = params[:attribute])
|
78
|
+
{ attribute_name => params_for(mdl.symbolize)[attribute_name] }
|
79
|
+
else
|
80
|
+
params_for mdl.symbolize
|
81
|
+
end
|
82
|
+
|
83
|
+
render_or_redirect_after save_record
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# The +destroy+ action. (DELETE, unsafe, non-idempotent)
|
88
|
+
#
|
89
|
+
# Destroys the specified record if it is within the current write scope, defined by +write_scope+ and +write_scope_for+ on the current model.
|
90
|
+
def destroy
|
91
|
+
if find_record
|
92
|
+
result = callback(:before_destroy) and @record.destroy and callback(:after_destroy)
|
93
|
+
render_for_destroy result
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# The +undestroy+ action. (POST, unsafe, idempotent)
|
98
|
+
#
|
99
|
+
# Reverses a previous destroy on the specified record if it is within the current write scope, defined by +write_scope+ and +write_scope_for+ on the current model.
|
100
|
+
def undestroy
|
101
|
+
if find_deleted_record
|
102
|
+
result = callback(:before_undestroy) and @record.undestroy and callback(:after_undestroy)
|
103
|
+
render_for_destroy result
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# The +suggest+ action. (GET, safe, idempotent)
|
108
|
+
#
|
109
|
+
# Lists the current resource's records that would be listed by an index, filtered to show only those where at least one of the specified keys matches each whitespace-separated term in the query.
|
110
|
+
def suggest
|
111
|
+
@results = if params[:q].blank?
|
112
|
+
log 'No query specified.'
|
113
|
+
elsif params[:fields].blank?
|
114
|
+
log "No fields specified."
|
115
|
+
elsif !callback(:before_suggest)
|
116
|
+
# fail
|
117
|
+
else
|
118
|
+
fields = params[:fields].split(',')
|
119
|
+
@queries = params[:q].downcase.split(/\s+/)
|
120
|
+
|
121
|
+
mdl.suggest fields, @queries
|
122
|
+
end
|
123
|
+
|
124
|
+
if @results.nil?
|
125
|
+
escort_for_bad_request
|
126
|
+
else
|
127
|
+
callback(:after_suggest)
|
128
|
+
render :action => "suggest_#{fields.join(',')}", :layout => false
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def tasks_for_index
|
136
|
+
retrieve_resource and callback(:before_index) and (!inline_createable_resource? || tasks_for_new)
|
137
|
+
end
|
138
|
+
|
139
|
+
def tasks_for_new
|
140
|
+
callback(:before_modify) and callback(:before_new) if assign_createable
|
141
|
+
end
|
142
|
+
|
143
|
+
def find_record_on_create
|
144
|
+
if findable_on_create?
|
145
|
+
if record = nest_scope.find(:first, :conditions => params_for(mdl.symbolize))
|
146
|
+
log "suitable record already exists: #{record}"
|
147
|
+
assign_entity record
|
148
|
+
else
|
149
|
+
log "couldn't find a suitable record, proceeding with creation."
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def save_record
|
155
|
+
verb = @record.new_record? ? 'create' : 'update'
|
156
|
+
if callback("before_#{verb}") and callback(:before_save) and save
|
157
|
+
callback("after_#{verb}") and callback(:after_save)
|
158
|
+
else
|
159
|
+
log "#{mdl} errors: " + @record.errors.full_messages.join(', ')
|
160
|
+
callback("after_failed_#{verb}") and callback(:after_failed_save) and false
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def save
|
165
|
+
@record.save
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Hammock
|
2
|
+
module RestfulRendering
|
3
|
+
def self.included base # :nodoc:
|
4
|
+
base.send :include, InstanceMethods
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
end
|
10
|
+
|
11
|
+
module InstanceMethods
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def render_or_redirect_after result
|
16
|
+
if request.xhr?
|
17
|
+
render_for_safe_actions result, :editable => true, :edit => false
|
18
|
+
else
|
19
|
+
if postsave_render result
|
20
|
+
# rendered - no redirect
|
21
|
+
elsif result
|
22
|
+
render_http_success
|
23
|
+
else
|
24
|
+
render_http_failure
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def render_for_safe_actions result = true, opts = {}
|
30
|
+
if request.xhr?
|
31
|
+
if params[:attribute]
|
32
|
+
render_attribute opts
|
33
|
+
elsif params[:display_as]
|
34
|
+
render_as
|
35
|
+
else
|
36
|
+
respond_to {|format|
|
37
|
+
format.html { render :template => "#{controller_name}/#{action_name}", :layout => false }
|
38
|
+
format.xml
|
39
|
+
format.js
|
40
|
+
}
|
41
|
+
end
|
42
|
+
else
|
43
|
+
respond_to do |format|
|
44
|
+
format.html
|
45
|
+
format.xml { render :xml => @record }
|
46
|
+
format.js
|
47
|
+
end unless rendered_or_redirected?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def render_http_success
|
52
|
+
flash[:notice] = "#{mdl} was successfully #{@record.new_or_deleted_before_save? ? 'created' : 'updated'}."
|
53
|
+
respond_to do |format|
|
54
|
+
format.html {
|
55
|
+
default_redirect_path = postsave_redirect || nested_path_for(inline_createable_resource? ? mdl : @entity)
|
56
|
+
|
57
|
+
if params[:redirect] == 'back'
|
58
|
+
redirect_back_or default_redirect_path
|
59
|
+
else
|
60
|
+
redirect_to default_redirect_path
|
61
|
+
end
|
62
|
+
}
|
63
|
+
format.xml {
|
64
|
+
if @record.new_or_deleted_before_save?
|
65
|
+
render :xml => @record, :status => :created, :location => @record
|
66
|
+
else # update
|
67
|
+
head :ok
|
68
|
+
end
|
69
|
+
}
|
70
|
+
format.js
|
71
|
+
format.json { render :json => {:result => 'success'}.to_json }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def render_http_failure
|
76
|
+
respond_to do |format|
|
77
|
+
format.html {
|
78
|
+
if inline_createable_resource?
|
79
|
+
tasks_for_index
|
80
|
+
render :action => :index
|
81
|
+
else
|
82
|
+
render :action => (@record.new_record? ? 'new' : 'edit')
|
83
|
+
end
|
84
|
+
}
|
85
|
+
format.xml { render :xml => @record.errors, :status => :unprocessable_entity }
|
86
|
+
format.js
|
87
|
+
format.json { render :json => {:result => 'failure'}.to_json }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def render_attribute opts = {}
|
92
|
+
render :partial => "restful/attribute", :locals => {
|
93
|
+
:record => @record,
|
94
|
+
:attribute => params[:attribute],
|
95
|
+
:editable => opts[:editable],
|
96
|
+
:editing => editing?(@record)
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
def render_for_destroy success, opts = {}
|
101
|
+
if request.xhr?
|
102
|
+
render :partial => "shared/#{action_name}", :locals => { :record => @record }
|
103
|
+
else
|
104
|
+
respond_to do |format|
|
105
|
+
format.html { redirect_to postdestroy_redirect || nested_path_for(@record.class) }
|
106
|
+
format.xml { head :ok }
|
107
|
+
format.js { render :partial => "shared/#{action_name}", :locals => {:record => @record} }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|