hobo 0.6.1 → 0.6.2

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 (47) hide show
  1. data/bin/hobo +3 -2
  2. data/hobo_files/plugin/CHANGES.txt +299 -2
  3. data/hobo_files/plugin/Rakefile +12 -10
  4. data/hobo_files/plugin/generators/hobo/templates/guest.rb +1 -13
  5. data/hobo_files/plugin/generators/hobo_migration/hobo_migration_generator.rb +11 -7
  6. data/hobo_files/plugin/generators/hobo_rapid/hobo_rapid_generator.rb +1 -0
  7. data/hobo_files/plugin/generators/hobo_rapid/templates/hobo_rapid.js +1 -1
  8. data/hobo_files/plugin/generators/hobo_rapid/templates/lowpro.js +405 -0
  9. data/hobo_files/plugin/generators/hobo_rapid/templates/themes/default/views/application.dryml +1 -1
  10. data/hobo_files/plugin/generators/hobo_user_model/templates/model.rb +1 -9
  11. data/hobo_files/plugin/init.rb +5 -0
  12. data/hobo_files/plugin/lib/active_record/has_many_association.rb +1 -1
  13. data/hobo_files/plugin/lib/extensions.rb +26 -5
  14. data/hobo_files/plugin/lib/extensions/test_case.rb +1 -1
  15. data/hobo_files/plugin/lib/hobo.rb +37 -11
  16. data/hobo_files/plugin/lib/hobo/authenticated_user.rb +7 -2
  17. data/hobo_files/plugin/lib/hobo/authentication_support.rb +7 -6
  18. data/hobo_files/plugin/lib/hobo/composite_model.rb +5 -0
  19. data/hobo_files/plugin/lib/hobo/controller.rb +4 -4
  20. data/hobo_files/plugin/lib/hobo/dryml.rb +5 -5
  21. data/hobo_files/plugin/lib/hobo/dryml/part_context.rb +3 -6
  22. data/hobo_files/plugin/lib/hobo/dryml/template.rb +16 -15
  23. data/hobo_files/plugin/lib/hobo/dryml/template_environment.rb +24 -20
  24. data/hobo_files/plugin/lib/hobo/email_address.rb +4 -0
  25. data/hobo_files/plugin/lib/hobo/field_spec.rb +2 -1
  26. data/hobo_files/plugin/lib/hobo/guest.rb +21 -0
  27. data/hobo_files/plugin/lib/hobo/hobo_helper.rb +42 -2
  28. data/hobo_files/plugin/lib/hobo/http_parameters.rb +225 -0
  29. data/hobo_files/plugin/lib/hobo/model.rb +55 -37
  30. data/hobo_files/plugin/lib/hobo/model_controller.rb +151 -151
  31. data/hobo_files/plugin/lib/hobo/model_queries.rb +30 -5
  32. data/hobo_files/plugin/lib/hobo/user_controller.rb +27 -16
  33. data/hobo_files/plugin/lib/hobo/where_fragment.rb +6 -1
  34. data/hobo_files/plugin/tags/rapid.dryml +88 -58
  35. data/hobo_files/plugin/tags/rapid_document_tags.dryml +5 -5
  36. data/hobo_files/plugin/tags/rapid_editing.dryml +3 -3
  37. data/hobo_files/plugin/tags/rapid_forms.dryml +35 -26
  38. data/hobo_files/plugin/tags/rapid_navigation.dryml +13 -12
  39. data/hobo_files/plugin/tags/rapid_pages.dryml +35 -31
  40. data/hobo_files/plugin/tags/rapid_plus.dryml +41 -0
  41. data/hobo_files/plugin/tags/rapid_support.dryml +18 -9
  42. data/hobo_files/plugin/tasks/dump_fixtures.rake +61 -0
  43. metadata +7 -11
  44. data/hobo_files/plugin/spec/fixtures/users.yml +0 -9
  45. data/hobo_files/plugin/spec/spec.opts +0 -6
  46. data/hobo_files/plugin/spec/spec_helper.rb +0 -28
  47. data/hobo_files/plugin/spec/unit/hobo/dryml/template_spec.rb +0 -650
@@ -5,7 +5,13 @@ module Hobo
5
5
  include Hobo::Controller
6
6
 
