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,167 @@
1
+ module Hammock
2
+ module RestfulSupport
3
+ def self.included base # :nodoc:
4
+ base.send :include, InstanceMethods
5
+ base.send :extend, ClassMethods
6
+
7
+ base.class_eval {
8
+ before_modify :set_editing
9
+ # TODO Investigate the usefulness of this.
10
+ # before_destroy :set_editing
11
+ before_create :set_creator_id_if_appropriate
12
+ helper_method :mdl, :mdl_name, :editing?, :nested_within?, :partial_exists?
13
+ }
14
+ end
15
+
16
+ module ClassMethods
17
+
18
+ end
19
+
20
+ module InstanceMethods
21
+ private
22
+
23
+ # The model this controller operates on. Defined as the singularized controller name. For example, for +GelatinousBlobsController+, this will return the +GelatinousBlob+ class.
24
+ def mdl
25
+ @hammock_cached_mdl ||= Object.const_get self.class.to_s.sub('Controller', '').classify
26
+ end
27
+ # The lowercase name of the model this controller operates on. For example, for +GelatinousBlobsController+, this will return "gelatinous_blob".
28
+ def mdl_name
29
+ @hammock_cached_mdl_name ||= self.class.to_s.sub('Controller', '').singularize.underscore
30
+ end
31
+
32
+ def current_route
33
+ @hammock_cached_current_route ||= route_for(action_name.to_sym, *current_nested_records.push(@entity)) unless @entity.nil?
34
+ end
35
+
36
+ # Returns true if the current action represents an edit on +record+.
37
+ #
38
+ # For example, consider the route <tt>/articles/3/comments/31/edit</tt>, which fires <tt>CommentsController#edit</tt>. The nested route handler would assign <tt>@comment</tt> and <tt>@article</tt> to the appropriate records, and then the following would be observed:
39
+ # editing?(@comment) #=> true
40
+ # editing?(@article) #=> false
41
+ def editing? record
42
+ record == @editing
43
+ end
44
+
45
+ # Returns <tt>params[key]</tt>, defaulting to an empty Hash if <tt>params[key]</tt> can't receive :[].
46
+ #
47
+ # This is useful for concise nested parameter access. For example, if <tt>params[:account]</tt> is nil:
48
+ # params[:account][:email] #=> NoMethodError: undefined method `[]' for nil:NilClass
49
+ # params_for(:account)[:email] #=> nil
50
+ def params_for key
51
+ params[key] || {}
52
+ end
53
+
54
+ def assign_entity record_or_records
55
+ @entity = if record_or_records.nil?
56
+ # Fail
57
+ elsif record_or_records.is_a? ActiveRecord::Base
58
+ instance_variable_set "@#{mdl_name}", (@record = record_or_records)
59
+ elsif record_or_records.is_a? Ambition::Context
60
+ # log "Unkicked query: #{record_or_records.to_s}"
61
+ instance_variable_set "@#{mdl_name.pluralize}", (@records = record_or_records)
62
+ elsif record_or_records.is_a? Array
63
+ instance_variable_set "@#{mdl_name.pluralize}", (@records = record_or_records)
64
+ else
65
+ raise "Unknown record(s) type #{record_or_records.class}."
66
+ end
67
+
68
+ if assign_nestable_resources
69
+ @entity
70
+ else
71
+ escort :not_found
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def make_new_record resource = mdl
78
+ resource.new_with(params_for(resource.symbolize))
79
+ end
80
+
81
+ def assign_createable
82
+ assign_entity make_createable
83
+ end
84
+
85
+ def make_createable resource = mdl
86
+ if !(new_record = make_new_record(resource))
87
+ log "Couldn't create a new #{resource.base_model} with the given nesting level and parameters."
88
+ elsif !new_record.createable_by?(@current_account)
89
+ log "#{requester_name} can't create #{new_record.resource_name}."
90
+ else
91
+ new_record
92
+ end
93
+ end
94
+
95
+ def assign_nestable_resources
96
+ @current_nested_records, @current_nested_resources = [], []
97
+ params.symbolize_keys.dragnet(*nestable_resources.keys).all? {|param_name,column_name|
98
+ constant_name = param_name.to_s.sub(/_id$/, '').camelize
99
+ constant = Object.const_get constant_name rescue nil
100
+
101
+ if constant.nil?
102
+ log "'#{constant_name}' is not available for #{param_name}."
103
+ elsif (record = constant.find_by_id(params[param_name])).nil?
104
+ log "#{constant}<#{params[param_name]}> not found."
105
+ else
106
+ @current_nested_records << record
107
+ @current_nested_resources << record.class
108
+ @record.send "#{nestable_resources[param_name]}=", params[param_name] unless @record.nil?
109
+ # log "Assigning @#{constant.name.underscore} with #{record.inspect}."
110
+ instance_variable_set "@#{constant_name.underscore}", record
111
+ end
112
+ }
113
+ end
114
+
115
+ def current_nested_records
116
+ @current_nested_records.nil? ? [] : @current_nested_records.dup
117
+ end
118
+
119
+ def current_nested_resources
120
+ @current_nested_resources.nil? ? [] : @current_nested_resources.dup
121
+ end
122
+
123
+ def nested_within? record_or_resource
124
+ if record_or_resource.is_a? ActiveRecord::Base
125
+ @current_nested_records.include? record_or_resource
126
+ else
127
+ @current_nested_resources.include? record_or_resource
128
+ end
129
+ end
130
+
131
+ def safe_verb_and_implication?
132
+ request.get? && !action_name.to_s.in?(Hammock::Constants::ImpliedUnsafeActions)
133
+ end
134
+
135
+ def set_editing
136
+ @editing = @record
137
+ end
138
+
139
+ def set_creator_id_if_appropriate
140
+ @record.creator_id = @current_account.id if @record.respond_to?(:creator_id=)
141
+ end
142
+
143
+ def partial_exists? name, extension = nil
144
+ partial_name, ctrler_name = name.split('/', 2).reverse
145
+ !Dir.glob(File.join(
146
+ RAILS_ROOT,
147
+ 'app/views',
148
+ ctrler_name || '',
149
+ "_#{partial_name}.html.#{extension || '*'}"
150
+ )).empty?
151
+ end
152
+
153
+ def redirect_back_or opts = {}, *parameters_for_method_reference
154
+ if request.referer.blank?
155
+ redirect_to opts, *parameters_for_method_reference
156
+ else
157
+ redirect_to request.referer
158
+ end
159
+ end
160
+
161
+ def rendered_or_redirected?
162
+ @performed_render || @performed_redirect
163
+ end
164
+
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,22 @@
1
+ module Hammock
2
+ module RouteDrawingHooks
3
+ MixInto = ActionController::Routing::RouteSet
4
+
5
+ def self.included base # :nodoc:
6
+ base.send :include, Methods
7
+
8
+ base.class_eval {
9
+ alias_method_chain_once :draw, :hammock_route_map_init
10
+ }
11
+ end
12
+
13
+ module Methods
14
+
15
+ def draw_with_hammock_route_map_init &block
16
+ ActionController::Routing::Routes.send :initialize_hammock_route_map
17
+ draw_without_hammock_route_map_init &block
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,58 @@
1
+ module Hammock
2
+ module RouteFor
3
+ def self.included base # :nodoc:
4
+ base.send :include, InstanceMethods
5
+ base.send :extend, ClassMethods
6
+
7
+ base.class_eval {
8
+ helper_method :path_for, :nested_path_for, :route_for, :nested_route_for
9
+ }
10
+ end
11
+
12
+ module ClassMethods
13
+ end
14
+
15
+ module InstanceMethods
16
+ private
17
+
18
+ def path_for *args
19
+ route_for(*args).path
20
+ end
21
+
22
+ def nested_path_for *args
23
+ nested_route_for(*args).path
24
+ end
25
+
26
+ def route_for *args
27
+ opts = args.extract_options!
28
+ verb = args.shift if args.first.is_a?(Symbol)
29
+
30
+ ActionController::Routing::Routes.route_map.for verb_for(verb, args.last), args, opts
31
+ end
32
+
33
+ def verb_for requested_verb, record
34
+ requested_verb = :show if requested_verb.blank?
35
+
36
+ if (:show == requested_verb) && record.is_a?(Class)
37
+ :index
38
+ elsif :modify == requested_verb
39
+ record.new_record? ? :new : :edit
40
+ elsif :save == requested_verb
41
+ record.new_record? ? :create : :update
42
+ else
43
+ requested_verb
44
+ end
45
+ end
46
+
47
+ def nested_route_for *resources
48
+ resources.delete_if &:nil?
49
+ requested_verb = resources.shift if resources.first.is_a?(Symbol)
50
+ args = @current_nested_records.dup.concat(resources)
51
+
52
+ args.unshift(requested_verb) unless requested_verb.nil?
53
+ route_for *args
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,120 @@
1
+ module Hammock
2
+ module Scope
3
+ def self.included base # :nodoc:
4
+ base.send :include, InstanceMethods
5
+ base.send :extend, ClassMethods
6
+
7
+ base.class_eval {
8
+ helper_method :can_verb_entity?
9
+ }
10
+ end
11
+
12
+ module ClassMethods
13
+ end
14
+
15
+ module InstanceMethods
16
+ private
17
+
18
+ def can_verb_entity? verb, entity
19
+ if entity.is_a? ActiveRecord::Base
20
+ can_verb_record? verb, entity
21
+ else
22
+ can_verb_resource? verb, entity
23
+ end == :ok
24
+ end
25
+
26
+ def can_verb_resource? verb, resource
27
+ raise "The verb at #{call_point} must be supplied as a Symbol." unless verb.nil? || verb.is_a?(Symbol)
28
+ route = route_for verb, resource
29
+ if route.safe? && !resource.indexable_by(@current_account)
30
+ log "#{requester_name} can't index #{resource.name.pluralize}. #{describe_call_point 4}"
31
+ :not_found
32
+ elsif !route.safe? && !make_createable(resource)
33
+ log "#{requester_name} can't #{verb} #{resource.name.pluralize}. #{describe_call_point 4}"
34
+ :read_only
35
+ else
36
+ # log "#{requester_name} can #{verb} #{resource.name.pluralize}."
37
+ :ok
38
+ end
39
+ end
40
+
41
+ def can_verb_record? verb, record
42
+ raise "The verb at #{call_point} must be supplied as a Symbol." unless verb.nil? || verb.is_a?(Symbol)
43
+ route = route_for verb, record
44
+ if route.verb.in?(:save, :create) && record.new_record?
45
+ if !record.createable_by?(@current_account)
46
+ log "#{requester_name} can't create a #{record.class} with #{record.attributes.inspect}. #{describe_call_point 4}"
47
+ :unauthed
48
+ else
49
+ :ok
50
+ end
51
+ else
52
+ if !record.readable_by?(@current_account)
53
+ log "#{requester_name} can't see #{record.class}<#{record.id}>. #{describe_call_point 4}"
54
+ :not_found
55
+ elsif !route.safe? && !record.writeable_by?(@current_account)
56
+ log "#{requester_name} can't #{verb} #{record.class}<#{record.id}>. #{describe_call_point 4}"
57
+ :read_only
58
+ else
59
+ # log "#{requester_name} can #{verb} #{record.class}<#{record.id}>."
60
+ :ok
61
+ end
62
+ end
63
+ end
64
+
65
+ def verb_scope
66
+ if @current_account && (scope_name = account_verb_scope?)
67
+ # log "got an account_verb_scope #{scope_name}."
68
+ mdl.send scope_name, @current_account
69
+ elsif !(scope_name = public_verb_scope?)
70
+ log "No #{@current_account.nil? ? 'public' : 'account'} #{scope_name_for_action} scope available for #{mdl}.#{' May be available after login.' if account_verb_scope?}"
71
+ nil
72
+ else
73
+ # log "got a #{scope_name} public_verb_scope."
74
+ mdl.send scope_name
75
+ end
76
+ end
77
+
78
+ def nest_scope
79
+ params.symbolize_keys.dragnet(*nestable_resources.keys).inject(mdl.ambition_context) {|acc,(k,v)|
80
+ # TODO this would be more ductile if it used AR assocs instead of explicit FK
81
+ eval "acc.select {|r| r.#{nestable_resources[k]} == v }"
82
+ }
83
+ end
84
+
85
+ def current_scope
86
+ if (resultant_scope = nest_scope.chain(verb_scope)).nil?
87
+ nil
88
+ else
89
+ resultant_scope = resultant_scope.chain(custom_scope) unless custom_scope.nil?
90
+ resultant_scope.sort_by &mdl.sorter
91
+ end
92
+ end
93
+
94
+
95
+ private
96
+
97
+ def scope_name_for_action
98
+ if 'index' == action_name
99
+ 'index'
100
+ elsif safe_verb_and_implication?
101
+ 'read'
102
+ else
103
+ 'write'
104
+ end
105
+ end
106
+
107
+ def requester_name
108
+ @current_account.nil? ? 'Anonymous' : "#{@current_account.class}<#{@current_account.id}>"
109
+ end
110
+
111
+ def account_verb_scope?
112
+ mdl.has_account_scope? scope_name_for_action
113
+ end
114
+ def public_verb_scope?
115
+ mdl.has_public_scope? scope_name_for_action
116
+ end
117
+
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,36 @@
1
+ module Hammock
2
+ module Suggest
3
+ MixInto = ActiveRecord::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
+
12
+ def suggest given_fields, queries, limit = 15
13
+ if (fields = given_fields & columns.map(&:name)).length != given_fields.length
14
+ log "Invalid columns #{(given_fields - fields).inspect}."
15
+ else
16
+ find(:all,
17
+ :limit => limit,
18
+ :order => fields.map {|f| "#{f} ASC" }.join(', '),
19
+ :conditions => [
20
+ fields.map {|f|
21
+ ([ "LOWER(#{table_name}.#{f}) LIKE ?" ] * queries.length).join(' AND ')
22
+ }.map {|clause|
23
+ "(#{clause})"
24
+ }.join(' OR ')
25
+ ].concat(queries.map{|q| "%#{q}%" } * fields.length)
26
+ )
27
+ end
28
+ end
29
+
30
+ end
31
+
32
+ module InstanceMethods
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,42 @@
1
+ module Hammock
2
+ module Utils
3
+ def self.included base # :nodoc:
4
+ base.send :include, Methods
5
+ base.send :extend, Methods
6
+ end
7
+
8
+ module Methods
9
+ private
10
+
11
+ require 'pathname'
12
+ def rails_root
13
+ @hammock_cached_rails_root ||= Pathname(RAILS_ROOT).realpath.to_s
14
+ end
15
+
16
+ def rails_env
17
+ ENV['RAILS_ENV'] || 'development'
18
+ end
19
+
20
+ def development?
21
+ 'development' == rails_env
22
+ end
23
+
24
+ def production?
25
+ 'production' == rails_env
26
+ end
27
+
28
+ def sqlite?
29
+ 'SQLite' == connection.adapter_name
30
+ end
31
+
32
+ def describe_call_point offset = 0
33
+ "(called from #{call_point offset + 1})"
34
+ end
35
+
36
+ def call_point offset = 0
37
+ caller[offset + 1].strip.gsub(rails_root, '').gsub(/\:in\ .*$/, '')
38
+ end
39
+
40
+ end
41
+ end
42
+ end