benhoskings-hammock 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/LICENSE +24 -0
  2. data/Manifest.txt +42 -0
  3. data/README.rdoc +105 -0
  4. data/Rakefile +27 -0
  5. data/lib/hammock.rb +25 -0
  6. data/lib/hammock/ajaxinate.rb +152 -0
  7. data/lib/hammock/callbacks.rb +107 -0
  8. data/lib/hammock/canned_scopes.rb +121 -0
  9. data/lib/hammock/constants.rb +7 -0
  10. data/lib/hammock/controller_attributes.rb +66 -0
  11. data/lib/hammock/export_scope.rb +74 -0
  12. data/lib/hammock/hamlink_to.rb +47 -0
  13. data/lib/hammock/javascript_buffer.rb +63 -0
  14. data/lib/hammock/logging.rb +98 -0
  15. data/lib/hammock/model_attributes.rb +38 -0
  16. data/lib/hammock/model_logging.rb +30 -0
  17. data/lib/hammock/monkey_patches/action_pack.rb +32 -0
  18. data/lib/hammock/monkey_patches/active_record.rb +227 -0
  19. data/lib/hammock/monkey_patches/array.rb +73 -0
  20. data/lib/hammock/monkey_patches/hash.rb +49 -0
  21. data/lib/hammock/monkey_patches/logger.rb +28 -0
  22. data/lib/hammock/monkey_patches/module.rb +27 -0
  23. data/lib/hammock/monkey_patches/numeric.rb +25 -0
  24. data/lib/hammock/monkey_patches/object.rb +61 -0
  25. data/lib/hammock/monkey_patches/route_set.rb +200 -0
  26. data/lib/hammock/monkey_patches/string.rb +197 -0
  27. data/lib/hammock/overrides.rb +32 -0
  28. data/lib/hammock/resource_mapping_hooks.rb +28 -0
  29. data/lib/hammock/resource_retrieval.rb +115 -0
  30. data/lib/hammock/restful_actions.rb +170 -0
  31. data/lib/hammock/restful_rendering.rb +114 -0
  32. data/lib/hammock/restful_support.rb +167 -0
  33. data/lib/hammock/route_drawing_hooks.rb +22 -0
  34. data/lib/hammock/route_for.rb +58 -0
  35. data/lib/hammock/scope.rb +120 -0
  36. data/lib/hammock/suggest.rb +36 -0
  37. data/lib/hammock/utils.rb +42 -0
  38. data/misc/scaffold.txt +83 -0
  39. data/misc/template.rb +17 -0
  40. data/tasks/hammock_tasks.rake +5 -0
  41. data/test/hammock_test.rb +8 -0
  42. 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,7 @@
1
+ module Hammock
2
+ module Constants
3
+
4
+ ImpliedUnsafeActions = [:new, :edit, :destroy]
5
+
6
+ end
7
+ 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