hobo 0.8.3 → 0.8.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 (80) hide show
  1. data/CHANGES.txt +330 -0
  2. data/Manifest +12 -4
  3. data/Rakefile +4 -6
  4. data/dryml_generators/rapid/cards.dryml.erb +5 -1
  5. data/dryml_generators/rapid/forms.dryml.erb +8 -10
  6. data/dryml_generators/rapid/pages.dryml.erb +65 -36
  7. data/hobo.gemspec +28 -15
  8. data/lib/active_record/association_collection.rb +3 -22
  9. data/lib/hobo.rb +25 -258
  10. data/lib/hobo/accessible_associations.rb +131 -0
  11. data/lib/hobo/authentication_support.rb +15 -9
  12. data/lib/hobo/composite_model.rb +1 -1
  13. data/lib/hobo/controller.rb +7 -8
  14. data/lib/hobo/dryml.rb +9 -10
  15. data/lib/hobo/dryml/dryml_builder.rb +7 -1
  16. data/lib/hobo/dryml/dryml_doc.rb +161 -0
  17. data/lib/hobo/dryml/dryml_generator.rb +18 -9
  18. data/lib/hobo/dryml/part_context.rb +76 -42
  19. data/lib/hobo/dryml/tag_parameters.rb +1 -0
  20. data/lib/hobo/dryml/taglib.rb +2 -1
  21. data/lib/hobo/dryml/template.rb +39 -29
  22. data/lib/hobo/dryml/template_environment.rb +79 -37
  23. data/lib/hobo/dryml/template_handler.rb +66 -21
  24. data/lib/hobo/guest.rb +2 -10
  25. data/lib/hobo/hobo_helper.rb +125 -53
  26. data/lib/hobo/include_in_save.rb +0 -1
  27. data/lib/hobo/lifecycles.rb +54 -24
  28. data/lib/hobo/lifecycles/actions.rb +95 -31
  29. data/lib/hobo/lifecycles/creator.rb +18 -23
  30. data/lib/hobo/lifecycles/lifecycle.rb +86 -62
  31. data/lib/hobo/lifecycles/state.rb +1 -2
  32. data/lib/hobo/lifecycles/transition.rb +22 -28
  33. data/lib/hobo/model.rb +64 -176
  34. data/lib/hobo/model_controller.rb +67 -54
  35. data/lib/hobo/model_router.rb +5 -2
  36. data/lib/hobo/permissions.rb +397 -0
  37. data/lib/hobo/permissions/associations.rb +167 -0
  38. data/lib/hobo/scopes.rb +15 -38
  39. data/lib/hobo/scopes/association_proxy_extensions.rb +15 -5
  40. data/lib/hobo/scopes/automatic_scopes.rb +43 -18
  41. data/lib/hobo/scopes/named_scope_extensions.rb +2 -2
  42. data/lib/hobo/user.rb +10 -4
  43. data/lib/hobo/user_controller.rb +6 -5
  44. data/lib/hobo/view_hints.rb +58 -0
  45. data/rails_generators/hobo/hobo_generator.rb +7 -3
  46. data/rails_generators/hobo/templates/guest.rb +1 -13
  47. data/rails_generators/hobo_front_controller/hobo_front_controller_generator.rb +1 -1
  48. data/rails_generators/hobo_model/hobo_model_generator.rb +4 -2
  49. data/rails_generators/hobo_model/templates/hints.rb +4 -0
  50. data/rails_generators/hobo_model/templates/model.rb +8 -8
  51. data/rails_generators/hobo_model_controller/hobo_model_controller_generator.rb +10 -0
  52. data/rails_generators/hobo_model_controller/templates/controller.rb +1 -1
  53. data/rails_generators/hobo_rapid/templates/hobo-rapid.js +91 -56
  54. data/rails_generators/hobo_rapid/templates/lowpro.js +15 -15
  55. data/rails_generators/hobo_rapid/templates/reset.css +36 -3
  56. data/rails_generators/hobo_rapid/templates/themes/clean/public/stylesheets/clean.css +13 -17
  57. data/rails_generators/hobo_user_controller/templates/controller.rb +1 -1
  58. data/rails_generators/hobo_user_model/templates/model.rb +18 -16
  59. data/taglibs/core.dryml +60 -18
  60. data/taglibs/rapid.dryml +8 -401
  61. data/taglibs/rapid_core.dryml +586 -0
  62. data/taglibs/rapid_document_tags.dryml +28 -10
  63. data/taglibs/rapid_editing.dryml +92 -55
  64. data/taglibs/rapid_forms.dryml +406 -87
  65. data/taglibs/rapid_generics.dryml +1 -1
  66. data/taglibs/rapid_navigation.dryml +2 -1
  67. data/taglibs/rapid_pages.dryml +7 -16
  68. data/taglibs/rapid_plus.dryml +39 -14
  69. data/taglibs/rapid_support.dryml +1 -1
  70. data/taglibs/rapid_user_pages.dryml +14 -4
  71. data/tasks/{generate_tag_reference.rb → generate_tag_reference.rake} +49 -18
  72. data/tasks/hobo_tasks.rake +16 -0
  73. data/test/permissions/models/models.rb +134 -0
  74. data/test/permissions/models/schema.rb +55 -0
  75. data/test/permissions/models/test.sqlite3 +0 -0
  76. data/test/permissions/test_permissions.rb +436 -0
  77. metadata +27 -14
  78. data/lib/hobo/mass_assignment.rb +0 -64
  79. data/rails_generators/hobo/templates/patch_routing.rb +0 -30
  80. data/uninstall.rb +0 -1
