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
@@ -5,7 +5,7 @@ module Hobo
5
5
  alias_method :has_hobo_method?, :respond_to?
6
6
 
7
7
  def to_s
8
- "Guest"
8
+ "guest"
9
9
  end
10
10
 
11
11
  def guest?
@@ -16,16 +16,8 @@ module Hobo
16
16
  false
17
17
  end
18
18
 
19
- def super_user?
20
- false
21
- end
22
-
23
- def administrator?
24
- false
25
- end
26
-
27
19
  def login
28
- "Guest"
20
+ "guest"
29
21
  end
30
22
 
31
23
  end
@@ -20,7 +20,7 @@ module Hobo
20
20
  # simple one-hit-per-request cache
21
21
  @current_user ||= begin
22
22
  id = session._?[:user]
23
- (id && Hobo.object_from_dom_id(id) rescue nil) || ::Guest.new
23
+ (id && Hobo::Model.find_by_typed_id(id) rescue nil) || ::Guest.new
24
24
  end
25
25
  end
26
26
 
@@ -31,7 +31,7 @@ module Hobo
31
31
 
32
32
 
33
33
  def base_url
34
- request.relative_url_root
34
+ ActionController::Base.relative_url_root || ""
35
35
  end
36
36
 
37
37
 
@@ -58,7 +58,7 @@ module Hobo
58
58
  options[:subsite] ||= self.subsite
59
59
  subsite, method = options.get :subsite, :method
60
60
 
61
- if obj.respond_to?(:member_class)
61
+ if obj.respond_to?(:member_class) && obj.respond_to?(:origin)
62
62
  # Asking for URL of a collection, e.g. category/1/adverts or category/1/adverts/new
63
63
 
64
64
  refl = obj.origin.class.reverse_reflection(obj.origin_attribute)
@@ -90,7 +90,13 @@ module Hobo
90
90
  obj = obj.class
91
91
  end
92
92
 
93
- klass = obj.is_a?(Class) ? obj : obj.class
93
+ klass = if obj.is_a?(Class)
94
+ obj
95
+ elsif obj.respond_to?(:member_class)
96
+ obj.member_class # We get here if we're passed a scoped class
97
+ else
98
+ obj.class
99
+ end
94
100
  end
95
101
 
96
102
  if Hobo::ModelRouter.linkable?(klass, action, options)
@@ -119,7 +125,7 @@ module Hobo
119
125
  "#{name}=' + #{obj} + '"
120
126
  else
121
127
  v = if obj.is_a?(ActiveRecord::Base) or obj.is_a?(Array)
122
- "@" + dom_id(obj)
128
+ "@" + typed_id(obj)
123
129
  else
124
130
  obj.to_s.gsub("'"){"\\'"}
125
131
  end
@@ -148,7 +154,7 @@ module Hobo
148
154
 
149
155
 
150
156
  def model_id_class(object=this, attribute=nil)
151
- object.respond_to?(:typed_id) ? "model:#{dom_id(object, attribute).dasherize}" : ""
157
+ object.respond_to?(:typed_id) ? "model::#{typed_id(object, attribute).to_s.dasherize}" : ""
152
158
  end
153
159
 
154
160
 
@@ -156,12 +162,13 @@ module Hobo
156
162
  # TODO: Calls to respond_to? in here can cause the full collection hiding behind a scoped collection to get loaded
157
163
  res = []
158
164
  empty = true
159
- scope.new_scope(:repeat_collection => enum) do
165
+ scope.new_scope(:repeat_collection => enum, :even_odd => 'odd') do
160
166
  if enum.respond_to?(:each_pair)
161
167
  enum.each_pair do |key, value|
162
168
  empty = false;
163
169
  self.this_key = key;
164
170
  new_object_context(value) { res << yield }
171
+ scope.even_odd = scope.even_odd == "even" ? "odd" : "even"
165
172
  end
166
173
  else
167
174
  index = 0
@@ -172,6 +179,7 @@ module Hobo
172
179
  else
173
180
  new_object_context(e) { res << yield }
174
181
  end
182
+ scope.even_odd = scope.even_odd == "even" ? "odd" : "even"
175
183
  index += 1
176
184
  end
177
185
  end
@@ -195,8 +203,6 @@ module Hobo
195
203
  case x
196
204
  when nil
197
205
  []
198
- when Symbol
199
- x.to_s
200
206
  when String
201
207
  x.strip.split(/\s*,\s*/)
202
208
  else
