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,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
|