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
@@ -1,73 +1,137 @@
1
+ # We need to be able to eval an expression outside of the Hobo module
2
+ # so that, e.g. "Userd" doesn't eval to "Hobo::User"
3
+ # (Ruby determines this constant lookup scope lexically)
4
+ def __top_level_eval__(obj, expr)
5
+ obj.instance_eval(expr)
6
+ end
7
+
1
8
  module Hobo
2
9
 
3
10
  module Lifecycles
4
11
 
5
12
  module Actions
6
13
 
7
- def set_or_check_who!(record, user)
14
+ def available_to_acting_user?(record)
15
+ return true if available_to.nil? # available_to not specified (these steps are not published)
16
+ acting_user_is?(available_to, record)
17
+ end
18
+
19
+
20
+ def acting_user_is?(who, record)
21
+ user = record.acting_user
22
+
8
23
  case who
9
- when :nobody
10
- user == :nobody
11
- when :anybody
24
+ when :all
12
25
  true
26
+
27
+ when :key_holder
28
+ record.lifecycle.valid_key?
29
+
13
30
  when :self
14
31
  record == user
32
+
15
33
  when Array
16
- who.detect {|attribute| record.send(attribute) == user }
34
+ # recursively apply the same test to every item in the array
35
+ who.detect { |w| acting_user_is?(w, record) }
36
+
17
37
  else
18
- if (current = record.send(who)) # it's already set, check it's the same user
19
- user == current
20
- elsif user.is_a?(record.class.attr_type(who))
21
- record.send("#{who}=", user)
22
- true
38
+ refl = record.class.reflections[who]
39
+ if refl && refl.macro == :has_many
40
+ send(who).include?(user)
41
+ elsif refl && refl.macro == :belongs_to
42
+ send("#{who}_is?", user)
23
43
  else
24
- false
44
+ value = run_hook(record, who)
45
+ if value.is_a?(Class)
46
+ user.is_a?(value)
47
+ elsif value.respond_to?(:include?)
48
+ value.include?(user)
49
+ else
50
+ value == user
51
+ end
25
52
  end
26
53
  end
27
54
  end
28
55
 
29
56
 
30
57
  def run_hook(record, hook, *args)
31
- if hook.is_a?(Symbol)
58
+ case hook
59
+ when Symbol
32
60
  record.send(hook, *args)
33
- elsif hook.is_a?(Proc)
34
- hook.call(record, *args)
61
+ when String
62
+ __top_level_eval__(record, hook)
63
+ when Proc
64
+ if hook.arity == 1
65
+ hook.call(record)
66
+ else
67
+ record.instance_eval(&hook)
68
+ end
35
69
  end
36
70
  end
37
71
 
38
72
 
39
73
  def fire_event(record, event)
40
- record.instance_eval(&event) if event
74
+ if event
75
+ if event.arity == 1
76
+ event.call(record)
77
+ else
78
+ record.instance_eval(&event)
79
+ end
80
+ end
41
81
  end
42
82
 
43
83
 
44
- def check_guard(record, user)
45
- !options[:if] || run_hook(record, options[:if], user)
46
- end
47
-
48
- def check_invariants(record)
49
- record.lifecycle.invariants_satisfied?
84
+ def guard_ok?(record)
85
+ if options[:if]
86
+ raise ArgumentError, "do not provide both :if and :unless to lifecycle steps" if options[:unless]
87
+ run_hook(record, options[:if])
88
+ elsif options[:unless]
89
+ !run_hook(record, options[:unless])
90
+ else
91
+ true
92
+ end
50
93
  end
51
94
 
52
95
 
53
- def prepare(record, user, attributes=nil)
54
- if attributes
55
- attributes = extract_attributes(attributes)
56
- record.attributes = attributes
57
- end
96
+ def prepare!(record, attributes)
97
+ record.attributes = extract_attributes(attributes) if attributes
58
98
  record.lifecycle.generate_key if options[:new_key]
59
- set_or_check_who!(record, user) && record
99
+ apply_user_becomes!(record)
60
100
  end
