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
@@ -2,13 +2,12 @@ module Hobo
2
2
 
3
3
  module Lifecycles
4
4
 
5
- class State < Struct.new(:name, :on_enter, :transitions_in, :transitions_out)
5
+ class State < Struct.new(:name, :on_enter, :transitions_out)
6
6
 
7
7
  include Actions
8
8
 
9
9
  def initialize(*args)
10
10
  super
11
- self.transitions_in = []
12
11
  self.transitions_out = []
13
12
  end
14
13
 
@@ -2,57 +2,51 @@ module Hobo
2
2
 
3
3
  module Lifecycles
4
4
 
5
- class Transition < Struct.new(:lifecycle, :name, :who, :start_states, :end_state, :on_transition, :options)
5
+ class Transition < Struct.new(:lifecycle, :name, :start_states, :end_state, :on_transition, :options)
6
6
 
7
7
  include Actions
8
8
 
9
9
 
10
10
  def initialize(*args)
11
11
  super
12
+ self.name = name.to_sym
12
13
  start_states.each do |from|
13
- state = lifecycle.states[from.to_s]
14
- raise ArgumentError, "No such state '#{from}' in #{'name'} transition (#{lifecycle.model.name})" unless state
14
+ state = lifecycle.states[from]
15
+ raise ArgumentError, "No such state '#{from}' in #{name} transition (#{lifecycle.model.name})" unless state
15
16
  state.transitions_out << self
16
17
  end
17
- unless end_state.to_s == "destroy"
18
- state = lifecycle.states[end_state.to_s]
19
- raise ArgumentError, "No such state '#{end_state}' in '#{name}' transition (#{lifecycle.model.name})" unless state
20
- state.transitions_in << self
21
- end
22
18
  lifecycle.transitions << self
23
19
  end
24
20
 
25
21
 
26
- def allowed?(record, user, attributes=nil)
27
- prepare_and_check!(record, user, attributes) && true
28
- end
29
-
30
-
31
22
  def extract_attributes(attributes)
32
23
  update_attributes = options.fetch(:update, [])
33
24
  attributes & update_attributes
34
25
  end
26
+
27
+
28
+ def change_state(record)
29
+ record.lifecycle.become(get_state(record, end_state))
30
+ end
35
31
 
36
32
 
37
33
  def run!(record, user, attributes)
38
- if prepare_and_check!(record, user, attributes)
39
- if record.lifecycle.become end_state
40
- fire_event(record, on_transition)
41
- end
42
- else
43
- raise Hobo::Model::PermissionDeniedError
34
+ current_state = record.lifecycle.state_name
35
+ unless start_states.include?(current_state)
36
+ raise Hobo::Lifecycles::LifecycleError, "Transition #{record.class}##{name} cannot be run from the '#{current_state}' state"
44
37
  end
45
- end
46
-
47
-
48
- def set_or_check_who_with_key!(record, user)
49
- if who == :with_key
50
- record.lifecycle.valid_key? or raise LifecycleKeyError
51
- else
52
- set_or_check_who_without_key!(record, user)
38
+ record.lifecycle.active_step = self
39
+ record.with_acting_user(user) do
40
+ prepare!(record, attributes)
41
+ if can_run?(record)
42
+ if change_state(record)
43
+ fire_event(record, on_transition)
44
+ end
45
+ else
46
+ raise Hobo::PermissionDeniedError
47
+ end
53
48
  end
54
49
  end
55
- alias_method_chain :set_or_check_who!, :key
56
50
 
57
51
 
58
52
  def parameters
@@ -2,7 +2,6 @@ module Hobo
2
2
 
3
3
  module Model
4
4
 
5
- class PermissionDeniedError < RuntimeError; end
6
5
  class NoNameError < RuntimeError; end
7
6
 
8
7
  NAME_FIELD_GUESS = %w(name title)
@@ -13,8 +12,6 @@ module Hobo
13
12
  def self.included(base)
14
13
  base.extend(ClassMethods)
15
14
 
16
- included_in_class_callbacks(base)
17
-
18
15
  register_model(base)
19
16
 
20
17
  patch_will_paginate
@@ -23,18 +20,15 @@ module Hobo
23
20
  inheriting_cattr_reader :default_order
