hobo 0.8.3 → 0.8.4

Sign up to get free protection for your applications and to get access to all the features.
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
+