hobo 0.8.2 → 0.8.3

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 (41) hide show
  1. data/CHANGES.txt +131 -0
  2. data/Manifest +1 -2
  3. data/Rakefile +3 -3
  4. data/dryml_generators/rapid/cards.dryml.erb +1 -1
  5. data/dryml_generators/rapid/forms.dryml.erb +3 -1
  6. data/dryml_generators/rapid/pages.dryml.erb +8 -5
  7. data/hobo.gemspec +8 -8
  8. data/lib/active_record/association_collection.rb +5 -2
  9. data/lib/active_record/association_reflection.rb +14 -5
  10. data/lib/hobo.rb +1 -1
  11. data/lib/hobo/controller.rb +6 -5
  12. data/lib/hobo/dryml.rb +4 -4
  13. data/lib/hobo/dryml/taglib.rb +7 -3
  14. data/lib/hobo/dryml/template_environment.rb +7 -7
  15. data/lib/hobo/hobo_helper.rb +31 -15
  16. data/lib/hobo/include_in_save.rb +9 -1
  17. data/lib/hobo/lifecycles.rb +0 -7
  18. data/lib/hobo/lifecycles/creator.rb +1 -1
  19. data/lib/hobo/lifecycles/lifecycle.rb +8 -3
  20. data/lib/hobo/mass_assignment.rb +64 -0
  21. data/lib/hobo/model.rb +41 -17
  22. data/lib/hobo/model_controller.rb +76 -25
  23. data/lib/hobo/model_router.rb +10 -13
  24. data/lib/hobo/user.rb +13 -13
  25. data/rails_generators/hobo_model/hobo_model_generator.rb +4 -0
  26. data/rails_generators/hobo_model/templates/model.rb +1 -1
  27. data/rails_generators/hobo_rapid/hobo_rapid_generator.rb +0 -2
  28. data/rails_generators/hobo_rapid/templates/hobo-rapid.js +180 -43
  29. data/rails_generators/hobo_rapid/templates/themes/clean/public/stylesheets/clean.css +25 -4
  30. data/rails_generators/hobo_rapid/templates/themes/clean/public/stylesheets/rapid-ui.css +0 -2
  31. data/taglibs/core.dryml +8 -5
  32. data/taglibs/rapid.dryml +9 -5
  33. data/taglibs/rapid_document_tags.dryml +1 -1
  34. data/taglibs/rapid_editing.dryml +1 -1
  35. data/taglibs/rapid_forms.dryml +108 -32
  36. data/taglibs/rapid_generics.dryml +2 -2
  37. data/taglibs/rapid_lifecycles.dryml +0 -18
  38. data/taglibs/rapid_user_pages.dryml +8 -41
  39. metadata +7 -7
  40. data/rails_generators/hobo_rapid/templates/nicEditorIcons.gif +0 -0
  41. data/rails_generators/hobo_rapid/templates/nicedit.js +0 -91
@@ -61,7 +61,9 @@ module Hobo
61
61
  if obj.respond_to?(:member_class)
62
62
  # Asking for URL of a collection, e.g. category/1/adverts or category/1/adverts/new
63
63
 
64
- owner_name = obj.origin.class.reverse_reflection(obj.origin_attribute).name.to_s
64
+ refl = obj.origin.class.reverse_reflection(obj.origin_attribute)
65
+ owner_name = refl.name.to_s
66
+ owner_name = owner_name.singularize if refl.macro == :has_many
65
67
  if action == :new
66
68
  action_path = "#{obj.origin_attribute}/new"
67
69
  action = :"new_for_#{owner_name}"
@@ -154,25 +156,39 @@ module Hobo
154
156
  # TODO: Calls to respond_to? in here can cause the full collection hiding behind a scoped collection to get loaded
155
157
  res = []
156
158
  empty = true
