hobo 0.8.3 → 0.8.4

Sign up to get free protection for your applications and to get access to all the features.
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