hobo 0.8.3 → 0.8.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -7,6 +7,7 @@ module Hobo
|
|
7
7
|
VIEWLIB_DIR = "taglibs"
|
8
8
|
|
9
9
|
DONT_PAGINATE_FORMATS = [ Mime::CSV, Mime::YAML, Mime::JSON, Mime::XML, Mime::ATOM, Mime::RSS ]
|
10
|
+
WILL_PAGINATE_OPTIONS = [ :page, :per_page, :total_entries, :count, :finder]
|
10
11
|
|
11
12
|
READ_ONLY_ACTIONS = [:index, :show]
|
12
13
|
WRITE_ONLY_ACTIONS = [:create, :update, :destroy]
|
@@ -30,7 +31,7 @@ module Hobo
|
|
30
31
|
|
31
32
|
rescue_from ActiveRecord::RecordNotFound, :with => :not_found
|
32
33
|
|
33
|
-
rescue_from Hobo::
|
34
|
+
rescue_from Hobo::PermissionDeniedError, :with => :permission_denied
|
34
35
|
rescue_from Hobo::Lifecycles::LifecycleKeyError, :with => :permission_denied
|
35
36
|
|
36
37
|
alias_method_chain :render, :hobo_model
|
@@ -57,7 +58,7 @@ module Hobo
|
|
57
58
|
dir = "#{RAILS_ROOT}/app/controllers#{'/' + subsite if subsite}"
|
58
59
|
Dir.entries(dir).each do |f|
|
59
60
|
if f =~ /^[a-zA-Z_][a-zA-Z0-9_]*_controller\.rb$/
|
60
|
-
name = f.
|
61
|
+
name = f.remove(/.rb$/).camelize
|
61
62
|
name = "#{subsite.camelize}::#{name}" if subsite
|
62
63
|
name.constantize
|
63
64
|
end
|
@@ -69,12 +70,7 @@ module Hobo
|
|
69
70
|
names = (@controller_names || []).select { |n| subsite ? n =~ /^#{subsite.camelize}::/ : n !~ /::/ }
|
70
71
|
|
71
72
|
names.map do |name|
|
72
|
-
|
73
|
-
name.constantize
|
74
|
-
rescue NameError
|
75
|
-
@controller_names.delete name
|
76
|
-
nil
|
77
|
-
end
|
73
|
+
name.safe_constantize || (@controller_names.delete name; nil)
|
78
74
|
end.compact
|
79
75
|
end
|
80
76
|
|
@@ -85,7 +81,7 @@ module Hobo
|
|
85
81
|
attr_writer :model
|
86
82
|
|
87
83
|
def model_name
|
88
|
-
name.
|
84
|
+
name.demodulize.remove(/Controller$/).singularize
|
89
85
|
end
|
90
86
|
|
91
87
|
def model
|
@@ -115,7 +111,7 @@ module Hobo
|
|
115
111
|
# Make sure we have a copy of the options - it is being mutated somewhere
|
116
112
|
opts = {}.merge(options)
|
117
113
|
self.this = find_instance(opts) unless opts[:no_find]
|
118
|
-
raise Hobo::
|
114
|
+
raise Hobo::PermissionDeniedError unless Hobo.can_call?(current_user, @this, method)
|
119
115
|
if got_block
|
120
116
|
instance_eval(&block)
|
121
117
|
else
|
@@ -178,21 +174,23 @@ module Hobo
|
|
178
174
|
|
179
175
|
def def_lifecycle_actions
|
180
176
|
if model.has_lifecycle?
|
181
|
-
model::Lifecycle.
|
182
|
-
|
183
|
-
|
177
|
+
model::Lifecycle.publishable_creators.each do |creator|
|
178
|
+
name = creator.name
|
179
|
+
def_auto_action name do
|
180
|
+
creator_page_action name
|
184
181
|
end
|
185
|
-
def_auto_action "do_#{
|
186
|
-
do_creator_action
|
182
|
+
def_auto_action "do_#{name}" do
|
183
|
+
do_creator_action name
|
187
184
|
end
|
188
185
|
end
|
189
186
|
|
190
|
-
model::Lifecycle.
|
191
|
-
|
192
|
-
|
187
|
+
model::Lifecycle.publishable_transitions.each do |transition|
|
188
|
+
name = transition.name
|
189
|
+
def_auto_action name do
|
190
|
+
transition_page_action name
|
193
191
|
end
|
194
|
-
def_auto_action "do_#{
|
195
|
-
do_transition_action
|
192
|
+
def_auto_action "do_#{name}" do
|
193
|
+
do_transition_action name
|
196
194
|
end
|
197
195
|
end
|
198
196
|
end
|
@@ -220,7 +218,11 @@ module Hobo
|
|
220
218
|
define_method(name, &block)
|
221
219
|
else
|
222
220
|
if scope = options.delete(:scope)
|
223
|
-
|
221
|
+
if scope.is_a?(Symbol)
|
222
|
+
define_method(name) { hobo_index model.send(scope), options.dup }
|
223
|
+
else
|
224
|
+
define_method(name) { hobo_index scope, options.dup }
|
225
|
+
end
|
224
226
|
else
|
225
227
|
define_method(name) { hobo_index options.dup }
|
226
228
|
end
|
@@ -310,8 +312,8 @@ module Hobo
|
|
310
312
|
# GET users/signup, and would show the form, while 'do_signup'
|
311
313
|
# would be routed to POST /users/signup)
|
312
314
|
if model.has_lifecycle?
|
313
|
-
(model::Lifecycle.
|
314
|
-
model::Lifecycle.
|
315
|
+
(model::Lifecycle.publishable_creators.map { |c| [c.name, "do_#{c.name}"] } +
|
316
|
+
model::Lifecycle.publishable_transitions.map { |t| [t.name, "do_#{t.name}"] }).flatten.*.to_sym
|
315
317
|
else
|
316
318
|
[]
|
317
319
|
end
|
@@ -353,11 +355,10 @@ module Hobo
|
|
353
355
|
def valid?; this.errors.empty?; end
|
354
356
|
|
355
357
|
|
356
|
-
def re_render_form(default_action)
|
358
|
+
def re_render_form(default_action=nil)
|
357
359
|
if params[:page_path]
|
358
360
|
controller, view = Controller.controller_and_view_for(params[:page_path])
|
359
361
|
view = default_action if view == Dryml::EMPTY_PAGE
|
360
|
-
@this = instance_variable_get("@#{controller.singularize}")
|
361
362
|
render :template => "#{controller}/#{view}"
|
362
363
|
else
|
363
364
|
render :action => default_action
|
@@ -369,7 +370,7 @@ module Hobo
|
|
369
370
|
after_submit = params[:after_submit]
|
370
371
|
|
371
372
|
# The after_submit post parameter takes priority
|
372
|
-
(after_submit == "stay-here" ?
|
373
|
+
(after_submit == "stay-here" ? previous_page_path : after_submit) ||
|
373
374
|
|
374
375
|
# Then try the record's show page
|
375
376
|
(!destroyed && object_url(@this)) ||
|
@@ -384,6 +385,12 @@ module Hobo
|
|
384
385
|
home_page
|
385
386
|
end
|
386
387
|
|
388
|
+
|
389
|
+
# TODO: Get rid of this joke of an idea that fails miserably if you open another browser window.
|
390
|
+
def previous_page_path
|
391
|
+
session[:previous_page_path]
|
392
|
+
end
|
393
|
+
|
387
394
|
|
388
395
|
def redirect_after_submit(*args)
|
389
396
|
options = args.extract_options!
|
@@ -427,7 +434,7 @@ module Hobo
|
|
427
434
|
options.reverse_merge!(:page => params[:page] || 1)
|
428
435
|
finder.paginate(options)
|
429
436
|
else
|
430
|
-
finder.all(options)
|
437
|
+
finder.all(options.except(*WILL_PAGINATE_OPTIONS))
|
431
438
|
end
|
432
439
|
end
|
433
440
|
|
@@ -437,7 +444,7 @@ module Hobo
|
|
437
444
|
klass = refl.klass
|
438
445
|
id = params["#{klass.name.underscore}_id"]
|
439
446
|
owner = klass.find(id)
|
440
|
-
instance_variable_set("@#{
|
447
|
+
instance_variable_set("@#{owner_association}", owner)
|
441
448
|
[owner, owner.send(model.reverse_reflection(owner_association).name)]
|
442
449
|
end
|
443
450
|
|
@@ -469,7 +476,7 @@ module Hobo
|
|
469
476
|
|
470
477
|
|
471
478
|
def hobo_new(record=nil, &b)
|
472
|
-
self.this = record || model.user_new
|
479
|
+
self.this = record || model.user_new(current_user)
|
473
480
|
response_block(&b)
|
474
481
|
end
|
475
482
|
|
@@ -484,7 +491,7 @@ module Hobo
|
|
484
491
|
def hobo_create(*args, &b)
|
485
492
|
options = args.extract_options!
|
486
493
|
self.this = args.first || new_for_create
|
487
|
-
this.
|
494
|
+
this.user_update_attributes(current_user, options[:attributes] || attribute_parameters || {})
|
488
495
|
create_response(:new, &b)
|
489
496
|
end
|
490
497
|
|
@@ -493,7 +500,7 @@ module Hobo
|
|
493
500
|
options = args.extract_options!
|
494
501
|
owner, association = find_owner_and_association(owner)
|
495
502
|
self.this = args.first || association.new
|
496
|
-
this.
|
503
|
+
this.user_update_attributes(current_user, options[:attributes] || attribute_parameters || {})
|
497
504
|
create_response(:"new_for_#{owner}", &b)
|
498
505
|
end
|
499
506
|
|
@@ -506,7 +513,7 @@ module Hobo
|
|
506
513
|
def new_for_create
|
507
514
|
type_param = subtype_for_create
|
508
515
|
create_model = type_param ? type_param.constantize : model
|
509
|
-
create_model.
|
516
|
+
create_model.user_new(current_user)
|
510
517
|
end
|
511
518
|
|
512
519
|
|
@@ -542,7 +549,7 @@ module Hobo
|
|
542
549
|
|
543
550
|
self.this = args.first || find_instance
|
544
551
|
changes = options[:attributes] || attribute_parameters or raise RuntimeError, "No update specified in params"
|
545
|
-
this.
|
552
|
+
this.user_update_attributes(current_user, changes)
|
546
553
|
|
547
554
|
# Ensure current_user isn't out of date
|
548
555
|
@current_user = @this if @this == current_user
|
@@ -605,38 +612,49 @@ module Hobo
|
|
605
612
|
|
606
613
|
# --- Lifecycle Actions --- #
|
607
614
|
|
615
|
+
def creator_page_action(name, &b)
|
616
|
+
self.this = model.new
|
617
|
+
this.exempt_from_edit_checks = true
|
618
|
+
@creator = model::Lifecycle.creator(name)
|
619
|
+
raise Hobo::PermissionDeniedError unless @creator.allowed?(current_user)
|
620
|
+
response_block &b
|
621
|
+
end
|
622
|
+
|
623
|
+
|
608
624
|
def do_creator_action(name, options={}, &b)
|
609
|
-
@creator = model::Lifecycle.
|
625
|
+
@creator = model::Lifecycle.creator(name)
|
610
626
|
self.this = @creator.run!(current_user, attribute_parameters)
|
611
627
|
response_block(&b) or
|
612
628
|
if valid?
|
613
629
|
redirect_after_submit options
|
614
630
|
else
|
631
|
+
this.exempt_from_edit_checks = true
|
615
632
|
re_render_form(name)
|
616
633
|
end
|
617
634
|
end
|
618
635
|
|
619
636
|
|
620
|
-
def
|
621
|
-
|
622
|
-
|
623
|
-
@creator = model::Lifecycle.creators[name.to_s] or raise ArgumentError, "No such creator in lifecycle: #{name}"
|
624
|
-
end
|
625
|
-
|
626
|
-
|
627
|
-
def prepare_for_transition(name, options={})
|
637
|
+
def prepare_transition(name, options)
|
638
|
+
key = options.delete(:key) || params[:key]
|
639
|
+
|
628
640
|
self.this = find_instance do |record|
|
629
641
|
# The block allows us to perform actions on the records before the permission check
|
630
642
|
record.exempt_from_edit_checks = true
|
631
|
-
record.lifecycle.provided_key =
|
643
|
+
record.lifecycle.provided_key = key
|
632
644
|
end
|
633
|
-
|
645
|
+
this.lifecycle.find_transition(name, current_user) or raise Hobo::PermissionDeniedError
|
646
|
+
end
|
647
|
+
|
648
|
+
|
649
|
+
def transition_page_action(name, options={}, &b)
|
650
|
+
@transition = prepare_transition(name, options)
|
651
|
+
response_block &b
|
634
652
|
end
|
635
653
|
|
636
654
|
|
637
655
|
def do_transition_action(name, *args, &b)
|
638
656
|
options = args.extract_options!
|
639
|
-
|
657
|
+
@transition = prepare_transition(name, options)
|
640
658
|
@transition.run!(this, current_user, attribute_parameters)
|
641
659
|
response_block(&b) or
|
642
660
|
if valid?
|
@@ -647,11 +665,6 @@ module Hobo
|
|
647
665
|
end
|
648
666
|
|
649
667
|
|
650
|
-
def transition_page_action(name, *args)
|
651
|
-
options = args.extract_options!
|
652
|
-
prepare_for_transition(name, options)
|
653
|
-
end
|
654
|
-
|
655
668
|
# --- Miscelaneous Actions --- #
|
656
669
|
|
657
670
|
def hobo_completions(attribute, finder, options={})
|
@@ -667,7 +680,7 @@ module Hobo
|
|
667
680
|
ordering = params["#{model.name.underscore}_ordering"]
|
668
681
|
if ordering
|
669
682
|
ordering.each_with_index do |id, position|
|
670
|
-
model.
|
683
|
+
model.find(id).user_update_attributes(current_user, :position => position+1)
|
671
684
|
end
|
672
685
|
hobo_ajax_response || render(:nothing => true)
|
673
686
|
else
|
@@ -680,8 +693,8 @@ module Hobo
|
|
680
693
|
# --- Response helpers --- #
|
681
694
|
|
682
695
|
def permission_denied(error)
|
683
|
-
self.this =
|
684
|
-
if
|
696
|
+
self.this = true # Otherwise this gets sent user_view
|
697
|
+
if "permission_denied".in?(self.class.superclass.instance_methods)
|
685
698
|
super
|
686
699
|
else
|
687
700
|
respond_to do |wants|
|
@@ -701,7 +714,7 @@ module Hobo
|
|
701
714
|
|
702
715
|
|
703
716
|
def not_found(error)
|
704
|
-
if
|
717
|
+
if "not_found_response".in?(self.class.superclass.instance_methods)
|
705
718
|
super
|
706
719
|
elsif render_tag("not-found-page", {}, :status => 404)
|
707
720
|
# cool
|
data/lib/hobo/model_router.rb
CHANGED
@@ -173,14 +173,17 @@ module Hobo
|
|
173
173
|
|
174
174
|
def owner_routes
|
175
175
|
controller.owner_actions.each_pair do |owner, actions|
|
176
|
-
|
176
|
+
collection_refl = model.reverse_reflection(owner)
|
177
|
+
raise HoboError, "Hob routing error -- can't find reverse association for #{model}##{owner} " +
|
178
|
+
"(e.g. the :has_many that corresponds to a :belongs_to)" if collection_refl.nil?
|
179
|
+
collection = collection_refl.name
|
177
180
|
owner_class = model.reflections[owner].klass.name.underscore
|
178
181
|
|
179
182
|
owner = owner.to_s.singularize if model.reflections[owner].macro == :has_many
|
180
183
|
|
181
184
|
collection_path = "#{owner_class.pluralize}/:#{owner_class}_id/#{collection}"
|
182
185
|
|
183
|
-
actions.each do |action|
|
186
|
+
actions.each do |action|
|
184
187
|
case action
|
185
188
|
when :index
|
186
189
|
linkable_route("#{plural}_for_#{owner}", collection_path, "index_for_#{owner}",
|
@@ -0,0 +1,397 @@
|
|
1
|
+
module Hobo
|
2
|
+
|
3
|
+
module Permissions
|
4
|
+
|
5
|
+
def self.enable
|
6
|
+
Hobo::Permissions::Associations.enable
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.included(klass)
|
10
|
+
klass.extend ClassMethods
|
11
|
+
|
12
|
+
create_with_callbacks = find_aliased_name klass, :create_with_callbacks
|
13
|
+
update_with_callbacks = find_aliased_name klass, :update_with_callbacks
|
14
|
+
destroy_with_callbacks = find_aliased_name klass, :destroy_with_callbacks
|
15
|
+
|
16
|
+
klass.class_eval do
|
17
|
+
alias_method create_with_callbacks, :create_with_callbacks_with_hobo_permission_check
|
18
|
+
alias_method update_with_callbacks, :update_with_callbacks_with_hobo_permission_check
|
19
|
+
alias_method destroy_with_callbacks, :destroy_with_callbacks_with_hobo_permission_check
|
20
|
+
|
21
|
+
attr_accessor :acting_user, :origin, :origin_attribute
|
22
|
+
|
23
|
+
bool_attr_accessor :exempt_from_edit_checks
|
24
|
+
|
25
|
+
define_callbacks :after_user_new
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.find_aliased_name(klass, method_name)
|
30
|
+
# The method +method_name+ will have been aliased. We jump through some hoops to figure out
|
31
|
+
# what it's new name is
|
32
|
+
method_name = method_name.to_s
|
33
|
+
method = klass.instance_method method_name
|
34
|
+
methods = klass.private_instance_methods + klass.instance_methods
|
35
|
+
new_name = methods.select {|m| klass.instance_method(m) == method }.find { |m| m != method_name }
|
36
|
+
end
|
37
|
+
|
38
|
+
module ClassMethods
|
39
|
+
|
40
|
+
def user_find(user, *args)
|
41
|
+
record = find(*args)
|
42
|
+
yield(record) if block_given?
|
43
|
+
record.user_view user
|
44
|
+
record
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
def user_new(user, attributes={})
|
49
|
+
new(attributes) do |r|
|
50
|
+
r.set_creator user
|
51
|
+
yield r if block_given?
|
52
|
+
r.user_view(user)
|
53
|
+
r.send :callback, :after_user_new
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
def user_create(user, attributes={}, &block)
|
59
|
+
if attributes.is_a?(Array)
|
60
|
+
attributes.map { |attrs| user_create(user, attrs) }
|
61
|
+
else
|
62
|
+
record = user_new(user, attributes, &block)
|
63
|
+
record.user_save(user)
|
64
|
+
record
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
def user_create!(user, attributes={}, &block)
|
70
|
+
if attributes.is_a?(Array)
|
71
|
+
attributes.map { |attrs| user_create(user, attrs) }
|
72
|
+
else
|
73
|
+
record = user_new(user, attributes, &block)
|
74
|
+
record.user_save!(user)
|
75
|
+
record
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def viewable_by?(user, attribute=nil)
|
80
|
+
new.viewable_by?(user, attribute)
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
# --- Hook ActiveRecord CRUD actions --- #
|
87
|
+
|
88
|
+
|
89
|
+
def permission_check_required?
|
90
|
+
# Lifecycle steps are exempt from permission checks
|
91
|
+
acting_user && !(self.class.has_lifecycle? && lifecycle.active_step)
|
92
|
+
end
|
93
|
+
|
94
|
+
def create_with_callbacks_with_hobo_permission_check(*args)
|
95
|
+
return false if callback(:before_create) == false
|
96
|
+
|
97
|
+
if permission_check_required?
|
98
|
+
create_permitted? or raise PermissionDeniedError, "#{self.class.name}#create"
|
99
|
+
end
|
100
|
+
|
101
|
+
result = create_without_callbacks
|
102
|
+
callback(:after_create)
|
103
|
+
result
|
104
|
+
end
|
105
|
+
|
106
|
+
def update_with_callbacks_with_hobo_permission_check(*args)
|
107
|
+
return false if callback(:before_update) == false
|
108
|
+
|
109
|
+
if permission_check_required?
|
110
|
+
update_permitted? or raise PermissionDeniedError, "#{self.class.name}#update"
|
111
|
+
end
|
112
|
+
|
113
|
+
result = update_without_callbacks(*args)
|
114
|
+
callback(:after_update)
|
115
|
+
result
|
116
|
+
end
|
117
|
+
|
118
|
+
def destroy_with_callbacks_with_hobo_permission_check
|
119
|
+
return false if callback(:before_destroy) == false
|
120
|
+
|
121
|
+
if permission_check_required?
|
122
|
+
destroy_permitted? or raise PermissionDeniedError, "#{self.class.name}#.destroy"
|
123
|
+
end
|
124
|
+
|
125
|
+
result = destroy_without_callbacks
|
126
|
+
callback(:after_destroy)
|
127
|
+
result
|
128
|
+
end
|
129
|
+
|
130
|
+
# -------------------------------------- #
|
131
|
+
|
132
|
+
|
133
|
+
# --- Permissions API --- #
|
134
|
+
|
135
|
+
|
136
|
+
def with_acting_user(user)
|
137
|
+
old = acting_user
|
138
|
+
self.acting_user = user
|
139
|
+
result = yield
|
140
|
+
self.acting_user = old
|
141
|
+
result
|
142
|
+
end
|
143
|
+
|
144
|
+
def user_save(user)
|
145
|
+
with_acting_user(user) { save }
|
146
|
+
end
|
147
|
+
|
148
|
+
def user_save!(user)
|
149
|
+
with_acting_user(user) { save! }
|
150
|
+
end
|
151
|
+
|
152
|
+
def user_destroy(user)
|
153
|
+
with_acting_user(user) { destroy }
|
154
|
+
end
|
155
|
+
|
156
|
+
def user_view(user, attribute=nil)
|
157
|
+
raise PermissionDeniedError unless viewable_by?(user, attribute)
|
158
|
+
end
|
159
|
+
|
160
|
+
def user_update_attributes(user, attributes)
|
161
|
+
with_acting_user(user) do
|
162
|
+
self.attributes = attributes
|
163
|
+
save
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def user_update_attributes!(user, attributes)
|
168
|
+
with_acting_user(user) do
|
169
|
+
self.attributes = attributes
|
170
|
+
save!
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def creatable_by?(user)
|
175
|
+
with_acting_user(user) { create_permitted? }
|
176
|
+
end
|
177
|
+
|
178
|
+
def updatable_by?(user)
|
179
|
+
with_acting_user(user) { update_permitted? }
|
180
|
+
end
|
181
|
+
|
182
|
+
def destroyable_by?(user)
|
183
|
+
with_acting_user(user) { destroy_permitted? }
|
184
|
+
end
|
185
|
+
|
186
|
+
def method_callable_by?(user, method)
|
187
|
+
permission_method = "#{method}_call_permitted?"
|
188
|
+
respond_to?(permission_method) && with_acting_user(current_user) { send(permission_method) }
|
189
|
+
end
|
190
|
+
|
191
|
+
def viewable_by?(user, attribute=nil)
|
192
|
+
if attribute
|
193
|
+
attribute = attribute.to_s.sub(/\?$/, '').to_sym
|
194
|
+
return false if attribute && self.class.never_show?(attribute)
|
195
|
+
end
|
196
|
+
with_acting_user(user) { view_permitted?(attribute) }
|
197
|
+
end
|
198
|
+
|
199
|
+
|
200
|
+
def editable_by?(user, attribute=nil)
|
201
|
+
return false if attribute_protected?(attribute)
|
202
|
+
|
203
|
+
return true if exempt_from_edit_checks?
|
204
|
+
|
205
|
+
# Can't view implies can't edit
|
206
|
+
return false unless viewable_by?(user, attribute)
|
207
|
+
|
208
|
+
if attribute
|
209
|
+
attribute = attribute.to_s.sub(/\?$/, '').to_sym
|
210
|
+
|
211
|
+
# Try the attribute-specic edit-permission method if there is one
|
212
|
+
if has_hobo_method?(meth = "#{attribute}_edit_permitted?")
|
213
|
+
with_acting_user(user) { send(meth) }
|
214
|
+
end
|
215
|
+
|
216
|
+
# No setter = no edit permission
|
217
|
+
return false if !respond_to?("#{attribute}=")
|
218
|
+
|
219
|
+
refl = self.class.reflections[attribute.to_sym]
|
220
|
+
if refl && refl.macro != :belongs_to # a belongs_to is handled the same as a regular attribute
|
221
|
+
return association_editable_by?(user, refl)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
with_acting_user(user) { edit_permitted?(attribute) }
|
226
|
+
end
|
227
|
+
|
228
|
+
|
229
|
+
def attribute_protected?(attribute)
|
230
|
+
attribute = attribute.to_s
|
231
|
+
|
232
|
+
return true if attributes_protected_by_default.include? attribute
|
233
|
+
|
234
|
+
if self.class.accessible_attributes
|
235
|
+
return true if !self.class.accessible_attributes.include?(attribute)
|
236
|
+
elsif self.class.protected_attributes
|
237
|
+
return true if self.class.protected_attributes.include?(attribute)
|
238
|
+
end
|
239
|
+
|
240
|
+
# Readonly attributes can be set on creation but not thereafter
|
241
|
+
return self.class.readonly_attributes.include?(attribute) if !new_record? && self.class.readonly_attributes
|
242
|
+
|
243
|
+
false
|
244
|
+
end
|
245
|
+
|
246
|
+
|
247
|
+
def association_editable_by?(user, reflection)
|
248
|
+
# has_one and polymorphic associations are not editable (for now)
|
249
|
+
return false if reflection.macro == :has_one || reflection.options[:polymorphic]
|
250
|
+
|
251
|
+
return false unless reflection.options[:accessible]
|
252
|
+
|
253
|
+
record = if (through = reflection.through_reflection)
|
254
|
+
# For edit permission on a has_many :through,
|
255
|
+
# the user needs create+destroy permission on the join model
|
256
|
+
send(through.name).new_candidate
|
257
|
+
else
|
258
|
+
# For edit permission on a regular has_many,
|
259
|
+
# the user needs create/destroy permission on the member model
|
260
|
+
send(reflection.name).new_candidate
|
261
|
+
end
|
262
|
+
record.creatable_by?(user) && record.destroyable_by?(user)
|
263
|
+
end
|
264
|
+
|
265
|
+
# ----------------------- #
|
266
|
+
|
267
|
+
|
268
|
+
# --- Permission Declaration Helpers --- #
|
269
|
+
|
270
|
+
def only_changed?(*attributes)
|
271
|
+
attributes = attributes.map do |attr|
|
272
|
+
with_attribute_or_belongs_to_keys(attr) { |a, ftype| ftype ? [a, ftype] : a }
|
273
|
+
end.flatten
|
274
|
+
|
275
|
+
changed.all? { |attr| attributes.include?(attr) }
|
276
|
+
end
|
277
|
+
|
278
|
+
def none_changed?(*attributes)
|
279
|
+
attributes = attributes.map do |attr|
|
280
|
+
with_attribute_or_belongs_to_keys(attr) { |a, ftype| ftype ? [a, ftype] : a }
|
281
|
+
end.flatten
|
282
|
+
|
283
|
+
attributes.all? { |attr| !changed.include?(attr) }
|
284
|
+
end
|
285
|
+
|
286
|
+
def any_changed?(*attributes)
|
287
|
+
attributes.any? do |attr|
|
288
|
+
with_attribute_or_belongs_to_keys(attr) do |a, ftype|
|
289
|
+
if ftype
|
290
|
+
changed.include?(a) || changed.include?(ftype)
|
291
|
+
else
|
292
|
+
changed.include?(a)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def all_changed?(*attributes)
|
299
|
+
attributes = prepare_attributes_for_change_helpers(attributes)
|
300
|
+
attributes.all? do |attr|
|
301
|
+
with_attribute_or_belongs_to_keys(attr) do |a, ftype|
|
302
|
+
if ftype
|
303
|
+
changed.include?(a) || changed.include?(ftype)
|
304
|
+
else
|
305
|
+
changed.include?(a)
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def with_attribute_or_belongs_to_keys(attribute)
|
312
|
+
if (refl = self.class.reflections[attribute.to_sym]) && refl.macro == :belongs_to
|
313
|
+
if refl.options[:polymorphic]
|
314
|
+
yield refl.primary_key_name, refl.options[:foreign_type]
|
315
|
+
else
|
316
|
+
yield refl.primary_key_name, nil
|
317
|
+
end
|
318
|
+
else
|
319
|
+
yield attribute.to_s, nil
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
|
324
|
+
|
325
|
+
# -------------------------------------- #
|
326
|
+
|
327
|
+
|
328
|
+
# --- Default *_permitted? methods --- #
|
329
|
+
|
330
|
+
# Conservative default permissions #
|
331
|
+
def create_permitted?; false end
|
332
|
+
def update_permitted?; false end
|
333
|
+
def destroy_permitted?; false end
|
334
|
+
|
335
|
+
# Allow viewing by default
|
336
|
+
def view_permitted?(attribute) true end
|
337
|
+
|
338
|
+
# By default, attempt to derive edit permission from create/update permission
|
339
|
+
def edit_permitted?(attribute)
|
340
|
+
Hobo::Permissions.unknownify_attribute(self, attribute) if attribute
|
341
|
+
new_record? ? create_permitted? : update_permitted?
|
342
|
+
rescue Hobo::UndefinedAccessError
|
343
|
+
# The permission is dependent on the unknown value
|
344
|
+
# so this attribute is not editable
|
345
|
+
false
|
346
|
+
ensure
|
347
|
+
Hobo::Permissions.deunknownify_attribute(self, attribute) if attribute
|
348
|
+
end
|
349
|
+
|
350
|
+
|
351
|
+
# Add some singleton methods to +record+ so give the effect that +attribute+ is unknown. That is,
|
352
|
+
# attempts to access the attribute will result in a Hobo::UndefinedAccessError
|
353
|
+
def self.unknownify_attribute(record, attr)
|
354
|
+
record.metaclass.class_eval do
|
355
|
+
|
356
|
+
define_method attr do
|
357
|
+
raise Hobo::UndefinedAccessError
|
358
|
+
end
|
359
|
+
|
360
|
+
define_method "#{attr}_change" do
|
361
|
+
raise Hobo::UndefinedAccessError
|
362
|
+
end
|
363
|
+
|
364
|
+
define_method "#{attr}_was" do
|
365
|
+
read_attribute attr
|
366
|
+
end
|
367
|
+
|
368
|
+
define_method "#{attr}_changed?" do
|
369
|
+
true
|
370
|
+
end
|
371
|
+
|
372
|
+
def changed?
|
373
|
+
true
|
374
|
+
end
|
375
|
+
|
376
|
+
define_method :changed do
|
377
|
+
changed_attributes.keys | [attr]
|
378
|
+
end
|
379
|
+
|
380
|
+
def changes
|
381
|
+
raise Hobo::UndefinedAccessError
|
382
|
+
end
|
383
|
+
|
384
|
+
end
|
385
|
+
|
386
|
+
end
|
387
|
+
|
388
|
+
# Best. Name. Ever
|
389
|
+
def self.deunknownify_attribute(record, attr)
|
390
|
+
[attr, "#{attr}_change", "#{attr}_was", "#{attr}_changed?", :changed?, :changed, :changes].each do |m|
|
391
|
+
record.metaclass.send :remove_method, m.to_sym
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
end
|
397
|
+
|