24
21
  alias_method_chain :attributes=, :hobo_type_conversion
25
22
 
26
- attr_accessor :acting_user, :origin, :origin_attribute
27
-
28
- bool_attr_accessor :exempt_from_edit_checks
29
-
23
+ include Hobo::Permissions
30
24
  include Hobo::Lifecycles::ModelExtensions
31
25
  include Hobo::FindFor
32
- include Hobo::MassAssignment
26
+ include Hobo::AccessibleAssociations
33
27
  end
34
28
 
35
29
  class << base
36
30
  alias_method_chain :belongs_to, :creator_metadata
37
- alias_method_chain :belongs_to, :target_is
31
+ alias_method_chain :belongs_to, :test_methods
38
32
  alias_method_chain :attr_accessor, :creator_metadata
39
33
 
40
34
  alias_method_chain :has_one, :new_method
@@ -49,10 +43,12 @@ module Hobo
49
43
  end
50
44
 
51
45
  base.fields # force hobofields to load
46
+
47
+ included_in_class_callbacks(base)
52
48
  end
53
49
 
54
50
  def self.patch_will_paginate
55
- if defined?(WillPaginate) && !WillPaginate::Collection.respond_to?(:member_class)
51
+ if defined?(WillPaginate::Collection) && !WillPaginate::Collection.respond_to?(:member_class)
56
52
 
57
53
  WillPaginate::Collection.class_eval do
58
54
  attr_accessor :member_class, :origin, :origin_attribute
@@ -92,16 +88,45 @@ module Hobo
92
88
  # ...but only return the ones that registered themselves
93
89
  @model_names.*.constantize
94
90
  end
91
+
92
+
93
+ def self.find_by_typed_id(typed_id)
94
+ return nil if typed_id == 'nil'
95
+
96
+ _, name, id, attr = *typed_id.match(/^([^:]+)(?::([^:]+)(?::([^:]+))?)?$/)
97
+ raise ArgumentError.new("invalid typed-id: #{typed_id}") unless name
98
+
99
+ model_class = name.camelize.safe_constantize or raise ArgumentError.new("no such class in typed-id: #{typed_id}")
100
+ return nil unless model_class
101
+
102
+ if id
103
+ obj = model_class.find(id)
104
+ # Optimise: can we use eager loading in the situation where the attr is a belongs_to?
105
+ # We used to, but hit a bug in AR
106
+ attr ? obj.send(attr) : obj
107
+ else
108
+ model_class
109
+ end
110
+ end
95
111
 
96
112
 
97
113
  def self.enable
114
+ require 'active_record/association_collection'
115
+ require 'active_record/association_proxy'
116
+ require 'active_record/association_reflection'
117
+
98
118
  ActiveRecord::Base.class_eval do
99
119
  def self.hobo_model
100
120
  include Hobo::Model
101
121
  fields # force hobofields to load
102
122
  end
103
-
123
+ def self.hobo_user_model
124
+ include Hobo::Model
125
+ include Hobo::User
126
+ end
104
127
  alias_method :has_hobo_method?, :respond_to_without_attributes?
128
+
129
+ Hobo::Permissions.enable
105
130
  end
106
131
  end
107
132
 
@@ -131,45 +156,6 @@ module Hobo
131
156
  end
132
157
 
133
158
 
134
- def user_find(user, *args)
135
- record = find(*args)
136
- yield(record) if block_given?
137
- raise PermissionDeniedError unless Hobo.can_view?(user, record)
138
- record
139
- end
140
-
141
-
142
- def user_new(user, attributes={})
143
- record = new(attributes) {|r| r.acting_user = user; yield if block_given? }
144
- allowed = record.user_changes(user)
145
- record.acting_user = nil
146
- allowed && record
147
- end
148
-
149
-
150
- def user_new!(user, attributes={})
151
- user_new(user, attributes) or raise PermissionDeniedError
152
- end
153
-
154
-
155
- def user_create(user, attributes={})
156
- record = new(attributes)
157
- record.user_save_changes(user)
158
- record
159
- end
160
-
161
-
162
- def user_can_create?(user, attributes={})
163
- record = new(attributes)
164
- record.user_changes(user)
165
- end
166
-
167
-
168
- def user_update(user, id, attributes={})
169
- find(id).user_save_changes(user, attributes)
170
- end
171
-
172
-
173
159
  def name_attribute