7
7
  class PermissionDeniedError < RuntimeError; end
8
-
8
+ class UserPermissionError < StandardError
9
+ attr :models
10
+ def initialize(models)
11
+ @models = models || []
12
+ end
13
+ end
14
+
9
15
  VIEWLIB_DIR = "taglibs"
10
16
 
11
17
  GENERIC_PAGE_TAGS = [:index, :show, :new, :edit, :show_collection, :new_in_collection, :login, :signup]
@@ -99,9 +105,10 @@ module Hobo
99
105
  end
100
106
 
101
107
 
102
- def autocomplete_for(attr, options={})
103
- opts = { :limit => 15 }.update(options)
104
- @completers ||= {}
108
+ def autocomplete_for(attr, options={}, &b)
109
+ options = options.reverse_merge(:limit => 15)
110
+ options[:data_filters_block] = b
111
+ @completers ||= HashWithIndifferentAccess.new
105
112
  @completers[attr.to_sym] = opts
106
113
  end
107
114
 
@@ -110,12 +117,6 @@ module Hobo
110
117
  (@completers && @completers[name]) ||
111
118
  (superclass.respond_to?(:autocompleter) && superclass.autocompleter(name))
112
119
  end
113
-
114
-
115
- def def_data_filter(name, &b)
116
- @data_filters ||= {}
117
- @data_filters[name] = b
118
- end
119
120
 
120
121
 
121
122
  def web_method(web_name, method_name=nil)
@@ -139,12 +140,6 @@ module Hobo
139
140
  end
140
141
 
141
142
 
142
- def data_filter(name)
143
- (@data_filters && @data_filters[name]) ||
144
- (superclass.respond_to?(:data_filter) && superclass.data_filter(name))
145
- end
146
-
147
-
148
143
  def find_instance(id)
149
144
  if model.id_name? and id !~ /^\d+$/
150
145
  model.find_by_id_name(id)
@@ -154,6 +149,8 @@ module Hobo
154
149
  end
155
150
 
156
151
  end
152
+
153
+ include HttpParameters
157
154
 
158
155
  # --- ACTIONS --- #
159
156
 
@@ -166,11 +163,13 @@ module Hobo
166
163
  def destroy; hobo_destroy; end
167
164
 
168
165
  def completions
169
- attr = params[:for]
170
- opts = attr && self.class.autocompleter(attr.to_sym)
166
+ opts = self.class.autocompleter(params[:for])
171
167
  if opts
168
+ # Eval any defined filters
169
+ instance_eval(&opts[:data_filters_block]) if opts[:data_filters_block]
170
+ conditions = data_filter_conditions
172
171
  q = params[:query]
173
- items = find_with_data_filter(opts) { send("#{attr}_contains", q) }
172
+ items = model.find(:all) { all?(send("#{attr}_contains", q), conditions && block(conditions)) }
174
173
 
175
174
  render :text => "<ul>\n" + items.map {|i| "<li>#{i.send(attr)}</li>\n"}.join + "</ul>"
176
175
  else
@@ -179,10 +178,34 @@ module Hobo
179
178
  end
180
179
 
181
180
 
182
- ###### END OF ACTIONS ######
181
+ # --- END OF ACTIONS --- #
183
182
 
184
183
  protected
185
184
 
185
+ def data_filter(name, &b)
186
+ @data_filters ||= HashWithIndifferentAccess.new
187
+ @data_filters[name] = b
188
+ end
189
+
190
+ def search(*columns)
191
+ data_filter :search do |query|
192
+ columns_match = columns.map do |c|
193
+ if c.is_a?(Symbol)
194
+ send("#{c}_contains", query)
195
+ elsif c.is_a?(Hash)
196
+ c.map do |k, v|
197
+ related = send(k)
198
+ v = [v] unless v.is_a?(Array)
199
+ v.map { |related_col| related.send("#{related_col}_contains", query) }
200
+ end
201
+ end
202
+ end.flatten
203
+ any?(*columns_match)
204
+ end
205
+ end
206
+
207
+ attr_accessor :data_filters
208
+
186
209
  def overridable_response(options, key)
187
210
  if options.has_key?(key)
188
211
  options[key]
@@ -210,21 +233,34 @@ module Hobo
210
233
 
211
234
 