@@ -7,26 +7,18 @@ rescue MissingSourceFile
7
7
  # OK, Hobo won't do pagination then
8
8
  end
9
9
 
10
- (defined?(ActiveSupport::Dependencies) ? ActiveSupport::Dependencies : Dependencies).load_paths |= [ File.dirname(__FILE__) ]
10
+ ActiveSupport::Dependencies.load_paths |= [ File.dirname(__FILE__)]
11
11
 
12
12
  # Hobo can be installed in /vendor/hobo, /vendor/plugins/hobo, vendor/plugins/hobo/hobo, etc.
13
13
  ::HOBO_ROOT = File.expand_path(File.dirname(__FILE__) + "/..")
14
14
 
15
-
16
- # Modules that must *not* be auto-reloaded by activesupport
17
- # (explicitly requiring them means they're never unloaded)
18
- require 'hobo/model_router'
19
- require 'hobo/undefined'
20
- require 'hobo/user'
21
- require 'hobo/dryml'
22
- require 'hobo/dryml/template'
23
- require 'hobo/dryml/dryml_generator'
24
-
25
15
  class HoboError < RuntimeError; end
26
16
 
27
17
  module Hobo
28
18
 
29
- VERSION = "0.8.3"
19
+ VERSION = "0.8.4"
20
+
21
+ class PermissionDeniedError < RuntimeError; end
30
22
 
31
23
  class RawJs < String; end
32
24
 
@@ -49,43 +41,8 @@ module Hobo
49
41
  end
50
42
 
51
43
 