@@ -206,58 +212,113 @@ module Hobo
206
212
 
207
213
 
208
214
  def can_create?(object=this)
209
- Hobo.can_create?(current_user, object)
215
+ if object.is_a?(Class) and object < ActiveRecord::Base
216
+ object = object.new
217
+ elsif (refl = object.try.proxy_reflection) && refl.macro == :has_many
218
+ if Hobo.simple_has_many_association?(object)
219
+ object = object.new
220
+ object.set_creator(current_user)
221
+ else
222
+ return false
223
+ end
224
+ end
225
+ object.creatable_by?(current_user)
210
226
  end
211
227
 
212
228
 
213
- def can_update?(object, new)
214
- Hobo.can_update?(current_user, object, new)
229
+ def can_update?(object=this)
230
+ object.updatable_by?(current_user)
215
231
  end
216
232
 
217
233
 
218
234
  def can_edit?(*args)
219
- if args.empty?
220
- if this.respond_to?(:updatable_by?) && !this_field_reflection
221
- can_edit?(this, nil)
222
- elsif this_parent && this_field
223
- can_edit?(this_parent, this_field)
224
- else
225
- can_edit?(this, nil)
226
- end
227
- else
228
- object, field = args.length == 2 ? args : [this, args.first]
229
-
230
- if !field && (origin = object.try.origin)
231
- Hobo.can_edit?(current_user, origin, object.origin_attribute)
232
- else
233
- Hobo.can_edit?(current_user, object, field)
234
- end
235
+ object, field = if args.empty?
236
+ if this.respond_to?(:editable_by?) && !this_field_reflection
237
+ [this, nil]
238
+ elsif this_parent && this_field
239
+ [this_parent, this_field]
240
+ else
241
+ [this, nil]
242
+ end
243
+ elsif args.length == 2
244
+ args
245
+ else
246
+ [this, args.first]
247
+ end
248
+
249
+ if !field && (origin = object.try.origin)
250
+ object, field = origin, object.origin_attribute
235
251
  end
236
- end
237
252
 
253
+ object.editable_by?(current_user, field)
254
+ end
255
+
238
256
 
239
- def can_delete?(object=nil)
240
- Hobo.can_delete?(current_user, object || this)
257
+ def can_delete?(object=this)
258
+ object.destroyable_by?(current_user)
241
259
  end
242
260
 
243
261
 
244
- def can_view?(object=nil, field=nil)
245
- if object.nil? && field.nil?
262
+
263
+ def can_call?(*args)
264
+ method = args.last
265
+ object = args.length == 2 ? args.first : this
266
+
267
+ object.method_callable_by?(current_user, method)
268
+ end
269
+
270
+
271
+ # can_view? has special behaviour if it's passed a class or an
272
+ # association-proxy -- it instantiates the class, or creates a new
273
+ # instance "in" the association, and tests the permission of this
274
+ # object. This means the permission methods in models can't rely
275
+ # on the instance being properly initialised. But it's important
276
+ # that it works like this because, in the case of an association
277
+ # proxy, we don't want to loose the information that the object
278
+ # belongs_to the proxy owner.
279
+ def can_view?(*args)
280
+ # TODO: Man does this need a big cleanup!
281
+
282
+ if args.empty?
246
283
  if this_parent && this_field
247
- object, field = this_parent, this_field
284
+ object = this_parent
285
+ field = this_field
248
286
  else
249
287
  object = this
250
288
  end
289
+ elsif args.first.is_a?(String, Symbol)
290
+ object = this
291
+ field = args.first
292
+ else
293
+ object, field = args
251
294
  end
252
-
295
+
296
+ if field
297
+ # Field can be a dot separated path
298
+ if field.is_a?(String) && (path = field.split(".")).length > 1
299
+ _, _, object = Hobo.get_field_path(object, path[0..-2])
300
+ field = path.last
301
+ end
302
+ elsif (origin = object.try.origin)
303
+ object, field = origin, object.origin_attribute
304
+ end
305
+
253
306
  @can_view_cache ||= {}
254
307
  @can_view_cache[ [object, field] ] ||=
255
- if !field && (origin = object.try.origin)
256
- Hobo.can_view?(current_user, origin, object.origin_attribute)
308
+ if !object.respond_to?(:viewable_by)
309
+ true
310
+ elsif object.viewable_by?(current_user, field)
311
+ # If possible, we also check if the current *value* of the field is viewable
312
+ if field.is_a?(Symbol, String) && (v = object.send(field)) && v.respond_to?(:viewable_by?)
313
+ v.viewable_by?(current_user, nil)
314
+ else
315
+ true
316
+ end
257
317
  else