212
235
  def paginated_find(*args, &b)
213
- options = extract_options_from_args!(args)
214
-
215
- total_number = options.delete(:total_number)
216
- @association = options.delete(:association) or
236
+ options = args.extract_options!
237
+ filter_conditions = data_filter_conditions
238
+ conditions_proc = if b && filter_conditions
239
+ proc { block(b) & block(filter_conditions) }
240
+ else
241
+ b || filter_conditions
242
+ end
243
+
244
+ @association = options.delete(:association) ||
217
245
  if args.any?
218
246
  owner, collection_name = args
219
247
  @association = collection_name.to_s.split(".").inject(owner) { |m, name| m.send(name) }
220
248
  end
221
-
222
- if @association
223
- total_number ||= @association.count
224
- @reflection = @association.proxy_reflection if @association.respond_to?(:proxy_reflection)
225
- end
249
+ @reflection = @association.proxy_reflection if @association._?.respond_to?(:proxy_reflection)
250
+
251
+ total_number = options.delete(:total_number) ||
252
+ begin
253
+ # If there is a conditions block, it may depend on the includes
254
+ count_options = conditions_proc ? { :include => options[:include] } : {}
255
+ if @association
256
+ @association.count(count_options, &conditions_proc)
257
+ else
258
+ model.count(count_options, &conditions_proc)
259
+ end
260
+ end
261
+
262
+ puts total_number
226
263
 
227
- total_number ||= count_with_data_filter
228
264
  page_size = options.delete(:page_size) || 20
229
265
  page = options.delete(:page) || params[:page]
230
266
  @pages = ::ActionController::Pagination::Paginator.new(self, total_number, page_size, page)
@@ -234,11 +270,28 @@ module Hobo
234
270
  :offset => @pages.current.offset,
235
271
  }.merge(options)
236
272
 
273
+ unless options.has_key?(:order)
274
+ _, desc, field = *params[:sort]._?.match(/^(-)?([a-z_]+(?:\.[a-z_]+)?)$/)
275
+ if field
276
+ @sort_field = field
277
+ @sort_direction = desc ? "desc" : "asc"
278
+
279
+ table, column = if field =~ /^(.*)\.(.*)$/
280
+ [$1.camelize.constantize.table_name, $2]
281
+ else
282
+ sort_model = @association ? @association.member_class : model
283
+ [sort_model.table_name, field]
284
+ end
285
+ options[:order] = "#{table}.#{column} #{@sort_direction}"
286
+ elsif !@association
287
+ options[:order] = :default
288
+ end
289
+ end
290
+
237
291
  if @association
238
- @association.find(:all, options, &b)
292
+ @association.find(:all, options, &conditions_proc)
239
293
  else
240
- options[:order] ||= :default
241
- find_with_data_filter(options, &b)
294
+ model.find(:all, options, &conditions_proc)
242
295
  end
243
296
  end
244
297
 
@@ -291,6 +344,7 @@ module Hobo
291
344
 
292
345
  if (@this = options[:this])
293
346
  permission_denied(options) and return unless Hobo.can_create?(current_user, @this)
347
+ valid = @this.save
294
348
  else
295
349
  attributes = params[model.name.underscore]
296
350
  type_attr = params['type']
@@ -301,16 +355,14 @@ module Hobo
301
355
  model
302
356
  end
303
357
  @this = create_model.new
304
- @check_create_permission = [@this]
305
- initialize_from_params(@this, attributes)
306
- for obj in @check_create_permission
307
- permission_denied(options) and return unless Hobo.can_create?(current_user, obj)
308
- end
309
- @check_create_permission = nil
358
+ status = secure_change_transaction { initialize_record(@this, attributes) }
359
+ permission_denied(options) and return if status == :not_allowed
360
+ valid = status == :valid
310
361
  end
311
362
 
312
363
  set_named_this!
313
- if @this.save
364
+
365
+ if valid
314
366
  if block_given?
315
367
  yield
316
368
  else
@@ -335,6 +387,7 @@ module Hobo
335
387
  end
336
388
  end
337
389
 
390
+
338
391
  def hobo_edit(options={})
339
392
  hobo_show(options)
340
393
  end
@@ -346,22 +399,16 @@ module Hobo
346
399
  @this = find_instance_or_not_found(options, :this)