52
- def user_model=(model)
53
- @user_model = model && model.name
54
- end
55
-
56
-
57
- def user_model
58
- @user_model && @user_model.constantize
59
- end
60
-
61
-
62
- def object_from_dom_id(dom_id)
63
- return nil if dom_id == 'nil'
64
-
65
- _, name, id, attr = *dom_id.match(/^([a-z_]+)(?:_([0-9]+(?:_[0-9]+)*))?(?:_([a-z_]+))?$/)
66
- raise ArgumentError.new("invalid model-reference in dom id") unless name
67
- if name
68
- model_class = name.camelize.constantize rescue (raise ArgumentError.new("no such class in dom-id"))
69
- return nil unless model_class
70
-
71
- if id
72
- obj = if false and attr and model_class.reflections[attr.to_sym].klass.superclass == ActiveRecord::Base
73
- # DISABLED - Eager loading is broken - doesn't support ordering
74
- # http://dev.rubyonrails.org/ticket/3438
75
- # Don't do this for STI subclasses - it breaks!
76
- model_class.find(id, :include => attr)
77
- else
78
- model_class.find(id)
79
- end
80
- attr ? obj.send(attr) : obj
81
- else
82
- model_class
83
- end
84
- end
85
- end
86
-
87
- def dom_id(obj, attr=nil)
88
- attr ? "#{obj.typed_id}_#{attr}" : obj.typed_id
44
+ def typed_id(obj, attr=nil)
45
+ attr ? "#{obj.typed_id}:#{attr}" : obj.typed_id
89
46
  end
90
47
 
91
48
  def find_by_search(query, search_targets=nil)
@@ -130,16 +87,6 @@ module Hobo
130
87
  end
131
88
 
132
89
 
133
- def can_create_in_association?(array_or_reflection)
134
- refl =
135
- (array_or_reflection.is_a?(ActiveRecord::Reflection::AssociationReflection) and array_or_reflection) or
136
- array_or_reflection.try.proxy_reflection or
137
- (origin = array_or_reflection.try.origin and origin.send(array_or_reflection.origin_attribute).try.proxy_reflection)
138
-
139
- refl && refl.macro == :has_many && (!refl.through_reflection) && (!refl.options[:conditions])
140
- end
141
-
142
-
143
90
  def get_field(object, field)
144
91
  return nil if object.nil?
145
92
  field_str = field.to_s
@@ -168,147 +115,6 @@ module Hobo
168
115
  end
169
116
 
170
117
 
