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