157
- if enum.respond_to?(:each_pair)
158
- enum.each_pair do |key, value|
159
- empty = false;
160
- self.this_key = key;
161
- new_object_context(value) { res << yield }
162
- end
163
- else
164
- enum.each do |e|
165
- empty = false;
166
- if e.respond_to?(:new_record?) && !e.new_record?
167
- new_field_context(e.id.to_s, e) { res << yield }
168
- else
169
- new_object_context(e) { res << yield }
159
+ scope.new_scope(:repeat_collection => enum) do
160
+ if enum.respond_to?(:each_pair)
161
+ enum.each_pair do |key, value|
162
+ empty = false;
163
+ self.this_key = key;
164
+ new_object_context(value) { res << yield }
165
+ end
166
+ else
167
+ index = 0
168
+ enum.each do |e|
169
+ empty = false;
170
+ if enum == this
171
+ new_field_context(index, e) { res << yield }
172
+ else
173
+ new_object_context(e) { res << yield }
174
+ end
175
+ index += 1
170
176
  end
171
177
  end
178
+ Dryml.last_if = !empty
172
179
  end
173
- Dryml.last_if = !empty
174
180
  res
175
181
  end
182
+
183
+
184
+ def first_item?
185
+ this == scope.repeat_collection.first
186
+ end
187
+
188
+
189
+ def last_item?
190
+ this == scope.repeat_collection.last
191
+ end
176
192
 
177
193
 
178
194
  def comma_split(x)
@@ -16,13 +16,21 @@ module Hobo
16
16
  def validate_included_in_save
17
17
  if included_in_save
18
18
  included_in_save._?.each_pair do |association, records|
19
+ added = false
19
20
  records.each do |record|
20
- errors.add association, "is invalid" unless record.valid?
21
+ # we want to call valid? on each one, but only add the error to self once
22
+ unless record.valid?
23
+ unless added
24
+ errors.add association, "..."
25
+ added = true
26
+ end
27
+ end
21
28
  end
22
29
  end
23
30
  end
24
31
  end
25
32
 
33
+
26
34
  def save_included
27
35
  if included_in_save
28
36
  included_in_save.each_pair do |association, records|
@@ -1,5 +1,3 @@
1
- %w[lifecycle actions creator state transition].each { |lib| require "hobo/lifecycles/#{lib}" }
2
-
3
1
  module Hobo
4
2
 
5
3
  module Lifecycles
@@ -47,11 +45,6 @@ module Hobo
47
45
  @lifecycle ||= self.class::Lifecycle.new(self)
48
46
  end
49
47
 
50
-
51
- def become(state)
52
- self.lifecycle.become state
53
- end
54
-
55
48
  end
56
49
 
57
50
 
@@ -50,7 +50,7 @@ module Hobo
50
50
 
51
51
  def change_state(record)
52
52
  state = options[:become]
53
- record.become(state) if state
53
+ record.lifecycle.become(state) if state
54
54
  end
55
55
 
56
56
 
@@ -27,7 +27,7 @@ module Hobo
27
27
  name = name.to_s
28
28
  returning(State.new(name, on_enter)) do |s|
29
29
  states[name] = s
30
- class_eval "def #{name}?; record.state == '#{name}'; end"
30
+ class_eval "def #{name}_state?; state_name == '#{name}' end"
31
31
  end
32
32
  end
33
33
 
@@ -74,7 +74,10 @@ module Hobo
74
74
 
75
75
 
76
76
  def self.create(name, user, attributes=nil)
77
- creators[name.to_s].run!(user, attributes)
77
+ creator = creators[name.to_s]
78
+ record = creator.run!(user, attributes)
79
+ record.lifecycle.active_step = creator
80
+ record
78
81
  end
79
82
 
80
83
 
@@ -97,7 +100,7 @@ module Hobo
97
100
 
98
101
  attr_reader :record
99
102
 
100
- attr_accessor :provided_key
103
+ attr_accessor :provided_key, :active_step
101
104
 
102
105
 
103
106
  def initialize(record)
@@ -112,6 +115,7 @@ module Hobo
112
115
 
