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