174
160
  @name_attribute ||= begin
175
161
  names = columns.*.name + public_instance_methods
@@ -212,20 +198,26 @@ module Hobo
212
198
  belongs_to_without_creator_metadata(name, options, &block)
213
199
  end
214
200
 
215
- def belongs_to_with_target_is(name, options={}, &block)
216
- belongs_to_without_target_is(name, options, &block)
201
+ def belongs_to_with_test_methods(name, options={}, &block)
202
+ belongs_to_without_test_methods(name, options, &block)
217
203
  refl = reflections[name]
218
204
  if options[:polymorphic]
219
205
  class_eval %{
220
206
  def #{name}_is?(target)
221
207
  target.id == self.#{refl.primary_key_name} && target.class.name == self.#{refl.options[:foreign_type]}
222
208
  end
209
+ def #{name}_changed?
210
+ #{refl.primary_key_name}_changed? || #{refl.options[:foreign_type]}_changed?
211
+ end
223
212
  }
224
213
  else
225
214
  class_eval %{
226
215
  def #{name}_is?(target)
227
216
  target.id == self.#{refl.primary_key_name}
228
217
  end
218
+ def #{name}_changed?
219
+ #{refl.primary_key_name}_changed?
220
+ end
229
221
  }
230
222
  end
231
223
  end
@@ -341,15 +333,17 @@ module Hobo
341
333
  r.source_reflection == join_to_self
342
334
  end
343
335
  else
344
- # Find the :belongs_to that corresponds to a :has_many or vice versa
336
+ # Find the :belongs_to that corresponds to a :has_one / :has_many or vice versa
337
+
338
+ reverse_macros = case refl.macro
339
+ when :has_many, :has_one
340
+ [:belongs_to]
341
+ when :belongs_to
342
+ [:has_many, :has_one]
343
+ end
345
344
 
346
- reverse_macro = if refl.macro == :has_many
347
- :belongs_to
348
- elsif refl.macro == :belongs_to
349
- :has_many
350
- end
351
345
  refl.klass.reflections.values.find do |r|
352
- r.macro == reverse_macro &&
346
+ r.macro.in?(reverse_macros) &&
353
347
  r.klass == self &&
354
348
  !r.options[:conditions] &&
355
349
  r.primary_key_name == refl.primary_key_name
@@ -376,7 +370,7 @@ module Hobo
376
370
  end
377
371
 
378
372
 
379
- def respond_to?(method)
373
+ def respond_to?(method, include_private=false)
380
374
  super || create_automatic_scope(method)
381
375
  end
382
376
 
@@ -396,6 +390,12 @@ module Hobo
396
390
  def typed_id
397
391
  HoboFields.to_name(self) || name.underscore.gsub("/", "__")
398
392
  end
393
+
394
+
395
+ def view_hints
396
+ class_name = "#{name}Hints"
397
+ class_name.safe_constantize or Object.class_eval("class #{class_name} < Hobo::ViewHints; end; #{class_name}")
398
+ end
399
399
 
400
400
 
401
401
  end # --- of ClassMethods --- #
@@ -411,8 +411,8 @@ module Hobo
411
411
 
412
412
  def to_param
413
413
  name_attr = self.class.name_attribute and name = send(name_attr)
414
- if name_attr && !name.blank?
415
- readable = name.to_s.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/-+$/, '').gsub(/^-+$/, '').split('-')[0..5].join('-')
414
+ if name_attr && !name.blank? && id.is_a?(Fixnum)
415
+ readable = name.to_s.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/-+$/, '').gsub(/^-+/, '').split('-')[0..5].join('-')
416
416
  @to_param ||= "#{id}-#{readable}"
417
417
  else
418
418
  id.to_s
@@ -420,73 +420,6 @@ module Hobo
420
420
  end
421
421
 
422
422
 