61
101
 
62
102
 
63
- def prepare_and_check!(record, user, attributes=nil)
64
- prepare(record, user, attributes) && check_guard(record, user) && check_invariants(record)
103
+ def can_run?(record)
104
+ available_to_acting_user?(record) && guard_ok?(record) && record.lifecycle.invariants_satisfied?
105
+ end
106
+
107
+
108
+ def available_to
109
+ options[:available_to]
65
110
  end
66
111
 
112
+
67
113
  def publishable?
68
- who != :nobody
114
+ available_to
69
115
  end
70
-
116
+
117
+
118
+ def apply_user_becomes!(record)
119
+ if (assoc = options[:user_becomes])
120
+ record.send("#{assoc}=", record.acting_user)
121
+ end
122
+ end
123
+
124
+ def get_state(record, state)
125
+ case state
126
+ when Proc
127
+ state.call(record)
128
+ when String
129
+ eval(state, record.instance_eval { binding })
130
+ else
131
+ state
132
+ end
133
+ end
134
+
71
135
  end
72
136
 
73
137
  end
@@ -1,35 +1,25 @@
1
1
  module Hobo
2
2
  module Lifecycles
3
3
 
4
- class Creator < Struct.new(:lifecycle, :name, :who, :on_create, :options)
4
+ class Creator < Struct.new(:lifecycle, :name, :on_create, :options)
5
5
 
6
6
  def initialize(*args)
7
7
  super
8
+ self.name = name.to_sym
8
9
  lifecycle.creators[name] = self
9
10
  end
10
11
 
11
12
  include Actions
12
13
 
13
- def check_preconditions(record)
14
- record.lifecycle.preconditions_satisfied?
15
- end
16
-
17
-
18
- def prepare_and_check_with_preconditions!(record, user, attributes=nil)
19
- prepare_and_check_without_preconditions!(record, user, attributes) && check_preconditions(record)
20
- end
21
- alias_method_chain :prepare_and_check!, :preconditions
22
-
23
-
24
- def allowed?(user, attributes=nil)
14
+ def allowed?(user)
25
15
  record = lifecycle.model.new
26
- prepare_and_check!(record, user, attributes)
16
+ record.with_acting_user(user) { can_run?(record) }
27
17
  end
28
18
 
29
19
 
30
20
  def candidate(user, attributes=nil)
31
21
  record = lifecycle.model.new
32
- prepare_and_check!(record, user, attributes)
22
+ record.with_acting_user(user) { prepare!(record, attributes) }
33
23
  record.exempt_from_edit_checks = true
34
24
  record
35
25
  end
@@ -42,6 +32,7 @@ module Hobo
42
32
  params.each do |p|
43
33
  if (refl = model.reflections[p]) && refl.macro == :belongs_to
44
34
  allowed << refl.primary_key_name.to_s
35
+ allowed << refl.options[:foreign_type] if refl.options[:polymorphic]
45
36
  end
46
37
  end
47
38
  attributes & allowed
@@ -49,20 +40,24 @@ module Hobo
49
40
 
50
41
 
51
42
  def change_state(record)
52
- state = options[:become]
53
- record.lifecycle.become(state) if state
43
+ state = get_state(record, options[:become])
44
+ record.lifecycle.become state if state
54
45
  end
55
46
 
56
47
 
57
48
  def run!(user, attributes)
58
49
  record = lifecycle.model.new
59
- if prepare_and_check!(record, user, attributes)
60
- if change_state(record)
61
- fire_event(record, on_create)
50
+ record.lifecycle.active_step = self
51
+ record.with_acting_user(user) do
52
+ prepare!(record, attributes)
53
+ if can_run?(record)
54
+ if change_state(record)
55
+ fire_event(record, on_create)
56
+ end
57
+ record
58
+ else
59
+ raise Hobo::PermissionDeniedError
62
60
  end
63
- record
64
- else
65
- raise Hobo::Model::PermissionDeniedError
66
61
  end
67
62
  end
68
63
 
@@ -14,49 +14,51 @@ module Hobo
14
14
  @states = {}