113
116
  def transition(name, user, attributes=nil)
114
117
  transition = find_transition(name, user, attributes)
118
+ self.active_step = transition
115
119
  transition.run!(record, user, attributes)
116
120
  end
117
121
 
@@ -157,6 +161,7 @@ module Hobo
157
161
  raise ArgumentError, "No such state '#{state_name}' for #{record.class.name}" unless s
158
162
  if record.save(validate)
159
163
  s.activate! record
164
+ self.active_step = nil # That's the end of this step
160
165
  true
161
166
  else
162
167
  false
@@ -0,0 +1,64 @@
1
+ module Hobo
2
+
3
+ MassAssignment = classy_module do
4
+
5
+ include IncludeInSave
6
+
7
+
8
+ # --- has_many mass assignment support --- #
9
+
10
+ def self.has_many_with_mass_assignment(name, options={}, &block)
11
+ accessible = options.delete(:accessible)
12
+ has_many_without_mass_assignment(name, options, &block)
13
+
14
+ if accessible
15
+ class_eval %{
16
+ def #{name}_with_mass_assignment=(array_or_hash)
17
+ items = prepare_has_many_assignment(:#{name}, array_or_hash)
18
+ self.#{name}_without_mass_assignment = items
19
+ # ensure the loaded array contains any changed records
20
+ self.#{name}.proxy_target[0..-1] = items
21
+ end}, __FILE__, __LINE__ - 3
22
+ alias_method_chain :"#{name}=", :mass_assignment
23
+ end
24
+ end
25
+ metaclass.alias_method_chain :has_many, :mass_assignment
26
+
27
+
28
+ def prepare_has_many_assignment(association_name, array_or_hash)
29
+ association = send(association_name)
30
+
31
+ array = params_hash_to_array(array_or_hash)
32
+ array.map do |record_or_hash|
33
+ if record_or_hash.is_a?(Hash)
34
+ hash = record_or_hash
35
+
36
+ id = hash.delete(:id)
37
+ record = if id
38
+ association.find(id) # TODO: We don't really want to find these one by one
39
+ else
40
+ # Remove completely blank hashes
41
+ next if hash.values.join.blank?
42
+
43
+ record = association.build
44
+ end
45
+ record.attributes = hash
46
+ include_in_save(association_name, record)
47
+ else
48
+ record = record_or_hash
49
+ end
50
+ record
51
+
52
+ end.compact
53
+
54
+ end
55
+
56
+ def params_hash_to_array(array_or_hash)
57
+ if array_or_hash.is_a?(Hash)
58
+ array = array_or_hash.get(*array_or_hash.keys.sort_by(&:to_i))
59
+ else
60
+ array_or_hash
61
+ end
62
+ end
63
+ end
64
+ end
@@ -29,7 +29,7 @@ module Hobo
29
29
 
30
30
  include Hobo::Lifecycles::ModelExtensions
31
31
  include Hobo::FindFor
32
- include Hobo::IncludeInSave
32
+ include Hobo::MassAssignment
33
33
  end
34
34
 
35
35
  class << base
@@ -246,7 +246,7 @@ module Hobo
246
246
 
247
247
 
248
248
  def has_one_with_new_method(name, options={}, &block)
249
- has_one_without_new_method(name, options)
249
+ has_one_without_new_method(name, options, &block)
250
250
  class_eval "def new_#{name}(attributes={}); build_#{name}(attributes, false); end"
251
251
  end
252
252
 
@@ -321,21 +321,39 @@ module Hobo
321
321
  end
322
322
 
323
323
 
324
- # FIXME: This should really be a method on AssociationReflection
325
324
  def reverse_reflection(association_name)
326
325
  refl = reflections[association_name.to_sym] or raise "No reverse reflection for #{name}.#{association_name}"
327
326
  return nil if refl.options[:conditions] || refl.options[:polymorphic]
328
-
329
- reverse_macro = if refl.macro == :has_many
330
- :belongs_to
331
- elsif refl.macro == :belongs_to
332
- :has_many
333
- end
334
- refl.klass.reflections.values.find do |r|
335
- r.macro == reverse_macro &&
336
- r.klass == self &&
337
- !r.options[:conditions] &&
338
- r.primary_key_name == refl.primary_key_name
327
+
328
+ if refl.macro == :has_many && (self_to_join = refl.through_reflection)
329
+ # Find the reverse of a has_many :through (another has_many :through)
330
+
331
+ join_to_self = reverse_reflection(self_to_join.name)
332
+ join_to_other = refl.source_reflection
333
+ other_to_join = self_to_join.klass.reverse_reflection(join_to_other.name)
334
+
335
+ return nil if self_to_join.options[:conditions] || join_to_other.options[:conditions]
336
+
337
+ refl.klass.reflections.values.find do |r|
338
+ r.macro == :has_many &&
339
+ !r.options[:conditions] &&
340
+ r.through_reflection == other_to_join &&
341
+ r.source_reflection == join_to_self
342
+ end
343
+ else
344
+ # Find the :belongs_to that corresponds to a :has_many or vice versa
345
+
346
+ reverse_macro = if refl.macro == :has_many
347
+ :belongs_to
348
+ elsif refl.macro == :belongs_to
349
+ :has_many
350
+ end
351
+ refl.klass.reflections.values.find do |r|
352
+ r.macro == reverse_macro &&
353
+ r.klass == self &&
354
+ !r.options[:conditions] &&
355
+ r.primary_key_name == refl.primary_key_name
356
+ end
339
357
  end
340
358
  end
341
359
 
@@ -385,6 +403,7 @@ module Hobo
385
403
 
386
404
  include Scopes
387
405
 
406
+
388
407
  def to_url_path
389
408
  "#{self.class.to_url_path}/#{to_param}" unless new_record?
390
409
  end
@@ -396,7 +415,7 @@ module Hobo
396
415
  readable = name.to_s.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/-+$/, '').gsub(/^-+$/, '').split('-')[0..5].join('-')
