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.
- data/CHANGES.txt +330 -0
- data/Manifest +12 -4
- data/Rakefile +4 -6
- data/dryml_generators/rapid/cards.dryml.erb +5 -1
- data/dryml_generators/rapid/forms.dryml.erb +8 -10
- data/dryml_generators/rapid/pages.dryml.erb +65 -36
- data/hobo.gemspec +28 -15
- data/lib/active_record/association_collection.rb +3 -22
- data/lib/hobo.rb +25 -258
- data/lib/hobo/accessible_associations.rb +131 -0
- data/lib/hobo/authentication_support.rb +15 -9
- data/lib/hobo/composite_model.rb +1 -1
- data/lib/hobo/controller.rb +7 -8
- data/lib/hobo/dryml.rb +9 -10
- data/lib/hobo/dryml/dryml_builder.rb +7 -1
- data/lib/hobo/dryml/dryml_doc.rb +161 -0
- data/lib/hobo/dryml/dryml_generator.rb +18 -9
- data/lib/hobo/dryml/part_context.rb +76 -42
- data/lib/hobo/dryml/tag_parameters.rb +1 -0
- data/lib/hobo/dryml/taglib.rb +2 -1
- data/lib/hobo/dryml/template.rb +39 -29
- data/lib/hobo/dryml/template_environment.rb +79 -37
- data/lib/hobo/dryml/template_handler.rb +66 -21
- data/lib/hobo/guest.rb +2 -10
- data/lib/hobo/hobo_helper.rb +125 -53
- data/lib/hobo/include_in_save.rb +0 -1
- data/lib/hobo/lifecycles.rb +54 -24
- data/lib/hobo/lifecycles/actions.rb +95 -31
- data/lib/hobo/lifecycles/creator.rb +18 -23
- data/lib/hobo/lifecycles/lifecycle.rb +86 -62
- data/lib/hobo/lifecycles/state.rb +1 -2
- data/lib/hobo/lifecycles/transition.rb +22 -28
- data/lib/hobo/model.rb +64 -176
- data/lib/hobo/model_controller.rb +67 -54
- data/lib/hobo/model_router.rb +5 -2
- data/lib/hobo/permissions.rb +397 -0
- data/lib/hobo/permissions/associations.rb +167 -0
- data/lib/hobo/scopes.rb +15 -38
- data/lib/hobo/scopes/association_proxy_extensions.rb +15 -5
- data/lib/hobo/scopes/automatic_scopes.rb +43 -18
- data/lib/hobo/scopes/named_scope_extensions.rb +2 -2
- data/lib/hobo/user.rb +10 -4
- data/lib/hobo/user_controller.rb +6 -5
- data/lib/hobo/view_hints.rb +58 -0
- data/rails_generators/hobo/hobo_generator.rb +7 -3
- data/rails_generators/hobo/templates/guest.rb +1 -13
- data/rails_generators/hobo_front_controller/hobo_front_controller_generator.rb +1 -1
- data/rails_generators/hobo_model/hobo_model_generator.rb +4 -2
- data/rails_generators/hobo_model/templates/hints.rb +4 -0
- data/rails_generators/hobo_model/templates/model.rb +8 -8
- data/rails_generators/hobo_model_controller/hobo_model_controller_generator.rb +10 -0
- data/rails_generators/hobo_model_controller/templates/controller.rb +1 -1
- data/rails_generators/hobo_rapid/templates/hobo-rapid.js +91 -56
- data/rails_generators/hobo_rapid/templates/lowpro.js +15 -15
- data/rails_generators/hobo_rapid/templates/reset.css +36 -3
- data/rails_generators/hobo_rapid/templates/themes/clean/public/stylesheets/clean.css +13 -17
- data/rails_generators/hobo_user_controller/templates/controller.rb +1 -1
- data/rails_generators/hobo_user_model/templates/model.rb +18 -16
- data/taglibs/core.dryml +60 -18
- data/taglibs/rapid.dryml +8 -401
- data/taglibs/rapid_core.dryml +586 -0
- data/taglibs/rapid_document_tags.dryml +28 -10
- data/taglibs/rapid_editing.dryml +92 -55
- data/taglibs/rapid_forms.dryml +406 -87
- data/taglibs/rapid_generics.dryml +1 -1
- data/taglibs/rapid_navigation.dryml +2 -1
- data/taglibs/rapid_pages.dryml +7 -16
- data/taglibs/rapid_plus.dryml +39 -14
- data/taglibs/rapid_support.dryml +1 -1
- data/taglibs/rapid_user_pages.dryml +14 -4
- data/tasks/{generate_tag_reference.rb → generate_tag_reference.rake} +49 -18
- data/tasks/hobo_tasks.rake +16 -0
- data/test/permissions/models/models.rb +134 -0
- data/test/permissions/models/schema.rb +55 -0
- data/test/permissions/models/test.sqlite3 +0 -0
- data/test/permissions/test_permissions.rb +436 -0
- metadata +27 -14
- data/lib/hobo/mass_assignment.rb +0 -64
- data/rails_generators/hobo/templates/patch_routing.rb +0 -30
- data/uninstall.rb +0 -1
data/lib/hobo/guest.rb
CHANGED
@@ -5,7 +5,7 @@ module Hobo
|
|
5
5
|
alias_method :has_hobo_method?, :respond_to?
|
6
6
|
|
7
7
|
def to_s
|
8
|
-
"
|
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
|
-
"
|
20
|
+
"guest"
|
29
21
|
end
|
30
22
|
|
31
23
|
end
|
data/lib/hobo/hobo_helper.rb
CHANGED
@@ -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.
|
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
|
-
|
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)
|
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
|
-
"@" +
|
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
|
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
|
-
|
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
|
214
|
-
|
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
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
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=
|
240
|
-
|
257
|
+
def can_delete?(object=this)
|
258
|
+
object.destroyable_by?(current_user)
|
241
259
|
end
|
242
260
|
|
243
261
|
|
244
|
-
|
245
|
-
|
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
|
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 !
|
256
|
-
|
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
|
-
|
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(
|
353
|
+
def param_name_for(path)
|
293
354
|
field_path = field_path.to_s.split(".") if field_path.is_a?(String, Symbol)
|
294
|
-
attrs =
|
295
|
-
"#{
|
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(
|
363
|
+
param_name_for(path_for_form_field[0..-2] + [refl.primary_key_name])
|
303
364
|
else
|
304
|
-
param_name_for(
|
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
|
430
|
+
query = request.request_uri =~ /\?([^?]+)/ && $1
|
431
|
+
result = HashWithIndifferentAccess.new
|
370
432
|
if query
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
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
|
data/lib/hobo/include_in_save.rb
CHANGED
data/lib/hobo/lifecycles.rb
CHANGED
@@ -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.
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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(*
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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(
|
67
|
-
@lifecycle.def_creator(name,
|
100
|
+
def create(name, options={}, &block)
|
101
|
+
@lifecycle.def_creator(name, block, options)
|
68
102
|
end
|
69
103
|
|
70
|
-
def transition(
|
71
|
-
@lifecycle.def_transition(name,
|
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
|