hobo 0.8.5 → 0.8.6

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 (68) hide show
  1. data/CHANGES.txt +41 -0
  2. data/Manifest +1 -5
  3. data/Rakefile +10 -3
  4. data/bin/hobo +38 -15
  5. data/dryml_generators/rapid/cards.dryml.erb +7 -7
  6. data/dryml_generators/rapid/pages.dryml.erb +52 -24
  7. data/hobo.gemspec +42 -322
  8. data/init.rb +0 -7
  9. data/lib/active_record/association_collection.rb +9 -0
  10. data/lib/hobo.rb +13 -14
  11. data/lib/hobo/accessible_associations.rb +32 -7
  12. data/lib/hobo/authentication_support.rb +1 -1
  13. data/lib/hobo/controller.rb +5 -7
  14. data/lib/hobo/dryml.rb +9 -2
  15. data/lib/hobo/dryml/dryml_builder.rb +11 -12
  16. data/lib/hobo/dryml/dryml_doc.rb +22 -24
  17. data/lib/hobo/dryml/dryml_generator.rb +41 -4
  18. data/lib/hobo/dryml/part_context.rb +5 -3
  19. data/lib/hobo/dryml/template.rb +7 -7
  20. data/lib/hobo/dryml/template_environment.rb +11 -22
  21. data/lib/hobo/dryml/template_handler.rb +94 -25
  22. data/lib/hobo/find_for.rb +2 -2
  23. data/lib/hobo/hobo_helper.rb +21 -21
  24. data/lib/hobo/include_in_save.rb +9 -5
  25. data/lib/hobo/lifecycles/transition.rb +2 -2
  26. data/lib/hobo/model.rb +11 -61
  27. data/lib/hobo/model_controller.rb +28 -29
  28. data/lib/hobo/model_router.rb +12 -13
  29. data/lib/hobo/permissions.rb +47 -37
  30. data/lib/hobo/permissions/associations.rb +1 -1
  31. data/lib/hobo/scopes/association_proxy_extensions.rb +5 -6
  32. data/lib/hobo/scopes/automatic_scopes.rb +7 -4
  33. data/lib/hobo/tasks/rails.rb +4 -0
  34. data/lib/hobo/user.rb +0 -1
  35. data/lib/hobo/user_controller.rb +3 -1
  36. data/lib/hobo/view_hints.rb +17 -3
  37. data/rails_generators/hobo/hobo_generator.rb +1 -0
  38. data/rails_generators/hobo_front_controller/templates/functional_test.rb +1 -11
  39. data/rails_generators/hobo_front_controller/templates/index.dryml +1 -6
  40. data/rails_generators/hobo_rapid/hobo_rapid_generator.rb +1 -0
  41. data/rails_generators/hobo_rapid/templates/hobo-rapid.css +3 -2
  42. data/rails_generators/hobo_rapid/templates/hobo-rapid.js +24 -15
  43. data/rails_generators/hobo_rapid/templates/themes/clean/public/stylesheets/clean.css +17 -12
  44. data/rails_generators/hobo_rapid/templates/themes/clean/public/stylesheets/rapid-ui.css +6 -2
  45. data/rails_generators/hobo_rapid/templates/themes/clean/views/clean.dryml +2 -2
  46. data/rails_generators/hobo_user_model/templates/forgot_password.erb +2 -2
  47. data/rails_generators/hobo_user_model/templates/model.rb +2 -2
  48. data/taglibs/rapid.dryml +3 -2
  49. data/taglibs/rapid_core.dryml +21 -16
  50. data/taglibs/rapid_document_tags.dryml +1 -1
  51. data/taglibs/rapid_editing.dryml +7 -10
  52. data/taglibs/rapid_forms.dryml +115 -26
  53. data/taglibs/rapid_generics.dryml +13 -3
  54. data/taglibs/rapid_lifecycles.dryml +18 -1
  55. data/taglibs/rapid_navigation.dryml +50 -61
  56. data/taglibs/rapid_pages.dryml +103 -19
  57. data/taglibs/rapid_plus.dryml +54 -6
  58. data/taglibs/rapid_support.dryml +38 -1
  59. data/taglibs/rapid_user_pages.dryml +17 -5
  60. data/test/permissions/models/models.rb +24 -12
  61. data/test/permissions/models/test.sqlite3 +0 -0
  62. metadata +6 -15
  63. data/lib/extensions/test_case.rb +0 -129
  64. data/lib/hobo/composite_model.rb +0 -73
  65. data/lib/hobo/model_support.rb +0 -44
  66. data/tasks/fix_dryml.rake +0 -143
  67. data/tasks/generate_tag_reference.rake +0 -192
  68. data/test/dryml/complilation_test.rb +0 -261