397
416
  @to_param ||= "#{id}-#{readable}"
398
417
  else
399
- id
418
+ id.to_s
400
419
  end
401
420
  end
402
421
 
@@ -581,7 +600,12 @@ module Hobo
581
600
 
582
601
  elsif field_type <= Date
583
602
  if value.is_a? Hash
584
- Date.new(*(%w{year month day}.map{|s| value[s].to_i}))
603
+ parts = %w{year month day}.map{|s| value[s].to_i}
604
+ if parts.include?(0)
605
+ nil
606
+ else
607
+ Date.new(*parts)
608
+ end
585
609
  elsif value.is_a? String
586
610
  dt = parse_datetime(value)
587
611
  dt && dt.to_date
@@ -623,7 +647,7 @@ module Hobo
623
647
 
624
648
 
625
649
  def convert_collection_for_mass_assignment(reflection, value)
626
- klass = reflection.safe_class
650
+ klass = reflection.klass
627
651
  if klass.try.name_attribute && value.is_a?(Array)
628
652
  value.map do |x|
629
653
  if x.is_a?(String)
@@ -26,6 +26,7 @@ module Hobo
26
26
 
27
27
  helper_method :model, :current_user
28
28
  before_filter :set_no_cache_headers
29
+ after_filter :remember_page_path
29
30
 
30
31
  rescue_from ActiveRecord::RecordNotFound, :with => :not_found
31
32
 
@@ -82,14 +83,19 @@ module Hobo
82
83
  module ClassMethods
83
84
 
84
85
  attr_writer :model
85
-
86
+
87
+ def model_name
88
+ name.sub(/Controller$/, "").singularize
89
+ end
90
+
86
91
  def model
87
- @model ||= name.sub(/Controller$/, "").singularize.constantize
92
+ @model ||= model_name.constantize
88
93
  end