171
- # --- Permissions --- #
172
-
173
-
174
- def can_create?(person, object)
175
- if object.is_a?(Class) and object < ActiveRecord::Base
176
- object = object.new
177
- object.set_creator(person)
178
- elsif (refl = object.try.proxy_reflection) && refl.macro == :has_many
179
- if Hobo.simple_has_many_association?(object)
180
- object = object.new
181
- object.set_creator(person)
182
- else
183
- return false
184
- end
185
- end
186
- check_permission(:create, person, object)
187
- end
188
-
189
-
190
- def can_update?(person, object, new)
191
- check_permission(:update, person, object, new)
192
- end
193
-
194
-
195
- def can_edit?(person, object, field)
196
- return true if object.try.exempt_from_edit_checks?
197
- # Can't view implies can't edit
198
- return false if !can_view?(person, object, field)
199
-
200
- if field.nil?
201
- if object.has_hobo_method?(:editable_by?)
202
- object.editable_by?(person)
203
- elsif object.has_hobo_method?(:updatable_by?)
204
- object.updatable_by?(person, nil)
205
- else
206
- false
207
- end
208
-
209
- else
210
- attribute_name = field.to_s.sub /\?$/, ''
211
- return false if !object.respond_to?("#{attribute_name}=")
212
-
213
- refl = object.class.reflections[field.to_sym] if object.is_a?(ActiveRecord::Base)
214
-
215
- # has_one and polymorphic associations are not editable (for now)
216
- return false if refl and (refl.options[:polymorphic] or refl.macro == :has_one)
217
-
218
- if object.has_hobo_method?("#{field}_editable_by?")
219
- object.send("#{field}_editable_by?", person)
220
- elsif object.has_hobo_method?(:editable_by?)
221
- check_permission(:edit, person, object)
222
- elsif refl._?.macro == :has_many
223
- # The below technique to figure out edit permission based on
224
- # update permission doesn't work for has_many associations
225
- false
226
- else
227
- # Fake an edit test by setting the field in question to
228
- # Hobo::Undefined and then testing for update/create permission
229
- current = object.send(field)
230
- new = object.duplicate
231
-
232
- edit_test_value = if current == true
233
- false
234
- elsif current == false || (current.nil? && object.class.try.attr_type(field) == Hobo::Boolean)
235
- true
236
- elsif refl and refl.macro == :belongs_to
237
- Hobo::Undefined.new(refl.klass)
238
- else
239
- Hobo::Undefined.new
240
- end
241
-
242
- new.metaclass.send(:define_method, attribute_name) { edit_test_value }
243
-
244
- begin
245
- if object.new_record?
246
- check_permission(:create, person, new)
247
- else
248
- check_permission(:update, person, object, new)
249
- end
250
- rescue Hobo::UndefinedAccessError
251
- # The permission method performed some test on the undefined value
252
- # so this field is not editable
253
- false
254
- end
255
- end
256
- end
257
- end
258
-
259
-
260
- def can_delete?(person, object)
261
- check_permission(:delete, person, object)
262
- end
263
-
264
-
265
- # can_view? has special behaviour if it's passed a class or an
266
- # association-proxy -- it instantiates the class, or creates a new
267
- # instance "in" the association, and tests the permission of this
268
- # object. This means the permission methods in models can't rely
269
- # on the instance being properly initialised. But it's important
270
- # that it works like this because, in the case of an association
271
- # proxy, we don't want to loose the information that the object
272
- # belongs_to the proxy owner.
273
- def can_view?(person, object, field=nil)
274
- # Field can be a dot separated path
275
- if field && field.is_a?(String) && (path = field.split(".")).length > 1
276
- _, _, object = get_field_path(object, path[0..-2])
277
- field = path.last
278
- end
279
-
280
- if field
281
- field = field.to_sym if field.is_a? String
282
- return false if object.class.respond_to?(:never_show?) && object.class.never_show?(field)
283
- else
284
- # Special support for classes (can view instances?)
285
- if object.is_a?(Class) and object < Hobo::Model
286
- object = object.new
287
- elsif Hobo.simple_has_many_association?(object)
288
- object = object.new
289
- end
290
- end
291
- viewable = check_permission(:view, person, object, field)
292
- if viewable and field and
293
- ( (field_val = get_field(object, field)).is_a?(Hobo::Model) or field_val.is_a?(Array) )
294
- # also ask the current value if it is viewable
295
- can_view?(person, field_val)
296
- else
297
- viewable
298
- end
299
- end
300
-
301
-
302
- def can_call?(person, object, method)
303
- return true if person.has_hobo_method?(:super_user?) && person.super_user?
304
-
305
- m = "#{method}_callable_by?"
306
- object.has_hobo_method?(m) && object.send(m, person)
307
- end
308
-
309
- # --- end permissions -- #
310
-
311
-
312
118
  def static_tags
313
119
  @static_tags ||= begin
314
120
  path = if FileTest.exists?("#{RAILS_ROOT}/config/dryml_static_tags.txt")
@@ -328,58 +134,36 @@ module Hobo
328
134
  @subsites ||= Dir["#{RAILS_ROOT}/app/controllers/*"].map { |f| File.basename(f) if File.directory?(f) }.compact
329
135
  end
330
136
 
331
-
332
- private
333
-
334
-
335
- def check_permission(permission, person, object, *args)
336
- return true if person.has_hobo_method?(:super_user?) and person.super_user?
337
-
338
- return true if permission == :view && !(object.is_a?(ActiveRecord::Base) || object.is_a?(Hobo::CompositeModel))
339
-
340
- obj_method = case permission
341
- when :create; :creatable_by?
342
- when :update; :updatable_by?
343
- when :delete; :deletable_by?
344
- when :edit; :editable_by?
345
- when :view; :viewable_by?
346
- end
347
- p = if (obj_method.respond_to?(:has_hobo_method) ? object.has_hobo_method?(obj_method) : object.respond_to?(obj_method))
348
- begin
349
- object.send(obj_method, person, *args)
350
- rescue Hobo::UndefinedAccessError
351
- false
352
- end
353
- elsif object.class.respond_to?(obj_method)
354
- object.class.send(obj_method, person, *args)
355
- elsif !object.is_a?(Class) # No user fallback for class-level permissions
356
- person_method = "can_#{permission}?".to_sym
357
- if person.has_hobo_method?(person_method)
358
- person.send(person_method, object, *args)
359
- else
360
- # The object does not define permissions - you can only view it
361
- permission == :view
362
- end
363
- end
364
- end
365
-
366
137
  public