258
- Hobo.can_view?(current_user, object, field)
318
+ false
259
319
  end
260
320
  end
321
+
261
322
 
262
323
 
263
324
  def select_viewable(collection=this)
@@ -289,19 +350,19 @@ module Hobo
289
350
  end
290
351
 
291
352
 
292
- def param_name_for(object, field_path)
353
+ def param_name_for(path)
293
354
  field_path = field_path.to_s.split(".") if field_path.is_a?(String, Symbol)
294
- attrs = field_path.map{|part| "[#{part.to_s.sub /\?$/, ''}]"}.join
295
- "#{object.class.name.underscore}#{attrs}"
355
+ attrs = path.rest.map{|part| "[#{part.to_s.sub /\?$/, ''}]"}.join
356
+ "#{path.first}#{attrs}"
296
357
  end
297
358
 
298
359
 
299
360
  def param_name_for_this(foreign_key=false)
300
361
  return "" unless form_this
301
362
  name = if foreign_key && (refl = this_field_reflection) && refl.macro == :belongs_to
302
- param_name_for(form_this, form_field_path[0..-2] + [refl.primary_key_name])
363
+ param_name_for(path_for_form_field[0..-2] + [refl.primary_key_name])
303
364
  else
304
- param_name_for(form_this, form_field_path)
365
+ param_name_for(path_for_form_field)
305
366
  end
306
367
  register_form_field(name)
307
368
  name
@@ -366,17 +427,16 @@ module Hobo
366
427
  end
367
428
 
368
429
  def query_params
369
- query = request.request_uri.match(/(?:\?([^?]+))/)._?[1]
430
+ query = request.request_uri =~ /\?([^?]+)/ && $1
431
+ result = HashWithIndifferentAccess.new
370
432
  if query
371
- params = query.split('&')
372
- pairs = params.map do |param|
373
- pair = param.split('=', 2)
374
- pair.length == 1 ? pair + [''] : pair
433
+ query.gsub!('+', ' ')
434
+ query.split('&').each do |param|
435
+ name, val = param.split('=', 2)
436
+ result[URI.unescape(name)] = val ? URI.unescape(val) : ''
375
437
  end
376
- HashWithIndifferentAccess[*pairs.flatten]
377
- else
378
- HashWithIndifferentAccess.new
379
438
  end
439
+ result
380
440
  end
381
441
 
382
442
  def linkable?(*args)
@@ -384,7 +444,7 @@ module Hobo
384
444
  target = args.empty? || args.first.is_a?(Symbol) ? this : args.shift
385
445
  action = args.first
386
446
 
387
- if (origin = target.try.origin)
447
+ if target.respond_to?(:member_class) && (origin = target.try.origin)
388
448
  klass = origin.class
389
449
  action = if action == :new
390
450
  "new_#{target.origin_attribute.to_s.singularize}"
@@ -401,7 +461,11 @@ module Hobo
401
461
 
402
462
  Hobo::ModelRouter.linkable?(klass, action, options.reverse_merge(:subsite => subsite))
403
463
  end
404
-
464
+
465
+ def css_data(name, *args)
466
+ "#{name.to_s.dasherize}::#{args * '::'}"
467
+ end
468
+
405
469
 
406
470
  # Convenience helper for the default app
407
471
 
@@ -409,7 +473,15 @@ module Hobo
409
473
  def front_models
410
474
  Hobo::Model.all_models.select {|m| linkable?(m) }
411
475
  end
476
+
477
+
478
+ def this_field_name
479
+ this_parent.class.view_hints.field_name(this_field)
480
+ end
412
481
 
482
+ def this_field_help
483
+ this_parent.class.view_hints.field_help[this_field.to_sym]
484
+ end
413
485
 
414
486
 
415
487
  # debugging support
@@ -35,7 +35,6 @@ module Hobo
35
35
  if included_in_save
36
36
  included_in_save.each_pair do |association, records|
37
37
  records.each do |record|
38
- record.user_changes(acting_user) if acting_user
39
38
  record.save_without_validation # This means without transactions too
40
39
  end
41
40
  end
@@ -9,7 +9,11 @@ module Hobo
9
9
  ModelExtensions = classy_module do
10
10
 
11
11
  attr_writer :lifecycle
