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,121 @@
|
|
1
|
+
module Hammock
|
2
|
+
# TODO This file is horribly non-DRY.
|
3
|
+
module CannedScopes
|
4
|
+
MixInto = ActiveRecord::Base
|
5
|
+
|
6
|
+
# TODO Put this somewhere better.
|
7
|
+
StandardVerbs = [:read, :write, :index, :create]
|
8
|
+
|
9
|
+
def self.included base
|
10
|
+
base.send :include, InstanceMethods
|
11
|
+
base.send :extend, ClassMethods
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def public_resource
|
19
|
+
public_resource_for *StandardVerbs
|
20
|
+
end
|
21
|
+
|
22
|
+
def authed_resource
|
23
|
+
authed_resource_for *StandardVerbs
|
24
|
+
end
|
25
|
+
|
26
|
+
def creator_resource
|
27
|
+
creator_resource_for *StandardVerbs
|
28
|
+
end
|
29
|
+
|
30
|
+
def partitioned_resource
|
31
|
+
partitioned_resource_for *StandardVerbs
|
32
|
+
end
|
33
|
+
|
34
|
+
def creator_resource_for *verbs
|
35
|
+
metaclass.instance_eval {
|
36
|
+
verbs.each {|verb|
|
37
|
+
send :define_method, "#{verb}_scope_for" do |account|
|
38
|
+
creator_scope account
|
39
|
+
end
|
40
|
+
}
|
41
|
+
}
|
42
|
+
define_createable :creator_scope if verbs.include?(:create)
|
43
|
+
export_scopes *verbs
|
44
|
+
end
|
45
|
+
|
46
|
+
def public_resource_for *verbs
|
47
|
+
metaclass.instance_eval {
|
48
|
+
verbs.each {|verb|
|
49
|
+
send :define_method, "#{verb}_scope" do
|
50
|
+
public_scope nil
|
51
|
+
end
|
52
|
+
}
|
53
|
+
}
|
54
|
+
define_createable :public_scope if verbs.include?(:create)
|
55
|
+
export_scopes *verbs
|
56
|
+
end
|
57
|
+
|
58
|
+
def authed_resource_for *verbs
|
59
|
+
metaclass.instance_eval {
|
60
|
+
verbs.each {|verb|
|
61
|
+
send :define_method, "#{verb}_scope_for" do |account|
|
62
|
+
authed_scope account
|
63
|
+
end
|
64
|
+
}
|
65
|
+
}
|
66
|
+
define_createable :authed_scope if verbs.include?(:create)
|
67
|
+
export_scopes *verbs
|
68
|
+
end
|
69
|
+
|
70
|
+
def partitioned_resource_for *verbs
|
71
|
+
metaclass.instance_eval {
|
72
|
+
verbs.each {|verb|
|
73
|
+
send :define_method, "#{verb}_scope_for" do |account|
|
74
|
+
partitioned_scope account
|
75
|
+
end
|
76
|
+
}
|
77
|
+
}
|
78
|
+
define_createable :partitioned_scope if verbs.include?(:create)
|
79
|
+
export_scopes *verbs
|
80
|
+
end
|
81
|
+
|
82
|
+
def define_createable scope_name
|
83
|
+
instance_eval {
|
84
|
+
send :define_method, :createable_by? do |account|
|
85
|
+
self.class.send scope_name, account
|
86
|
+
end
|
87
|
+
}
|
88
|
+
end
|
89
|
+
|
90
|
+
def public_scope account
|
91
|
+
if sqlite?
|
92
|
+
lambda {|record| 1 }
|
93
|
+
else
|
94
|
+
lambda {|record| true }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def authed_scope account
|
99
|
+
has_account = !account.nil?
|
100
|
+
lambda {|record| has_account }
|
101
|
+
end
|
102
|
+
|
103
|
+
def creator_scope account
|
104
|
+
lambda {|record| record.creator_id == account.id }
|
105
|
+
end
|
106
|
+
|
107
|
+
def partitioned_scope account
|
108
|
+
lambda {|record| record.id == account.id }
|
109
|
+
end
|
110
|
+
|
111
|
+
def empty_scope
|
112
|
+
lambda {|record| false }
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
module InstanceMethods
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Hammock
|
2
|
+
module ControllerAttributes
|
3
|
+
def self.included base # :nodoc:
|
4
|
+
base.send :include, InstanceMethods
|
5
|
+
base.send :extend, ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
|
10
|
+
# Specifies parent resources that can appear above this one in the route, and will be applied as an extra scope condition whenever present.
|
11
|
+
#
|
12
|
+
# Supplied as a hash of parameter names to attribute names. For example, given the route <tt>/accounts/7/posts/31</tt>,
|
13
|
+
# nestable_by :account_id => :creator_id
|
14
|
+
# Would add an extra scope condition requiring that <tt>@post.creator_id</tt> == <tt>params[:account_id]</tt>.
|
15
|
+
def nestable_by resources
|
16
|
+
write_inheritable_attribute :nestable_by, resources
|
17
|
+
end
|
18
|
+
|
19
|
+
# When +inline_create+ is specified for a controller, the +index+ page will have the ability to directly create new resources, just as the +new+ page normally can.
|
20
|
+
#
|
21
|
+
# To use +inline_create+, refactor the relevant contents of your +new+ view into a partial and render it in an appropriate place within the +index+ view.
|
22
|
+
#
|
23
|
+
# A successful +create+ will redirect to the +show+ action for the new record, and a failed +create+ will re-render the +index+ action with a populated form, in the same way the +new+ action would normally be rendered in the event of a failed +create+.
|
24
|
+
def inline_create
|
25
|
+
write_inheritable_attribute :inline_create, true
|
26
|
+
end
|
27
|
+
|
28
|
+
# When +find_on_create+ is specified for a controller, attempts to +create+ new records will first check to see if an identical record already exists. If such a record is found, it is returned and the create is never attempted.
|
29
|
+
#
|
30
|
+
# This is useful for the management of administrative records like memberships or friendships, where the user may attempt to create a new record using some unique identifier like an email address. For such a resource, a pre-existing record should not be considered a failure, as would otherwise be triggered by uniqueness checks on the model.
|
31
|
+
def find_on_create
|
32
|
+
write_inheritable_attribute :find_on_create, true
|
33
|
+
end
|
34
|
+
|
35
|
+
# Use +find_column+ to specify the name of an alternate column with which record lookups should be performed.
|
36
|
+
#
|
37
|
+
# This is useful for controllers that are indexed by primary key, but are accessed with URLs containing some other unique attribute of the resource, like a randomly-generated key.
|
38
|
+
# find_column :key
|
39
|
+
def find_column column_name
|
40
|
+
# TODO define to_param on model.
|
41
|
+
write_inheritable_attribute :find_column, column_name
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module InstanceMethods
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def nestable_resources
|
50
|
+
self.class.read_inheritable_attribute(:nestable_by) || {}
|
51
|
+
end
|
52
|
+
|
53
|
+
def inline_createable_resource?
|
54
|
+
self.class.read_inheritable_attribute :inline_create
|
55
|
+
end
|
56
|
+
|
57
|
+
def findable_on_create?
|
58
|
+
self.class.read_inheritable_attribute :find_on_create
|
59
|
+
end
|
60
|
+
|
61
|
+
def find_column_name
|
62
|
+
self.class.read_inheritable_attribute(:find_column) || :id
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Hammock
|
2
|
+
module ExportScope
|
3
|
+
MixInto = ActiveRecord::Base
|
4
|
+
|
5
|
+
def self.included base
|
6
|
+
base.send :include, InstanceMethods
|
7
|
+
base.send :extend, ClassMethods
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
def has_public_scope? scope_name
|
13
|
+
"#{scope_name}able" if respond_to? "#{scope_name}_scope"
|
14
|
+
end
|
15
|
+
|
16
|
+
def has_account_scope? scope_name
|
17
|
+
"#{scope_name}able_by" if respond_to? "#{scope_name}_scope_for"
|
18
|
+
end
|
19
|
+
|
20
|
+
def export_scopes *verbs
|
21
|
+
verbs.discard(:create).each {|verb| export_scope verb }
|
22
|
+
end
|
23
|
+
|
24
|
+
def export_scope verb
|
25
|
+
verbable = "#{verb}able"
|
26
|
+
|
27
|
+
metaclass.instance_eval {
|
28
|
+
# Model.verbable_by: returns all records that are verbable by account.
|
29
|
+
define_method "#{verbable}_by" do |account|
|
30
|
+
if !account.nil? && respond_to?("#{verb}_scope_for")
|
31
|
+
select &send("#{verb}_scope_for", account)
|
32
|
+
elsif respond_to?("#{verb}_scope")
|
33
|
+
select &send("#{verb}_scope")
|
34
|
+
else
|
35
|
+
log "No #{verb} scopes available."
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Model.verbable: returns all records that are verbable by anonymous users.
|
41
|
+
define_method verbable do
|
42
|
+
send "#{verbable}_by", nil
|
43
|
+
end
|
44
|
+
}
|
45
|
+
|
46
|
+
# Model#verbable_by?: returns whether this record is verbable by account.
|
47
|
+
define_method "#{verbable}_by?" do |account|
|
48
|
+
if !account.nil? && self.class.respond_to?("#{verb}_scope_for")
|
49
|
+
self.class.send("#{verb}_scope_for", account).call(self)
|
50
|
+
elsif self.class.respond_to?("#{verb}_scope")
|
51
|
+
self.class.send("#{verb}_scope").call(self)
|
52
|
+
else
|
53
|
+
log "No #{verb} scopes available, returning false."
|
54
|
+
false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Model#verbable?: returns whether this record is verbable by anonymous users.
|
59
|
+
define_method "#{verbable}?" do
|
60
|
+
send "#{verbable}_by?", nil
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
module InstanceMethods
|
67
|
+
|
68
|
+
def createable_by? account
|
69
|
+
false
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Hammock
|
2
|
+
module HamlinkTo
|
3
|
+
MixInto = ActionView::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
|
+
end
|
12
|
+
|
13
|
+
module InstanceMethods
|
14
|
+
|
15
|
+
# Generate a restful link to verb the provided resources if the request would be allowed under the current scope.
|
16
|
+
# If the scope would not allow the specified request, the link is ommitted entirely from the page.
|
17
|
+
#
|
18
|
+
# Use this method to render edit and delete links, and anything else that is only available to certain users or under certain conditions.
|
19
|
+
def hamlink_to *args
|
20
|
+
opts = args.extract_options!
|
21
|
+
verb = args.first if args.first.is_a?(Symbol)
|
22
|
+
entity = args.last
|
23
|
+
|
24
|
+
if can_verb_entity?(verb, entity)
|
25
|
+
route = route_for *args.push(opts.dragnet(:nest, :format))
|
26
|
+
|
27
|
+
# opts[:class] = ['current', opts[:class]].squash.join(' ') if opts[:indicate_current] && (route == controller.current_route)
|
28
|
+
opts[:class] = [link_class_for(route.verb, entity), opts[:class]].squash.join(' ')
|
29
|
+
|
30
|
+
text = opts.delete(:text) || opts.delete(:text_or_else)
|
31
|
+
|
32
|
+
if text.is_a?(Symbol)
|
33
|
+
text = entity.send(text)
|
34
|
+
end
|
35
|
+
|
36
|
+
link_to(text || route.verb,
|
37
|
+
route.path(opts.delete(:params)),
|
38
|
+
opts.merge(:method => (route.http_method unless route.get?))
|
39
|
+
)
|
40
|
+
else
|
41
|
+
opts[:else] || opts[:text_or_else]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Hammock
|
2
|
+
module JavascriptBuffer
|
3
|
+
MixInto = ActionView::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
|
+
end
|
12
|
+
|
13
|
+
module InstanceMethods
|
14
|
+
|
15
|
+
# Add +snippet+ to the request's domready javascript cache.
|
16
|
+
#
|
17
|
+
# The contents of this cache can be rendered into a jQuery <tt>$(function() { ... })</tt> block within a <tt>\<script type="text/javascript"></tt> block by calling <tt>javascript_for_page</tt> within the \<head> of the layout.
|
18
|
+
def append_javascript snippet
|
19
|
+
# TODO This should be an array of strings.
|
20
|
+
@_domready_javascript ||= ''
|
21
|
+
@_domready_javascript << snippet.strip.end_with(';') << "\n\n" unless snippet.nil?
|
22
|
+
end
|
23
|
+
|
24
|
+
# Add +snippet+ to the request's toplevel javascript cache.
|
25
|
+
#
|
26
|
+
# The contents of this cache can be rendered into a <tt>\<script type="text/javascript"></tt> block by calling <tt>javascript_for_page</tt> within the \<head> of the layout.
|
27
|
+
def append_toplevel_javascript snippet
|
28
|
+
@_toplevel_javascript ||= ''
|
29
|
+
@_toplevel_javascript << snippet.strip.end_with(';') << "\n\n" unless snippet.nil?
|
30
|
+
end
|
31
|
+
|
32
|
+
# Render the snippets cached by +append_javascript+ and +append_toplevel_javascript+ within a <tt>\<script type="text/javascript"></tt> tag.
|
33
|
+
#
|
34
|
+
# This should be called somewhere within the \<head> in your layout.
|
35
|
+
def javascript_for_page
|
36
|
+
javascript_tag %Q{
|
37
|
+
#{@_toplevel_javascript}
|
38
|
+
|
39
|
+
(jQuery)(function() {
|
40
|
+
#{@_domready_javascript}
|
41
|
+
});
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
# If the current request is XHR, render all cached javascript as +javascript_for_page+ would and clear the request's javascript cache.
|
46
|
+
#
|
47
|
+
# The purpose of this method is for rendering javascript into partials that form XHR responses, without causing duplicate javascript to be rendered by nested partials multiply calling this method.
|
48
|
+
def javascript_for_ajax_response
|
49
|
+
# TODO this should be called from outside the partials somewhere, once only
|
50
|
+
if request.xhr?
|
51
|
+
js = javascript_for_page
|
52
|
+
clear_js_caches
|
53
|
+
js
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def clear_js_caches
|
58
|
+
@_domready_javascript = @_toplevel_javascript = nil
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Hammock
|
2
|
+
module Logging
|
3
|
+
MixInto = ActionController::Base
|
4
|
+
|
5
|
+
def self.included base
|
6
|
+
base.send :include, Methods
|
7
|
+
base.send :extend, Methods
|
8
|
+
|
9
|
+
base.class_eval {
|
10
|
+
helper_method :log
|
11
|
+
}
|
12
|
+
end
|
13
|
+
|
14
|
+
module Methods
|
15
|
+
|
16
|
+
def log_hit
|
17
|
+
log_concise [
|
18
|
+
request.remote_ip.colorize('green'),
|
19
|
+
(@current_site.subdomain unless @current_site.nil?),
|
20
|
+
(session.nil? ? 'nil' : ('...' + session.session_id[-8, 8])),
|
21
|
+
(@current_account.nil? ? "unauthed" : "Account<#{@current_account.id}> #{@current_account.name}").colorize('green'),
|
22
|
+
headers['Status'],
|
23
|
+
log_hit_request_info,
|
24
|
+
log_hit_route_info
|
25
|
+
].squash.join(' | ')
|
26
|
+
end
|
27
|
+
|
28
|
+
def log_hit_request_info
|
29
|
+
(request.xhr? ? 'XHR/' : '') +
|
30
|
+
(params[:_method] || request.method).to_s.upcase +
|
31
|
+
' ' +
|
32
|
+
request.request_uri.colorize('grey', '?')
|
33
|
+
end
|
34
|
+
|
35
|
+
def log_hit_route_info
|
36
|
+
params[:controller] +
|
37
|
+
'#' +
|
38
|
+
params[:action] +
|
39
|
+
' ' +
|
40
|
+
params.discard(:controller, :action).inspect.gsub("\n", '\n').colorize('grey')
|
41
|
+
end
|
42
|
+
|
43
|
+
def log_concise msg, report = false
|
44
|
+
buf = "#{Time.now.strftime('%Y-%m-%d %H:%M:%S %Z')} | #{msg}\n"
|
45
|
+
path = File.join RAILS_ROOT, 'log', rails_env
|
46
|
+
|
47
|
+
File.open("#{path}.concise.log", 'a') {|f| f << buf }
|
48
|
+
File.open("#{path}.report.log", 'a') {|f| f << buf } if report
|
49
|
+
|
50
|
+
nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def report *args
|
54
|
+
opts = args.extract_options!
|
55
|
+
log *(args << opts.merge(:report => true, :skip => (opts[:skip] || 0) + 1))
|
56
|
+
log caller.remove_framework_backtrace.join("\n")
|
57
|
+
end
|
58
|
+
|
59
|
+
def dlog *args
|
60
|
+
unless production?
|
61
|
+
opts = args.extract_options!
|
62
|
+
log *(args << opts.merge(:skip => (opts[:skip] || 0) + 1))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def log_fail *args
|
67
|
+
log *(args << opts.merge(:skip => (opts[:skip] || 0) + 1))
|
68
|
+
false
|
69
|
+
end
|
70
|
+
|
71
|
+
def log *args
|
72
|
+
opts = {
|
73
|
+
:skip => 0
|
74
|
+
}.merge(args.extract_options!)
|
75
|
+
|
76
|
+
msg = if opts[:error]
|
77
|
+
"#{ErrorPrefix}: #{opts[:error]}"
|
78
|
+
elsif args.first.is_a? String
|
79
|
+
args.first
|
80
|
+
elsif args.all? {|i| i.is_a?(ActiveRecord::Base) }
|
81
|
+
@errorModels = args unless opts[:errorModels] == false
|
82
|
+
args.map {|record| "#{record.inspect}: #{record.errors.full_messages.inspect}" }.join(', ')
|
83
|
+
else
|
84
|
+
args.map(&:inspect).join(', ')
|
85
|
+
end
|
86
|
+
|
87
|
+
msg.colorize!('on red') if opts[:error] || opts[:report]
|
88
|
+
|
89
|
+
callpoint = caller[opts[:skip]].sub(rails_root.end_with('/'), '')
|
90
|
+
entry = "#{callpoint}#{msg.blank? ? (opts[:report] ? ' <-- something broke here' : '.') : ' | '}#{msg}"
|
91
|
+
|
92
|
+
logger.send opts[:error].blank? ? :info : :error, entry # Write to the Rails log
|
93
|
+
log_concise entry, opts[:report] # Also write to the concise log
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|