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
@@ -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::Model::PermissionDeniedError, :with => :permission_denied
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.sub(/.rb$/, '').camelize
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
- begin
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.sub(/Controller$/, "").singularize
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::Model::PermissionDeniedError unless Hobo.can_call?(current_user, @this, method)
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.creator_names.each do |creator|
182
- def_auto_action creator do
183
- creator_page_action creator
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_#{creator}" do
186
- do_creator_action creator
182
+ def_auto_action "do_#{name}" do
183
+ do_creator_action name
187
184
  end
188
185
  end
189
186
 
190
- model::Lifecycle.transition_names.each do |transition|
191
- def_auto_action transition do
192
- transition_page_action transition
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_#{transition}" do
195
- do_transition_action transition
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
- define_method(name) { hobo_index scope, options.dup }
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.creator_names.map { |c| [c, "do_#{c}"] } +
314
- model::Lifecycle.transition_names.map { |t| [t, "do_#{t}"] }).flatten.*.to_sym
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" ? session[:previous_page_path] : after_submit) ||
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("@#{owner.class.name.underscore}", owner)
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!(current_user)
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.user_save_changes(current_user, options[:attributes] || attribute_parameters || {})
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.user_save_changes(current_user, options[:attributes] || attribute_parameters || {})
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.new
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.user_save_changes(current_user, changes)
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.creators[name.to_s]
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 creator_page_action(name)
621
- self.this = model.new
622
- this.exempt_from_edit_checks = true
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 = params[:key]
643
+ record.lifecycle.provided_key = key
632
644
  end
633
- @transition = this.lifecycle.find_transition(name, current_user)
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
- prepare_for_transition(name)
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.user_update(current_user, id, :position => position+1)
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 = nil # Otherwise this gets sent user_view
684
- if :permission_denied.in?(self.class.superclass.instance_methods)
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 :not_found_response.in?(self.class.superclass.instance_methods)
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
@@ -173,14 +173,17 @@ module Hobo
173
173
 
174
174
  def owner_routes
175
175
  controller.owner_actions.each_pair do |owner, actions|
176
- collection = model.reverse_reflection(owner).name
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
+