15
15
  @creators = {}
16
16
  @transitions = []
17
- @preconditions = []
18
17
  @invariants = []
19
18
  end
20
19
 
21
20
  class << self
22
- attr_accessor :model, :options, :states, :initial_state,
23
- :creators, :transitions, :invariants, :preconditions
21
+ attr_accessor :model, :options, :states, :default_state,
22
+ :creators, :transitions, :invariants
24
23
  end
25
24
 
26
25
  def self.def_state(name, on_enter)
27
- name = name.to_s
28
- returning(State.new(name, on_enter)) do |s|
26
+ name = name.to_sym
27
+ returning(Lifecycles::State.new(name, on_enter)) do |s|
29
28
  states[name] = s
30
- class_eval "def #{name}_state?; state_name == '#{name}' end"
29
+ class_eval "def #{name}_state?; state_name == :#{name} end"
31
30
  end
32
31
  end
33
32
 
34
33
 
35
- def self.def_creator(name, who, on_create, options)
36
- name = name.to_s
37
- returning(Creator.new(self, name, who, on_create, options)) do |creator|
34
+ def self.def_creator(name, on_create, options)
35
+ name = name.to_sym
36
+ returning(Creator.new(self, name, on_create, options)) do |creator|
38
37
 
39
38
  class_eval %{
40
39
  def self.#{name}(user, attributes=nil)
41
- create('#{name}', user, attributes)
40
+ create(:#{name}, user, attributes)
42
41
  end
43
42
  def self.can_#{name}?(user, attributes=nil)
44
- can_create?('#{name}', user, attributes)
43
+ can_create?(:#{name}, user)
45
44
  end
46
45
  }
47
46
 
48
47
  end
49
48
  end
50
49
 
51
- def self.def_transition(name, who, start_state, end_states, on_transition, options)
52
- returning(Transition.new(self, name.to_s, who, start_state, end_states, on_transition, options)) do |t|
50
+ def self.def_transition(name, start_state, end_states, on_transition, options)
51
+ returning(Transition.new(self, name.to_s, start_state, end_states, on_transition, options)) do |t|
53
52
 
54
53
  class_eval %{
55
- def #{name}(user, attributes=nil)
56
- transition('#{name}', user, attributes)
54
+ def #{name}!(user, attributes=nil)
55
+ transition(:#{name}, user, attributes)
57
56
  end
58
57
  def can_#{name}?(user, attributes=nil)
59
- can_transition?('#{name}', user, attributes)
58
+ can_transition?(:#{name}, user)
59
+ end
60
+ def valid_for_#{name}?
61
+ valid_for_transition?(:#{name})
60
62
  end
61
63
  }
62
64
 
@@ -66,28 +68,33 @@ module Hobo
66
68
  def self.state_names
67
69
  states.keys
68
70
  end
69
-
70
-
71
- def self.can_create?(name, user, attributes=nil)
72
- creators[name.to_s].allowed?(user, attributes)
71
+
72
+ def self.publishable_creators
73
+ creators.values.where.publishable?
73
74
  end
74
-
75
-
76
- def self.create(name, user, attributes=nil)
77
- creator = creators[name.to_s]
78
- record = creator.run!(user, attributes)
79
- record.lifecycle.active_step = creator
80
- record
75
+
76
+ def self.publishable_transitions
77
+ transitions.where.publishable?
78
+ end
79
+
80
+ def self.step_names
81
+ (creators.keys | transitions.*.name).uniq
82
+ end
83
+
84
+
85
+ def self.creator(name)
86
+ creators[name.to_sym] or raise ArgumentError, "No such creator in lifecycle: #{name}"
81
87
  end
82
88
 
83
89
 
84
- def self.creator_names
85
- creators.keys
90
+ def self.can_create?(name, user)
91
+ creators[name.to_sym].allowed?(user)
86
92
  end
87
93
 
88
94
 
89
- def self.transition_names
90
- transitions.*.name.uniq
95
+ def self.create(name, user, attributes=nil)
96
+ creator = creators[name.to_sym]
97
+ creator.run!(user, attributes)
91
98
  end
92
99
 
93
100
 
@@ -108,26 +115,33 @@ module Hobo
108
115
  end
109
116
 
110
117
 
111
- def can_transition?(name, user, attributes=nil)
112
- available_transitions_for(user, name, attributes).any?
118
+ def can_transition?(name, user)
119
+ available_transitions_for(user, name).any?
113
120
  end
114
121
 
115
122
 
116
- def transition(name, user, attributes=nil)
117
- transition = find_transition(name, user, attributes)
118
- self.active_step = transition
123
+ def transition(name, user, attributes)
124
+ transition = find_transition(name, user)
119
125
  transition.run!(record, user, attributes)
120
126
  end
121
127
 
122
128
 
123
- def find_transition(name, user, attributes=nil)
124
- available_transitions_for(user, name, attributes).first or
125
- raise LifecycleError, "No #{name} transition available to #{user} on this #{record.class.name}"
129
+ def find_transition(name, user)
130
+ available_transitions_for(user, name).first
131
+ end
132
+
133
+
134
+ def valid_for_transition?(name)
135
+ record.valid?
136
+ callback = :"validate_on_#{name}"
137
+ record.run_callbacks callback
138
+ record.send callback
139
+ record.errors.empty?
126
140
  end
127
141
 
128
142
 
129
143
  def state_name
130
- record.read_attribute self.class.state_field
144
+ record.read_attribute(self.class.state_field).to_sym
131
145
  end
132
146
 
133
147
 
@@ -141,36 +155,38 @@ module Hobo
141
155
  end
142
156
 
143
157
 
144
- def available_transitions_for(user, name=nil, attributes=nil)
158
+ def available_transitions_for(user, name=nil)
159
+ name = name.to_sym if name
145
160
  matches = available_transitions
146
- matches = matches.select { |t| t.name == name.to_s } if name
147
- matches.select { |t| t.allowed?(record, user, attributes) }
161
+ matches = matches.select { |t| t.name == name } if name
162
+ record.with_acting_user(user) do
163
+ matches.select { |t| t.can_run?(record) }
164
+ end
148
165
  end
149
166
 
150
167
 
151
168
  def become(state_name, validate=true)
152
- state_name = state_name.to_s
153
- if self.state != state_name
154
- record.write_attribute self.class.state_field, state_name
155
-
156
- if state_name == "destroy"
157
- record.destroy
169
+ state_name = state_name.to_sym
170
+ record.write_attribute self.class.state_field, state_name.to_s
171
+
172
+ if state_name == :destroy
173
+ record.destroy
174
+ true
175
+ else
176
+ s = self.class.states[state_name]
177
+ raise ArgumentError, "No such state '#{state_name}' for #{record.class.name}" unless s
178
+
179
+ if record.save(validate)
180
+ s.activate! record
181
+ self.active_step = nil # That's the end of this step
158
182
  true
159
183
  else
160
- s = self.class.states[state_name]
161
- raise ArgumentError, "No such state '#{state_name}' for #{record.class.name}" unless s
162
- if record.save(validate)
163
- s.activate! record
164
- self.active_step = nil # That's the end of this step
165
- true
166
- else
167
- false
168
- end
184
+ false
169
185
  end
170
186
  end
171
187
  end
172
-
173
-
188
+
189
+
174
190
  def key_timestamp_field
175
191
  record.class::Lifecycle.options[:key_timestamp_field]
176
192
  end
@@ -205,8 +221,16 @@ module Hobo
205
221
  end
206
222
 
207
223
 
208
- def preconditions_satisfied?
209
- self.class.preconditions.all? { |i| record.instance_eval(&i) }
224
+ def active_step_is?(name)
225
+ active_step && active_step.name == name.to_sym
226
+ end
227
+
228
+ def method_missing(name, *args)
229
+ if name.to_s =~ /^(.*)_in_progress\?$/
230
+ active_step_is?($1)
231
+ else
232
+ super
233
+ end
210
234
  end
211
235
 
212
236
  end