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