367
138
 
368
139
  def enable
369
- # Rails monkey patches
370
- require 'active_record/association_collection'
371
- require 'active_record/association_proxy'
372
- require 'active_record/association_reflection'
373
140
  require 'action_view_extensions/helpers/tag_helper'
374
141
 
142
+ # Modules that must *not* be auto-reloaded by activesupport
143
+ # (explicitly requiring them means they're never unloaded)
144
+ require 'hobo/model_router'
145
+ require 'hobo/undefined'
146
+ require 'hobo/user'
147
+ require 'hobo/dryml'
148
+ require 'hobo/dryml/template'
149
+ require 'hobo/dryml/dryml_generator'
150
+
151
+ Hobo::Model.enable
375
152
  Hobo::Dryml.enable
153
+ Hobo::Permissions.enable
154
+ Hobo::ViewHints.enable
376
155
 
377
156
  Hobo.developer_features = RAILS_ENV.in?(["development", "test"]) if Hobo.developer_features?.nil?
378
157
 
379
158
  require 'hobo/dev_controller' if RAILS_ENV == Hobo.developer_features?
380
159
 
381
160
  ActionController::Base.send(:include, Hobo::ControllerExtensions)
382
- ActiveRecord::Base.send(:include, Hobo::ModelExtensions)
161
+
162
+ if defined? HoboFields
163
+ HoboFields.never_wrap(Hobo::Undefined)
164
+ end
165
+
166
+ ActiveSupport::Dependencies.load_paths |= [ "#{RAILS_ROOT}/app/viewhints" ]
383
167
  end
384
168
 
385
169
  end
@@ -402,18 +186,6 @@ module Hobo
402
186
 
403
187
  end
404
188
 
405
-
406
- ModelExtensions = classy_module do
407
- def self.hobo_model
408
- include Hobo::Model
409
- end
410
- def self.hobo_user_model
411
- include Hobo::Model
412
- include Hobo::User
413
- end
414
- end
415
-
416
-
417
189
  # Empty class to represent the boolean type.
418
190
  class Boolean; end
419
191
 
@@ -421,11 +193,6 @@ module Hobo
421
193
  end
422
194
 
423
195
 
424
- if defined? HoboFields
425
- HoboFields.never_wrap(Hobo::Undefined)
426
- end
427
-
428
-
429
196
  # Add support for type metadata to arrays
430
197
  class ::Array
431
198
 
@@ -437,7 +204,7 @@ class ::Array
437
204
  end
438
205
 
439
206
  def typed_id
440
- origin and origin_id = origin.try.typed_id and "#{origin_id}_#{origin_attribute}"
207
+ origin and origin_id = origin.try.typed_id and "#{origin_id}:#{origin_attribute}"
441
208
  end
442
209
 
443
210
  end