347
400
  return unless @this
348
401
 
349
- original = @this.duplicate
350
-
351
402
  changes = params[model.name.underscore]
352
-
353
- if changes
354
- # The 'duplicate' call above can set these, but they can
355
- # conflict with the changes so we clear them
356
- @this.send(:clear_aggregation_cache)
357
- @this.send(:clear_association_cache)
358
-
359
- update_with_params(@this, changes)
360
- permission_denied(options) and return unless Hobo.can_update?(current_user, original, @this)
361
- end
403
+ status = secure_change_transaction { update_record(@this, changes) }
362
404
 
363
405
  set_named_this!
364
- if changes.nil? || @this.save
406
+ case status
407
+ when :not_allowed
408
+ permission_denied(options)
409
+ return
410
+
411
+ when :valid
365
412
  # Ensure current_user isn't out of date
366
413
  @current_user = @this if @this == current_user
367
414
 
@@ -392,7 +439,7 @@ module Hobo
392
439
  end
393
440
  end
394
441
 
395
- else
442
+ when :invalid
396
443
  # Validation errors
397
444
  respond_to do |wants|
398
445
  wants.html do
@@ -494,18 +541,19 @@ module Hobo
494
541
  end
495
542
 
496
543
 
497
- def permission_denied(options=nil)
498
- if options and options[:permission_denied_response]
544
+ def permission_denied(options={})
545
+ if options[:permission_denied_response]
499
546
  # do nothing (callback handled by LazyHash)
500
547
  elsif respond_to? :permission_denied_response
501
548
  permission_denied_response
502
549
  else
503
- render :text => "Permission Denied", :status => 403
550
+ message = options[:message] || "Permission Denied"
551
+ render :text => message, :status => 403
504
552
  end
505
553
  end
506
554
 
507
- def not_found(options=nil)
508
- if options && options[:not_found_response]
555
+ def not_found(options={})
556
+ if options[:not_found_response]
509
557
  # do nothing (callback handled by LazyHash)
510
558
  elsif respond_to? :not_found_response
511
559
  not_found_response
@@ -531,15 +579,24 @@ module Hobo
531
579
 
532
580
  template = Hobo::ModelController.find_model_template(page_model, page_kind)
533
581
 
534
- if template
535
- render :template => template
536
- true
537
- else
538
- if page_kind.in? GENERIC_PAGE_TAGS
539
- render_tag("#{page_kind.to_s.camelize}Page", :with => @this)
582
+ begin
583
+ if template
584
+ render :template => template
540
585
  true
541
586
  else
542
- false
587
+ # This returns false if no such tag exists
588
+ render_tag("#{page_kind.to_s.camelize}Page", :with => @this)
589
+ end
590
+ rescue ActionView::TemplateError => wrapper
591
+ e = wrapper.original_exception if wrapper.respond_to? :original_exception
592
+ if e.is_a? Hobo::ModelController::UserPermissionError
593
+ if current_user.guest?
594
+ redirect_to login_url(e.models.first || UserController.user_models.first)
595
+ else
596
+ permission_denied(:message => e.message)
597
+ end
598
+ else
599
+ raise
543
600
  end
544
601
  end
545
602
  end
@@ -548,112 +605,55 @@ module Hobo
548
605
  def model
549
606
  self.class.model
550
607
  end
551
-
608
+
552
609
 
553
610
  def find_template
554
611
  Hobo::ModelController.find_model_template(model, params[:action])
555
612
  end
556
613
 
557
-
558
- def with_data_filter(operation, *args, &block)
559
- filter_param = params.keys.find &it.starts_with?("where_")
560
- proc = filter_param && self.class.data_filter(filter_param[6..-1].to_sym)
561
- if proc
562
- filter_args = params[filter_param]
563
- filter_args = [filter_args] unless filter_args.is_a? Array
564
- model.send(operation, *args) do
565
- if block
566
- instance_eval(&block) & instance_exec(*filter_args, &proc)
567
- else
568
- instance_exec(*filter_args, &proc)
569
- end
570
- end
571
- else
572
- if block
573
- model.send(operation, *args) { instance_eval(&block) }
574
- else
575
- model.send(operation, *args)
576
- end
577
- end
578
- end
579
614
 