@@ -6,7 +6,7 @@ module Hobo
6
6
 
7
7
  NAME_FIELD_GUESS = %w(name title)
8
8
  PRIMARY_CONTENT_GUESS = %w(description body content profile)
9
- SEARCH_COLUMNS_GUESS = %w(name title body content profile)
9
+ SEARCH_COLUMNS_GUESS = %w(name title body description content profile)
10
10
 
11
11
 
12
12
  def self.included(base)
@@ -86,7 +86,9 @@ module Hobo
86
86
  end
87
87
 
88
88
  # ...but only return the ones that registered themselves
89
- @model_names.*.constantize
89
+ @model_names.map do |name|
90
+ name.safe_constantize || (@model_names.delete name; nil)
91
+ end.compact
90
92
  end
91
93
 
92
94
 
@@ -133,9 +135,6 @@ module Hobo
133
135
 
134
136
  module ClassMethods
135
137
 
136
- # include methods also shared by CompositeModel
137
- #include ModelSupport::ClassMethods
138
-
139
138
  attr_accessor :creator_attribute
140
139
  attr_writer :name_attribute, :primary_content_attribute
141
140
 
@@ -204,7 +203,7 @@ module Hobo
204
203
  if options[:polymorphic]
205
204
  class_eval %{
206
205
  def #{name}_is?(target)
207
- target.id == self.#{refl.primary_key_name} && target.class.name == self.#{refl.options[:foreign_type]}
206
+ target.class.name == self.#{refl.options[:foreign_type]} && target.id == self.#{refl.primary_key_name}
208
207
  end
209
208
  def #{name}_changed?
210
209
  #{refl.primary_key_name}_changed? || #{refl.options[:foreign_type]}_changed?
@@ -213,7 +212,7 @@ module Hobo
213
212
  else
214
213
  class_eval %{
215
214
  def #{name}_is?(target)
216
- target.id == self.#{refl.primary_key_name}
215
+ target.class == ::#{refl.klass.name} && target.id == self.#{refl.primary_key_name}
217
216
  end
218
217
  def #{name}_changed?
219
218
  #{refl.primary_key_name}_changed?
@@ -359,10 +358,7 @@ module Hobo
359
358
 
360
359
  def method_missing(name, *args, &block)
361
360
  name = name.to_s
362
- if name =~ /\./
363
- # FIXME: Do we need this now?
364
- call_method_chain(name, args, &block)
365
- elsif create_automatic_scope(name)
361
+ if create_automatic_scope(name)
366
362
  send(name.to_sym, *args, &block)
367
363
  else
368
364
  super(name.to_sym, *args, &block)
@@ -375,13 +371,6 @@ module Hobo
375
371
  end
376
372
 
377
373
 
378
- def call_method_chain(chain, args, &block)
379
- parts = chain.split(".")
380
- s = parts[0..-2].inject(self) { |m, scope| m.send(scope) }
381
- s.send(parts.last, *args)
382
- end
383
-
384
-
385
374
  def to_url_path
386
375
  "#{name.underscore.pluralize}"
387
376
  end
@@ -412,7 +401,7 @@ module Hobo
412
401
  def to_param
413
402
  name_attr = self.class.name_attribute and name = send(name_attr)
414
403
  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('-')
404
+ readable = name.to_s.downcase.gsub(/[^a-z0-9]+/, '-').remove(/-+$/).remove(/^-+/).split('-')[0..5].join('-')
416
405
  @to_param ||= "#{id}-#{readable}"
417
406
  else
418
407
  id.to_s
@@ -431,6 +420,9 @@ module Hobo
431
420
  end
432
421
 
433
422
 
423
+ # We deliberately give these three methods unconventional (java-esque) names to avoid
424
+ # polluting the application namespace
425
+
434
426
  def set_creator(user)
435
427
  set_creator!(user) unless get_creator
436
428
  end
@@ -452,53 +444,11 @@ module Hobo
452
444
  end
453
445
 
454
446
 
455
- # We deliberately give this method an unconventional name to avoid
456
- # polluting the application namespace too badly
457
447
  def get_creator
458
448
  self.class.creator_attribute && send(self.class.creator_attribute)
459
449
  end
460
450
 
461
451
 
462
- def duplicate
463
- copy = self.class.new
464
- copy.copy_instance_variables_from(self, ["@attributes_cache"])
465
- copy.instance_variable_set("@attributes", @attributes.dup)
466
- copy.instance_variable_set("@new_record", nil) unless new_record?
467
-
468
- # Shallow copy of belongs_to associations
469
- for refl in self.class.reflections.values
470
- if refl.macro == :belongs_to and (target = self.send(refl.name))
471
- bta = ActiveRecord::Associations::BelongsToAssociation.new(copy, refl)
472
- bta.replace(target)
473
- copy.instance_variable_set("@#{refl.name}", bta)
474
- end
475
- end
476
- copy
477
- end
478
-
479
-
480
- def same_fields?(other, *fields)
481
- return true if other.nil?
482
-
483
- fields = fields.flatten
484
- fields.all?{|f| self.send(f) == other.send(f)}
485
- end
486
-
487
-
488
- def only_changed_fields?(other, *changed_fields)
489
- return true if other.nil?
490
-
491
- changed_fields = changed_fields.flatten.*.to_s
492
- all_cols = self.class.columns.*.name - []
493
- all_cols.all?{|c| c.in?(changed_fields) || self.send(c) == other.send(c) }
494
- end
495
-
496
-
497
- def compose_with(object, use=nil)
498
- CompositeModel.new_for([self, object])
499
- end
500
-
501
-
502
452
  def typed_id
503
453
  "#{self.class.name.underscore}:#{self.id}" if id
504
454
  end
@@ -4,10 +4,9 @@ module Hobo
4
4
 
5
5
  include Hobo::Controller
6
6
 
7
- VIEWLIB_DIR = "taglibs"
8
-
9
7
  DONT_PAGINATE_FORMATS = [ Mime::CSV, Mime::YAML, Mime::JSON, Mime::XML, Mime::ATOM, Mime::RSS ]
10
- WILL_PAGINATE_OPTIONS = [ :page, :per_page, :total_entries, :count, :finder]
8
+
9
+ WILL_PAGINATE_OPTIONS = [ :page, :per_page, :total_entries, :count, :finder ]
11
10
 
12
11
  READ_ONLY_ACTIONS = [:index, :show]
13
12
  WRITE_ONLY_ACTIONS = [:create, :update, :destroy]
@@ -27,7 +26,6 @@ module Hobo
27
26
 
28
27
  helper_method :model, :current_user
29
28
  before_filter :set_no_cache_headers
30
- after_filter :remember_page_path
31
29
 
32
30
  rescue_from ActiveRecord::RecordNotFound, :with => :not_found
33
31
 
@@ -35,7 +33,7 @@ module Hobo
35
33
  rescue_from Hobo::Lifecycles::LifecycleKeyError, :with => :permission_denied
36
34
 
37
35
  alias_method_chain :render, :hobo_model
38
-
36
+
39
37
  end
40
38
  register_controller(base)
41
39
 
@@ -97,7 +95,7 @@ module Hobo
97
95
  index_action "complete_#{name}", &block
98
96
  else
99
97
  index_action "complete_#{name}" do
100
- hobo_completions name, model, options
98
+ hobo_completions field, model, options
101
99
  end
102
100
  end
103
101
  end
@@ -109,8 +107,8 @@ module Hobo
109
107
  got_block = block_given?
110
108
  define_method web_name do
111
109
  # Make sure we have a copy of the options - it is being mutated somewhere
112
- opts = {}.merge(options)
113
- self.this = find_instance(opts) unless opts[:no_find]
110
+ opts = options.dup
111
+ self.this = find_instance(opts)
114
112
  raise Hobo::PermissionDeniedError unless @this.method_callable_by?(current_user, method)
115
113
  if got_block
116
114
  instance_eval(&block)
@@ -118,7 +116,7 @@ module Hobo
118
116
  @this.send(method)
119
117
  end
120
118
 
121
- hobo_ajax_response || render(:nothing => true) unless performed?
119
+ hobo_ajax_response unless performed?
122
120
  end
123
121
  end
124
122
 
@@ -370,7 +368,7 @@ module Hobo
370
368
  after_submit = params[:after_submit]
371
369
 
372
370
  # The after_submit post parameter takes priority
373
- (after_submit == "stay-here" ? previous_page_path : after_submit) ||
371
+ (after_submit == "stay-here" ? url_for_page_path : after_submit) ||
374
372
 
375
373
  # Then try the record's show page
376
374
  (!destroyed && object_url(@this)) ||
@@ -385,6 +383,11 @@ module Hobo
385
383
  home_page
386
384
  end
387
385
 
386
+
387
+ def url_for_page_path
388
+ controller, view = Controller.controller_and_view_for(params[:page_path])
389
+ url_for :controller => controller, :action => view
390
+ end
388
391
 
389
392
  # TODO: Get rid of this joke of an idea that fails miserably if you open another browser window.
390
393
  def previous_page_path
@@ -410,10 +413,12 @@ module Hobo
410
413
 
411
414
  def response_block(&b)
412
415
  if b
413
- if b.arity == 1
414
- respond_to {|wants| yield(wants) }
415
- else
416
- yield
416
+ respond_to do |format|
417
+ if b.arity == 1
418
+ yield format
419
+ else
420
+ format.html { yield }
421
+ end
417
422
  end
418
423
  performed?
419
424
  end
@@ -421,7 +426,7 @@ module Hobo
421
426
 
422
427
 
423
428
  def request_requires_pagination?
424
- request.format.not_in?(DONT_PAGINATE_FORMATS)
429
+ request.format.not_in?(DONT_PAGINATE_FORMATS) && model.view_hints.paginate?
425
430
  end
426
431
 
427
432
 
@@ -442,7 +447,7 @@ module Hobo
442
447
  def find_owner_and_association(owner_association)
443
448
  refl = model.reflections[owner_association]
444
449
  klass = refl.klass
445
- id = params["#{klass.name.underscore}_id"]
450
+ id = params["#{owner_association}_id"]
446
451
  owner = klass.find(id)
447
452
  instance_variable_set("@#{owner_association}", owner)
448
453
  [owner, owner.send(model.reverse_reflection(owner_association).name)]
@@ -470,7 +475,7 @@ module Hobo
470
475
 
471
476
  def hobo_show(*args, &b)
472
477
  options = args.extract_options!
473
- self.this = find_instance(options)
478
+ self.this ||= find_instance(options)
474
479
  response_block(&b)
475
480
  end
476
481
 
@@ -490,7 +495,7 @@ module Hobo
490
495
 
491
496
  def hobo_create(*args, &b)
492
497
  options = args.extract_options!
493
- self.this = args.first || new_for_create
498
+ self.this ||= args.first || new_for_create
494
499
  this.user_update_attributes(current_user, options[:attributes] || attribute_parameters || {})
495
500
  create_response(:new, &b)
496
501
  end
@@ -499,7 +504,7 @@ module Hobo
499
504
  def hobo_create_for(owner, *args, &b)
500
505
  options = args.extract_options!
501
506
  owner, association = find_owner_and_association(owner)
502
- self.this = args.first || association.new
507
+ self.this ||= args.first || association.new
503
508
  this.user_update_attributes(current_user, options[:attributes] || attribute_parameters || {})
504
509
  create_response(:"new_for_#{owner}", &b)
505
510
  end
@@ -547,7 +552,7 @@ module Hobo
547
552
  def hobo_update(*args, &b)
548
553
  options = args.extract_options!
549
554
 
550
- self.this = args.first || find_instance
555
+ self.this ||= args.first || find_instance
551
556
  changes = options[:attributes] || attribute_parameters or raise RuntimeError, "No update specified in params"
552
557
  this.user_update_attributes(current_user, changes)
553
558
 
@@ -594,7 +599,7 @@ module Hobo
594
599
 
595
600
  def hobo_destroy(*args, &b)
596
601
  options = args.extract_options!
597
- self.this = args.first || find_instance
602
+ self.this ||= args.first || find_instance
598
603
  this.user_destroy(current_user)
599
604
  flash[:notice] = "The #{model.name.titleize.downcase} was deleted" unless request.xhr?
600
605
  destroy_response(&b)
@@ -671,7 +676,7 @@ module Hobo
671
676
  options = options.reverse_merge(:limit => 10, :param => :query, :query_scope => "#{attribute}_contains")
672
677
  finder = finder.limit(options[:limit]) unless finder.send(:scope, :find, :limit)
673
678
  finder = finder.send(options[:query_scope], params[options[:param]])
674
- items = finder.find(:all)
679
+ items = finder.find(:all).select { |r| r.viewable_by?(current_user) }
675
680
  render :text => "<ul>\n" + items.map {|i| "<li>#{i.send(attribute)}</li>\n"}.join + "</ul>"
676
681
  end
677
682
 
@@ -694,6 +699,7 @@ module Hobo
694
699
 
695
700
  def permission_denied(error)
696
701
  self.this = true # Otherwise this gets sent user_view
702
+ @permission_error = error
697
703
  if "permission_denied".in?(self.class.superclass.instance_methods)
698
704
  super
699
705
  else
@@ -762,13 +768,6 @@ module Hobo
762
768
  headers["Expires"] ='0'
763
769
  end
764
770
 
765
- def remember_page_path
766
- if request.method == :get
767
- session[:previous_page_path] = request.path
768
- session[:previous_page_path] += "?#{request.query_string}" unless request.query_string.blank?
769
- end
770
- end
771
-
772
771
  # --- end filters --- #
773
772
 
774
773
  public
@@ -73,20 +73,23 @@ module Hobo
73
73
  return
74
74
  end
75
75
 
76
- require "#{RAILS_ROOT}/app/controllers/application" unless Object.const_defined? :ApplicationController
77
-
76
+ begin
77
+ require "#{RAILS_ROOT}/app/controllers/application" unless Object.const_defined? :ApplicationController
78
+ rescue MissingSourceFile => ex
79
+ # must be on Rails 2.3. Yay!
80
+ end
81
+
78
82
  # Add non-subsite, and all subsite routes
79
83
  [nil, *Hobo.subsites].each { |subsite| add_routes_for(map, subsite) }
80
84
 
81
85
  add_developer_routes(map) if Hobo.developer_features?
82
86
 
83
- # Run the DRYML generators
84
- # TODO: This needs a proper home
85
- Hobo::Dryml::DrymlGenerator.run unless caller[-1] =~ /[\/\\]rake:\d+$/
86
87
  rescue ActiveRecord::StatementInvalid => e
87
88
  # Database problem? Just continue without routes
88
- ActiveRecord::Base.logger.warn "!! Database exception during Hobo routing -- continuing without routes"
89
- ActiveRecord::Base.logger.warn "!! #{e.to_s}"
89
+ if ActiveRecord::Base.logger
90
+ ActiveRecord::Base.logger.warn "!! Database exception during Hobo routing -- continuing without routes"
91
+ ActiveRecord::Base.logger.warn "!! #{e.to_s}"
92
+ end
90
93
  end
91
94
 
92
95
 
@@ -132,11 +135,7 @@ module Hobo
132
135
 
133
136
 
134
137
  def add_routes
135
- # Simple support for composite models, we might later need a CompositeModelController
136
- if model < Hobo::CompositeModel
137
- map.connect "#{plural}/:id", :controller => plural, :action => 'show', :requirements => ID_REQUIREMENT
138
-
139
- elsif controller < Hobo::ModelController
138
+ if controller < Hobo::ModelController
140
139
  # index routes need to be first so the index names don't get
141
140
  # taken as IDs
142
141
  index_action_routes
@@ -181,7 +180,7 @@ module Hobo
181
180
 
182
181
  owner = owner.to_s.singularize if model.reflections[owner].macro == :has_many
183
182
 
184
- collection_path = "#{owner_class.pluralize}/:#{owner_class}_id/#{collection}"
183
+ collection_path = "#{owner_class.pluralize}/:#{owner}_id/#{collection}"
185
184
 
186
185
  actions.each do |action|
187
186
  case action
@@ -319,68 +319,78 @@ module Hobo
319
319
 
320
320
  # By default, attempt to derive edit permission from create/update permission
321
321
  def edit_permitted?(attribute)
322
- if attribute
323
- with_attribute_or_belongs_to_keys(attribute) do |attr, ftype|
324
- unknownify_attribute(self, attr)
325
- unknownify_attribute(self, ftype) if ftype
326
- end
327
- end
322
+ unknownify_attribute(attribute) if attribute
328
323
  new_record? ? create_permitted? : update_permitted?
329
324
  rescue Hobo::UndefinedAccessError
330
325
  # The permission is dependent on the unknown value
331
326
  # so this attribute is not editable
332
327
  false
333
328
  ensure
334
- if attribute
335
- with_attribute_or_belongs_to_keys(attribute) do |attr, ftype|
336
- deunknownify_attribute(self, attr)
337
- deunknownify_attribute(self, ftype) if ftype
338
- end
339
- end
329
+ deunknownify_attribute(attribute) if attribute
340
330
  end
341
331
 
342
332
 
343
333
  # Add some singleton methods to +record+ so give the effect that +attribute+ is unknown. That is,
344
334
  # attempts to access the attribute will result in a Hobo::UndefinedAccessError
345
- def unknownify_attribute(record, attr)
346
- record.metaclass.class_eval do
347
-
335
+ def unknownify_attribute(attr)
336
+ metaclass.class_eval do
348
337
  define_method attr do
349
338
  raise Hobo::UndefinedAccessError
350
339
  end
340
+ end
341
+
342
+ if (refl = self.class.reflections[attr.to_sym]) && refl.macro == :belongs_to
343
+ # A belongs_to -- also unknownify the underlying fields
344
+ unknownify_attribute refl.primary_key_name
345
+ unknownify_attribute refl.options[:foreign_type] if refl.options[:polymorphic]
346
+ else
347
+ # A regular field -- hack the dirty tracking methods
351
348
 
352
- define_method "#{attr}_change" do
353
- raise Hobo::UndefinedAccessError
354
- end
349
+ metaclass.class_eval do
355
350
 
356
- define_method "#{attr}_was" do
357
- read_attribute attr
358
- end
351
+ define_method "#{attr}_change" do
352
+ raise Hobo::UndefinedAccessError
353
+ end
359
354
 
360
- define_method "#{attr}_changed?" do
361
- true
362
- end
355
+ define_method "#{attr}_was" do
356
+ read_attribute attr
357
+ end
358
+
359
+ define_method "#{attr}_changed?" do
360
+ true
361
+ end
363
362
 
364
- def changed?
365
- true
366
- end
363
+ def changed?
364
+ true
365
+ end
367
366
 
368
- define_method :changed do
369
- changed_attributes.keys | [attr]
370
- end
367
+ define_method :changed do
368
+ changed_attributes.keys | [attr.to_s]
369
+ end
371
370
 
372
- def changes
373
- raise Hobo::UndefinedAccessError
374
- end
371
+ def changes
372
+ raise Hobo::UndefinedAccessError
373
+ end
375
374
 
375
+ end
376
376
  end
377
-
378
377
  end
379
378
 
380
379
  # Best. Name. Ever
381
- def deunknownify_attribute(record, attr)
382
- [attr, "#{attr}_change", "#{attr}_was", "#{attr}_changed?", :changed?, :changed, :changes].each do |m|
383
- record.metaclass.send :remove_method, m.to_sym
380
+ def deunknownify_attribute(attr)
381
+ attr = attr.to_sym
382
+
383
+ metaclass.send :remove_method, attr
384
+
385
+ if (refl = self.class.reflections[attr]) && refl.macro == :belongs_to
386
+ # A belongs_to -- restore the underlying fields
387
+ deunknownify_attribute refl.primary_key_name
388
+ deunknownify_attribute refl.options[:foreign_type] if refl.options[:polymorphic]
389
+ else
390
+ # A regular field -- restore the dirty tracking methods
391
+ ["#{attr}_change", "#{attr}_was", "#{attr}_changed?", :changed?, :changed, :changes].each do |m|
392
+ metaclass.send :remove_method, m.to_sym
393
+ end
384
394
  end
385
395
  end
386
396
  end