89
94
 
90
95
 
91
- def autocomplete(name, options={}, &block)
92
- options = options.dup
96
+ def autocomplete(*args, &block)
97
+ options = args.extract_options!
98
+ name = args.first || model.name_attribute
93
99
  field = options.delete(:field) || name
94
100
  if block
95
101
  index_action "complete_#{name}", &block
@@ -223,19 +229,46 @@ module Hobo
223
229
  end
224
230
 
225
231
 
232
+ def creator_page_action(name, options={}, &block)
233
+ define_method(name) do
234
+ creator_page_action name, options, &block
235
+ end
236
+ end
237
+
238
+
239
+ def do_creator_action(name, options={}, &block)
240
+ define_method("do_#{name}") do
241
+ do_creator_action name, options, &block
242
+ end
243
+ end
244
+
245
+
246
+ def transtion_page_action(name, options={}, &block)
247
+ define_method(name) do
248
+ transtion_page_action name, options, &block
249
+ end
250
+ end
251
+
252
+
253
+ def do_transition_action(name, options={}, &block)
254
+ define_method("do_#{name}") do
255
+ do_transition_action name, options, &block
256
+ end
257
+ end
258
+
259
+
226
260
  def auto_actions_for(owner, actions)
227
- model.reverse_reflection(owner)._?.klass == model or
228
- raise ArgumentError, "Invalid owner association '#{owner}' for #{model}"
261
+ name = model.reflections[owner].macro == :has_many ? owner.to_s.singularize : owner
229
262
 
230
263
  owner_actions[owner] ||= []
231
264
  Array(actions).each do |action|
232
265
  case action
233
266
  when :new
234
- define_method("new_for_#{owner}") { hobo_new_for owner }
267
+ define_method("new_for_#{name}") { hobo_new_for owner }
235
268
  when :index
236
- define_method("index_for_#{owner}") { hobo_index_for owner }
269
+ define_method("index_for_#{name}") { hobo_index_for owner }
237
270
  when :create
238
- define_method("create_for_#{owner}") { hobo_create_for owner }
271
+ define_method("create_for_#{name}") { hobo_create_for owner }
239
272
  else
240
273
  raise ArgumentError, "Invalid owner action: #{action}"
241
274
  end
@@ -332,13 +365,11 @@ module Hobo
332
365
  end
333
366
 
334
367
 
335
- def destination_after_submit(record=nil, destroyed=false)
336
- record ||= this
337
-
368
+ def destination_after_submit(record=this, destroyed=false)
338
369
  after_submit = params[:after_submit]
339
370
 
340
371
  # The after_submit post parameter takes priority
341
- (after_submit == "stay-here" ? :back : after_submit) ||
372
+ (after_submit == "stay-here" ? session[:previous_page_path] : after_submit) ||
342
373
 
343
374
  # Then try the record's show page
344
375
  (!destroyed && object_url(@this)) ||
@@ -355,7 +386,18 @@ module Hobo
355
386
 
356
387
 
357
388
  def redirect_after_submit(*args)
358
- redirect_to destination_after_submit(*args)
389
+ options = args.extract_options!
390
+ o = options[:redirect]
391
+ if o
392
+ url = if o.is_a?(Symbol)
393
+ object_url(this, o)
394
+ else
395
+ object_url(*Array(o))
396
+ end
397
+ redirect_to url
398
+ else
399
+ redirect_to destination_after_submit(*args)
400
+ end
359
401
  end
360
402
 
361
403
 
@@ -395,7 +437,7 @@ module Hobo
395
437
  klass = refl.klass
396
438
  id = params["#{klass.name.underscore}_id"]
397
439
  owner = klass.find(id)
398
- instance_variable_set("@#{owner_association}", owner)
440
+ instance_variable_set("@#{owner.class.name.underscore}", owner)
399
441
  [owner, owner.send(model.reverse_reflection(owner_association).name)]
400
442
  end
401
443
 