580
- def find_with_data_filter(opts={}, &b)
581
- with_data_filter(:find, :all, opts, &b)
615
+ def data_filter_conditions
616
+ active_filters = data_filters && (params.keys & data_filters.keys)
617
+ filters = data_filters
618
+ params = self.params
619
+ proc do
620
+ all?(*active_filters.map {|f| instance_exec(params[f], &filters[f])})
621
+ end unless active_filters.blank?
582
622
  end
583
623
 
584
-
585
- def count_with_data_filter(opts={}, &b)
586
- with_data_filter(:count, opts, &b)
587
- end
588
-
589
-
590
- def initialize_from_params(obj, params)
591
- update_with_params(obj, params)
592
- obj.set_creator(current_user)
593
- (@check_create_permission ||= []) << obj
594
- obj
595
- end
596
-
597
624
 
598
- def update_with_params(object, params)
625
+ def XXupdate_with_params(object, params)
599
626
  return unless params
600
627
 
601
628
  params.each_pair do |field,value|
602
629
  field = field.to_sym
603
630
  refl = object.class.reflections[field]
604
- ar_value = if refl
605
- if refl.macro == :belongs_to
606
- associated_record(object, refl, value)
607
-
608
- elsif Hobo.simple_has_many_association?(refl) and object.new_record?
609
- # only populate has_many relationships for new records. For existing
610
- # records, AR updates the DB immediately, bypassing Hobo's permission check
611
- if value.is_a? Array
612
- value.map {|x| associated_record(object, refl, x) }
631
+ is_has_many = refl && Hobo.simple_has_many_association?(refl)
632
+
633
+ if is_has_many
634
+ items = if value.is_a? Array
635
+ value.map {|x| associated_record(object, refl, x) }
636
+ else
637
+ value.keys.every(:to_i).sort.map{|i| associated_record(object, refl, value[i.to_s]) }
638
+ end
639
+ object.send(field).target[0..-1] = items
640
+ else
641
+ ar_value = if refl
642
+ if refl.macro == :belongs_to
643
+ associated_record(object, refl, value)
613
644
  else
614
- value.keys.every(:to_i).sort.map{|i| associated_record(object, refl, value[i.to_s]) }
645
+ raise HoboError, "association #{object.class}.#{refl.name} is not settable via parameters"
615
646
  end
616
647
  else
617
- raise HoboError.new("association #{refl.name} is not settable via parameters")
648
+ param_to_value(object.class.field_type(field), value)
618
649
  end
619
- else
620
- param_to_value(object.class.field_type(field), value)
621
- end
622
- object.send("#{field}=".to_sym, ar_value)
623
- end
624
- end
625
-
626
-
627
- def parse_datetime(s)
628
- defined?(Chronic) ? Chronic.parse(s) : Time.parse(s)
629
- end
630
-
631
-
632
- def param_to_value(field_type, value)
633
- if field_type.nil?
634
- value
635
- elsif field_type <= Date
636
- if value.is_a? Hash
637
- Date.new(*(%w{year month day}.map{|s| value[s].to_i}))
638
- elsif value.is_a? String
639
- dt = parse_datetime(value)
640
- dt && dt.to_date
641
- end
642
- elsif field_type <= Time
643
- if value.is_a? Hash
644
- Time.local(*(%w{year month day hour minute}.map{|s| value[s].to_i}))
645
- elsif value.is_a? String
646
- parse_datetime(value)
650
+ object.send("#{field}=".to_sym, ar_value)
647
651
  end
648
- elsif field_type <= TrueClass
649
- (value.is_a?(String) && value.strip.downcase.in?(['0', 'false']) || value.blank?) ? false : true
650
- else
651
- # primitive field
652
- value
653
652
  end
654
653
  end
655
654
 
656
- def associated_record(owner, refl, value)
655
+
656
+ def XXassociated_record(owner, refl, value)
657
657
  if value.is_a? String
658
658
  if value.starts_with?('@')
659
659
  Hobo.object_from_dom_id(value[1..-1])
@@ -674,7 +674,7 @@ module Hobo
674
674
  end
675
675
 
676
676
 
677
- def object_from_param(param)
677
+ def XXobject_from_param(param)
678
678
  Hobo.object_from_dom_id(param)
679
679
  end
680
680