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
@@ -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
|
+
|