@@ -475,13 +517,13 @@ module Hobo
475
517
 
476
518
 
477
519
 
478
- def create_response(new_action, &b)
520
+ def create_response(new_action, options={}, &b)
479
521
  flash[:notice] = "The #{@this.class.name.titleize.downcase} was created successfully" if !request.xhr? && valid?
480
522
 
481
523
  response_block(&b) or
482
524
  if valid?
483
525
  respond_to do |wants|
484
- wants.html { redirect_after_submit }
526
+ wants.html { redirect_after_submit(options) }
485
527
  wants.js { hobo_ajax_response || render(:nothing => true) }
486
528
  end
487
529
  else
@@ -506,18 +548,18 @@ module Hobo
506
548
  @current_user = @this if @this == current_user
507
549
 
508
550
  in_place_edit_field = changes.keys.first if changes.size == 1 && params[:render]
509
- update_response(in_place_edit_field, &b)
551
+ update_response(in_place_edit_field, options, &b)
510
552
  end
511
553
 
512
554
 
513
- def update_response(in_place_edit_field=nil, &b)
555
+ def update_response(in_place_edit_field=nil, options={}, &b)
514
556
  flash[:notice] = "Changes to the #{@this.class.name.titleize.downcase} were saved" if !request.xhr? && valid?
515
557
 
516
558
  response_block(&b) or
517
559
  if valid?
518
560
  respond_to do |wants|
519
561
  wants.html do
520
- redirect_after_submit
562
+ redirect_after_submit options
521
563
  end
522
564
  wants.js do
523
565
  if in_place_edit_field
@@ -552,10 +594,10 @@ module Hobo
552
594
  end
553
595
 
554
596
 
555
- def destroy_response(&b)
597
+ def destroy_response(options={}, &b)
556
598
  response_block(&b) or
557
599
  respond_to do |wants|
558
- wants.html { redirect_after_submit(this, true) }
600
+ wants.html { redirect_after_submit(this, true, options) }
559
601
  wants.js { hobo_ajax_response || render(:nothing => true) }
560
602
  end
561
603
  end
@@ -563,12 +605,12 @@ module Hobo
563
605
 
564
606
  # --- Lifecycle Actions --- #
565
607
 
566
- def do_creator_action(name, &b)
608
+ def do_creator_action(name, options={}, &b)
567
609
  @creator = model::Lifecycle.creators[name.to_s]
568
610
  self.this = @creator.run!(current_user, attribute_parameters)
569
611
  response_block(&b) or
570
612
  if valid?
571
- redirect_after_submit
613
+ redirect_after_submit options
572
614
  else
573
615
  re_render_form(name)
574
616
  end
@@ -577,6 +619,7 @@ module Hobo
577
619
 
578
620
  def creator_page_action(name)
579
621
  self.this = model.new
622
+ this.exempt_from_edit_checks = true
580
623
  @creator = model::Lifecycle.creators[name.to_s] or raise ArgumentError, "No such creator in lifecycle: #{name}"
581
624
  end
582
625
 
@@ -592,11 +635,12 @@ module Hobo
592
635
 
593
636
 
594
637
  def do_transition_action(name, *args, &b)
638
+ options = args.extract_options!
595
639
  prepare_for_transition(name)
596
640
  @transition.run!(this, current_user, attribute_parameters)
597
641
  response_block(&b) or
598
642
  if valid?
599
- redirect_after_submit
643
+ redirect_after_submit options
600
644
  else
601
645
  re_render_form(name)
602
646
  end
@@ -705,6 +749,13 @@ module Hobo
705
749
  headers["Expires"] ='0'
706
750
  end
707
751
 
752
+ def remember_page_path
753
+ if request.method == :get
754
+ session[:previous_page_path] = request.path
755
+ session[:previous_page_path] += "?#{request.query_string}" unless request.query_string.blank?
756
+ end
757
+ end
758
+
708
759
  # --- end filters --- #
709
760
 
710
761
  public