@@ -0,0 +1,131 @@
1
+ module Hobo
2
+
3
+ module AccessibleAssociations
4
+
5
+ extend self
6
+
7
+ def prepare_has_many_assignment(association, association_name, array_or_hash)
8
+ owner = association.proxy_owner
9
+
10
+ array = params_hash_to_array(array_or_hash)
11
+ array.map! do |record_hash_or_string|
12
+ find_or_create_and_update(owner, association, association_name, record_hash_or_string) { association.build }
13
+ end
14
+ array.compact
15
+ end
16
+
17
+
18
+ def find_or_create_and_update(owner, association, association_name, record_hash_or_string)
19
+ if record_hash_or_string.is_a?(String)
20
+ # An ID (if it starts '@') or else a name
21
+ record = find_record(association, record_hash_or_string)
22
+
23
+ elsif record_hash_or_string.is_a?(Hash)
24
+ # A hash of attributes
25
+ hash = record_hash_or_string
26
+
27
+ # Remove completely blank hashes
28
+ return nil if hash.values.join.blank?
29
+
30
+ id = hash.delete(:id)
31
+
32
+ record = if id
33
+ association.find(id) # TODO: We don't really want to find these one by one
34
+ else
35
+ record = yield
36
+ end
37
+ record.attributes = hash
38
+ owner.include_in_save(association_name, record) unless owner.new_record? && record.new_record?
39
+
40
+ else
41
+ # It's already a record
42
+ record = record_hash_or_string
43
+ end
44
+ record
45
+ end
46
+
47
+
48
+ def params_hash_to_array(array_or_hash)
49
+ if array_or_hash.is_a?(Hash)
50
+ array = array_or_hash.get(*array_or_hash.keys.sort_by(&:to_i))
51
+ else
52
+ array_or_hash
53
+ end
54
+ end
55
+
56
+
57
+ def find_record(association, id_or_name)
58
+ klass = association.member_class
59
+ if id_or_name =~ /^@(.*)/
60
+ id = $1
61
+ if id =~ /:/
62
+ Hobo::Model.find_by_typed_id(id)
63
+ else
64
+ klass.find(id)
65
+ end
66
+ else
67
+ klass.named(id_or_name, :conditions => association.conditions)
68
+ end
69
+ end
70
+
71
+ end
72
+
73
+ classy_module(AccessibleAssociations) do
74
+
75
+ include IncludeInSave
76
+
77
+ # --- has_many mass assignment support --- #
78
+
79
+ def self.has_many_with_accessible(name, options={}, &block)
80
+ has_many_without_accessible(name, options, &block)
81
+
82
+ if options[:accessible]
83
+ class_eval %{
84
+ def #{name}_with_accessible=(array_or_hash)
85
+ items = Hobo::AccessibleAssociations.prepare_has_many_assignment(#{name}, :#{name}, array_or_hash)
86
+ self.#{name}_without_accessible = items
87
+ # ensure the loaded array contains any changed records
88
+ self.#{name}.proxy_target[0..-1] = items
89
+ end
90
+ }, __FILE__, __LINE__ - 7
91
+ alias_method_chain :"#{name}=", :accessible
92
+ end
93
+ end
94
+ metaclass.alias_method_chain :has_many, :accessible
95
+
96
+
97
+
98
+ # --- belongs_to assignment support --- #
99
+
100
+ def self.belongs_to_with_accessible(name, options={}, &block)
101
+ belongs_to_without_accessible(name, options, &block)
102
+
103
+ if options[:accessible]
104
+ class_eval %{
105
+ def #{name}_with_accessible=(record_hash_or_string)
106
+ record = Hobo::AccessibleAssociations.find_or_create_and_update(self, #{name}, :#{name}, record_hash_or_string) { self.class.reflections[:#{name}].klass.new }
107
+ self.#{name}_without_accessible = record
108
+ end
109
+ }, __FILE__, __LINE__ - 5
110
+ alias_method_chain :"#{name}=", :accessible
111
+ end
112
+ end
113
+ metaclass.alias_method_chain :belongs_to, :accessible
114
+
115
+
116
+ # Add :accessible to the valid keys so AR doesn't complain
117
+
118
+ def self.valid_keys_for_has_many_association_with_accessible
119
+ valid_keys_for_has_many_association_without_accessible + [:accessible]
120
+ end
121
+ metaclass.alias_method_chain :valid_keys_for_has_many_association, :accessible
122
+
123
+ def self.valid_keys_for_belongs_to_association_with_accessible
124
+ valid_keys_for_belongs_to_association_without_accessible + [:accessible]
125
+ end
126
+ metaclass.alias_method_chain :valid_keys_for_belongs_to_association, :accessible
127
+
128
+
129
+ end
130
+
131
+ end