423
- def with_acting_user(user)
424
- old = acting_user
425
- self.acting_user = user
426
- result = yield
427
- self.acting_user = old
428
- result
429
- end
430
-
431
-
432
- def user_changes(user, changes={})
433
- with_acting_user user do
434
- if new_record?
435
- self.attributes = changes
436
- set_creator(user)
437
- Hobo.can_create?(user, self)
438
- else
439
- original = duplicate
440
- # 'duplicate' can cause these to be set, but they can conflict
441
- # with the changes so we clear them
442
- clear_aggregation_cache
443
- clear_association_cache
444
-
445
- self.attributes = changes
446
-
447
- Hobo.can_update?(user, original, self)
448
- end
449
- end
450
- end
451
-
452
-
453
- def user_changes!(user, changes={})
454
- user_changes(user, changes) or raise PermissionDeniedError
455
- end
456
-
457
-
458
- def user_can_create?(user, attributes={})
459
- raise ArgumentError, "Called #user_can_create? on existing record" unless new_record?
460
- user_changes(user, attributes)
461
- end
462
-
463
-
464
- def user_save_changes(user, changes={})
465
- with_acting_user user do
466
- user_changes!(user, changes)
467
- save
468
- end
469
- end
470
-
471
-
472
- def user_save(user)
473
- user_save_changes(user)
474
- end
475
-
476
-
477
- def user_view(user, field=nil)
478
- raise PermissionDeniedError, self.inspect unless Hobo.can_view?(user, self, field)
479
- end
480
-
481
-
482
- def user_destroy(user)
483
- with_acting_user user do
484
- raise PermissionDeniedError unless Hobo.can_delete?(user, self)
485
- destroy
486
- end
487
- end
488
-
489
-
490
423
  def dependent_on
491
424
  self.class.dependent_on.map { |assoc| send(assoc) }
492
425
  end
@@ -567,7 +500,7 @@ module Hobo
567
500
 
568
501
 
569
502
  def typed_id
570
- "#{self.class.name.underscore}_#{self.id}" if id
503
+ "#{self.class.name.underscore}:#{self.id}" if id
571
504
  end
572
505
 
573
506
 
@@ -583,19 +516,8 @@ module Hobo
583
516
  private
584
517
 
585
518
 
586
- def parse_datetime(s)
587
- defined?(Chronic) ? Chronic.parse(s) : Time.parse(s)
588
- end
589
-
590
-
591
519
  def convert_type_for_mass_assignment(field_type, value)
592
- if field_type.is_a?(Class) && field_type < ActiveRecord::Base
593
- convert_record_reference_for_mass_assignment(field_type, value)
594
-
595
- elsif field_type.is_a?(ActiveRecord::Reflection::AssociationReflection)
596
- convert_collection_for_mass_assignment(field_type, value)
597
-
598
- elsif !field_type.is_a?(Class)
520
+ if !field_type.is_a?(Class)
599
521
  value
600
522
 
601
523
  elsif field_type <= Date
@@ -606,9 +528,6 @@ module Hobo
606
528
  else
607
529
  Date.new(*parts)
608
530
  end
609
- elsif value.is_a? String
610
- dt = parse_datetime(value)
611
- dt && dt.to_date
612
531
  else
613
532
  value
614
533
  end
@@ -616,8 +535,6 @@ module Hobo
616
535
  elsif field_type <= Time || field_type <= ActiveSupport::TimeWithZone
617
536
  if value.is_a? Hash
618
537
  Time.local(*(%w{year month day hour minute}.map{|s| value[s].to_i}))
619
- elsif value.is_a? String
620
- parse_datetime(value)
621
538
  else
622
539
  value
623
540
  end
@@ -632,35 +549,6 @@ module Hobo
632
549
  end
633
550
 
634
551
 
635
- def convert_record_reference_for_mass_assignment(klass, value)
636
- if value.is_a?(String)
637
- if value.starts_with?('@')
638
- # TODO: This @foo_1 feature is rarely (never?) used - get rid of it
639
- Hobo.object_from_dom_id(value[1..-1])
640
- else
641
- klass.named(value)
642
- end
643
- else
644
- value
645
- end
646
- end
647
-
648
-
649
- def convert_collection_for_mass_assignment(reflection, value)
650
- klass = reflection.klass
651
- if klass.try.name_attribute && value.is_a?(Array)
652
- value.map do |x|
653
- if x.is_a?(String)
654
- klass.named(x) unless x.blank?
655
- else
656
- x
657
- end
658
- end.compact
659
- else
660
- value
661
- end
662
- end
663
-
664
552
  end
665
553
 
666
554
  end