12
-
12
+
13
+ def self.has_lifecycle?
14
+ defined?(self::Lifecycle)
15
+ end
16
+
13
17
  def self.lifecycle(*args, &block)
14
18
  options = args.extract_options!
15
19
  options = options.reverse_merge(:state_field => :state,
@@ -18,6 +22,8 @@ module Hobo
18
22
  if defined? self::Lifecycle
19
23
  lifecycle = self::Lifecycle
20
24
  else
25
+ # First call
26
+
21
27
  module_eval "class ::#{name}::Lifecycle < Hobo::Lifecycles::Lifecycle; end"
22
28
  lifecycle = self::Lifecycle
23
29
  lifecycle.init(self, options)
@@ -26,19 +32,46 @@ module Hobo
26
32
  dsl = DeclarationDSL.new(lifecycle)
27
33
  dsl.instance_eval(&block)
28
34
 
29
- default = lifecycle.initial_state ? { :default => lifecycle.initial_state.name } : {}
35
+ default = lifecycle.default_state ? { :default => lifecycle.default_state.name.to_s } : {}
30
36
  declare_field(options[:state_field], :string, default)
37
+ never_show options[:state_field]
38
+ attr_protected options[:state_field]
31
39
 
32
- declare_field(options[:key_timestamp_field], :datetime)
33
-
34
- never_show options[:state_field], options[:key_timestamp_field]
35
- attr_protected options[:state_field], options[:key_timestamp_field]
40
+ unless options[:key_timestamp_field] == false
41
+ declare_field(options[:key_timestamp_field], :datetime)
42
+ never_show options[:key_timestamp_field]
43
+ attr_protected options[:key_timestamp_field]
44
+ end
45
+
36
46
  end
47
+
48
+
49
+ def self.validation_method_with_lifecycles(on)
50
+ if has_lifecycle? && self::Lifecycle.step_names.include?(on)
51
+ callback = :"validate_on_#{on}"
52
+
53
+ # define these lifecycle validation callbacks on demand
54
+ define_callbacks callback unless respond_to? :callback
55
+ define_method(callback) {}
56
+
57
+ callback
58
+ else
59
+ validation_method_without_lifecycles(on)
60
+ end
61
+ end
62
+ metaclass.alias_method_chain :validation_method, :lifecycles
63
+
64
+
65
+ def valid_with_lifecycles?
66
+ valid_without_lifecycles?
67
+
68
+ if self.class.has_lifecycle? && (step = lifecycle.active_step) && respond_to?(cb = "validate_on_#{step.name}")
69
+ run_callbacks cb
70
+ end
37
71
 
38
-
39
- def self.has_lifecycle?
40
- defined?(self::Lifecycle)
72
+ errors.empty?
41
73
  end
74
+ alias_method_chain :valid?, :lifecycles
42
75
 
43
76
 
44
77
  def lifecycle
@@ -54,21 +87,22 @@ module Hobo
54
87
  @lifecycle = lifecycle
55
88
  end
56
89
 
57
- def state(*names, &block)
58
- names.map {|name| @lifecycle.def_state(name, block) }
59
- end
60
-
61
- def initial_state(name, &block)
62
- s = @lifecycle.def_state(name, block)
63
- @lifecycle.initial_state = s
90
+ def state(*args, &block)
91
+ options = args.extract_options!
92
+ names = args
93
+ states = names.map {|name| @lifecycle.def_state(name, block) }
94
+ if options[:default]
95
+ raise ArgumentError, "you must define one state if you give the :default option" unless states.length == 1
96
+ @lifecycle.default_state = states.first
97
+ end
64
98
  end
65
99
 
66
- def create(who, name, options={}, &block)
67
- @lifecycle.def_creator(name, who, block, options)
100
+ def create(name, options={}, &block)
101
+ @lifecycle.def_creator(name, block, options)
68
102
  end
69
103
 
70
- def transition(who, name, change, options={}, &block)
71
- @lifecycle.def_transition(name, who,
104
+ def transition(name, change, options={}, &block)
105
+ @lifecycle.def_transition(name,
72
106
  Array(change.keys.first), change.values.first,
73
107
  block, options)
74
108
  end
@@ -77,10 +111,6 @@ module Hobo
77
111
  @lifecycle.invariants << block
78
112
  end
79
113
 
80
- def precondition(&block)
81
- @lifecycle.preconditions << block
82
- end
83
-
84
114
  end
85
115
 
86
116
  end