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
@@ -90,21 +90,27 @@ module Hobo
90
90
  # When called with before_filter :login_from_cookie will check for an :auth_token
91
91
  # cookie and log the user back in if apropriate
92
92
  def login_from_cookie
93
- return unless (token = cookies[:auth_token]) && !logged_in?
94
-
95
- user_model = token[:user_model].constantize
96
- user = user_model.find_by_remember_token(token)
97
- if user && user.remember_token?
93
+ if (user = authenticated_user_from_cookie)
98
94
  user.remember_me
99
- current_user = user
95
+ self.current_user = user
100
96
  create_auth_cookie
101
97
  end
102
98
  end
99
+
100
+
101
+ def authenticated_user_from_cookie
102
+ !logged_in? and
103
+ cookie = cookies[:auth_token] and
104
+ (token, model_name = cookie.split) and
105
+ user_model = model_name.constantize rescue nil and
106
+ user = user_model.find_by_remember_token(token) and
107
+ user.remember_token? and
108
+ user
109
+ end
103
110
 
104
111
  def create_auth_cookie
105
- cookies[:auth_token] = { :value => current_user.remember_token ,
106
- :expires => current_user.remember_token_expires_at,
107
- :user_model => current_user.model }
112
+ cookies[:auth_token] = { :value => "#{current_user.remember_token} #{current_user.class.name}",
113
+ :expires => current_user.remember_token_expires_at }
108
114
  end
109
115
 
110
116
  private
@@ -56,7 +56,7 @@ module Hobo
56
56
 
57
57
 
58
58
  def typed_id
59
- "#{self.class.name.underscore}_#{id}"
59
+ "#{self.class.name.underscore}:#{id}"
60
60
  end
61
61
 
62
62
 
@@ -15,6 +15,7 @@ module Hobo
15
15
  def included_in_class(klass)
16
16
  klass.extend(ClassMethods)
17
17
  klass.class_eval do
18
+ before_filter :login_from_cookie
18
19
  alias_method_chain :redirect_to, :object_url
19
20
  around_filter do |controller, action|
20
21
  Thread.current['Hobo::current_controller'] = controller
@@ -66,11 +67,10 @@ module Hobo
66
67
 
67
68
  def hobo_ajax_response(*args)
68
69
  results = args.extract_options!
69
- this = args.first || @this
70
70
  page_path = params[:page_path]
71
71
  r = params[:render]
72
72
  if r
73
- ajax_update_response(this, page_path, r.values, results)
73
+ ajax_update_response(page_path, r.values, results)
74
74
  true
75
75
  else
76
76
  false
@@ -78,8 +78,8 @@ module Hobo
78
78
  end
79
79
 
80
80
 
81
- def ajax_update_response(this, page_path, render_specs, results={})
82
- add_variables_to_assigns
81
+ def ajax_update_response(page_path, render_specs, results={})
82
+ @template.send(:_evaluate_assigns_and_ivars)
83
83
  renderer = Hobo::Dryml.page_renderer(@template, [], page_path) if page_path
84
84
 
85
85
  render :update do |page|
@@ -89,12 +89,11 @@ module Hobo
89
89
  dom_id = spec[:id]
90
90
 
91
91
  if spec[:part_context]
92
- part_name, part_this, locals = Dryml::PartContext.unmarshal(spec[:part_context], this, session)
93
- part_content = renderer.call_part(dom_id, part_name, part_this, *locals)
92
+ part_content = renderer.refresh_part(spec[:part_context], session, dom_id)
94
93
  page.call(function, dom_id, part_content)
95
94
  elsif spec[:result]
96
95
  result = results[spec[:result].to_sym]
97
- page.call(function, spec[:id], result)
96
+ page.call(function, dom_id, result)
98
97
  else
99
98
  # spec didn't specify any action :-/
100
99
  end
@@ -168,7 +167,7 @@ end
168
167
  class ActionController::Base
169
168
 
170
169
  def home_page
171
- ""
170
+ base_url
172
171
  end
173
172
 
174
173
  end
@@ -58,12 +58,12 @@ module Hobo
58
58
  end
59
59
 
60
60
 
61
- def page_renderer_for_template(view, template)
62
- page_renderer(view, template.locals.keys, template.path_without_extension)
61
+ def page_renderer_for_template(view, local_names, template)
62
+ page_renderer(view, local_names, template.path_without_extension, template.filename)
63
63
  end
64
64
 
65
65
 
66
- def page_renderer(view, local_names=[], page=nil)
66
+ def page_renderer(view, local_names=[], page=nil, filename=nil)
67
67
  if RAILS_ENV == "development"
68
68
  clear_cache
69
69
  Taglib.clear_cache
@@ -79,16 +79,15 @@ module Hobo
79
79
  make_renderer_class("", page, local_names, DEFAULT_IMPORTS, included_taglibs)
80
80
  @tag_page_renderer_classes[controller_class.name].new(page, view)
81
81
  else
82
- template_path = view.finder.pick_template(page, "dryml")
83
-
84
- mtime = File.mtime(template_path)
82
+ filename ||= view._pick_template(page + ".dryml").filename
83
+ mtime = File.mtime(filename)
85
84
  renderer_class = @renderer_classes[page]
86
85
 
87
86
  # do we need to recompile?
88
- if (!renderer_class or # nothing cached?
89
- (local_names - renderer_class.compiled_local_names).any? or # any new local names?
87
+ if (!renderer_class || # nothing cached?
88
+ (local_names - renderer_class.compiled_local_names).any? || # any new local names?
90
89
  renderer_class.load_time < mtime) # cache out of date?
91
- renderer_class = make_renderer_class(File.read(template_path), template_path, local_names,
90
+ renderer_class = make_renderer_class(File.read(filename), filename, local_names,
92
91
  DEFAULT_IMPORTS, included_taglibs)
93
92
  renderer_class.load_time = mtime
94
93
  @renderer_classes[page] = renderer_class
@@ -106,7 +105,7 @@ module Hobo
106
105
 
107
106
 
108
107
  def controller_taglibs(controller_class)
109
- (controller_class.respond_to?(:included_taglibs) && controller_class.included_taglibs) || []
108
+ controller_class.try.included_taglibs || []
110
109
  end
111
110
 
112
111
 
@@ -62,7 +62,13 @@ module Hobo::Dryml
62
62
  # Strip off "_erbout = ''" from the beginning and "; _erbout"
63
63
  # from the end, because we do things differently around
64
64
  # here. (_erbout is defined as a method)
65
- ERB.new(erb_src, nil, ActionView::Base.erb_trim_mode).src[("_erbout = '';").length..-("; _erbout".length)]
65
+ trim_mode = if defined?(ActionView::TemplateHandlers::ERB.erb_trim_mode)
66
+ ActionView::TemplateHandlers::ERB.erb_trim_mode
67
+ else
68
+ ActionView::Base.erb_trim_mode
69
+ end
70
+
71
+ ERB.new(erb_src, nil, trim_mode).src[("_erbout = '';").length..-("; _erbout".length)]
66
72
  end
67
73
 
68
74
 
@@ -0,0 +1,161 @@
1
+ require 'rexml/xpath'
2
+
3
+ module Hobo
4
+
5
+ module Dryml
6
+
7
+ # DrymlDoc provides the facility to parse a directory tree of DRYML taglibs, building a collection of objects that provide metadata
8
+ module DrymlDoc
9
+
10
+ def self.load_taglibs(directory, taglib_class=DrymlDoc::Taglib)
11
+ dryml_files = Dir["#{directory}/**/*.dryml"]
12
+
13
+ dryml_files.map { |f| taglib_class.new(directory, f) }
14
+ end
15
+
16
+ class Taglib
17
+
18
+ def initialize(home, filename)
19
+ @name = filename.sub(/.dryml$/, '')[home.length+1..-1]
20
+ @doc = Hobo::Dryml::Parser::Document.new(File.read(filename), filename)
21
+ parse_tag_defs
22
+ end
23
+
24
+ attr_reader :name, :doc, :tag_defs
25
+
26
+ def comment
27
+ first_node = doc[0][0]
28
+ doc.restore_erb_scriptlets(first_node.to_s.strip) if first_node.is_a?(REXML::Comment)
29
+ end
30
+
31
+ def comment_html
32
+ Maruku.new(comment).to_html
33
+ end
34
+
35
+
36
+ private
37
+
38
+ def tagdef_class
39
+ self.class.parent.const_get('TagDef')
40
+ end
41
+
42
+ def parse_tag_defs
43
+ @tag_defs = []
44
+ REXML::XPath.match(doc, '/*/*[@tag]').each { |node| @tag_defs << tagdef_class.new(self, node) }
45
+ end
46
+
47
+ end
48
+
49
+ class TagDef
50
+
51
+ def initialize(taglib, node)
52
+ @taglib = taglib
53
+ @node = node
54
+ end
55
+
56
+ attr_reader :taglib, :node
57
+ delegate :doc, :to => :taglib
58
+
59
+
60
+ def name
61
+ node.attributes['tag']
62
+ end
63
+
64
+ def source
65
+ doc.restore_erb_scriptlets(node.to_s).strip
66
+ end
67
+
68
+ # The contents of the XML comment, if any, immediately above the tag definition
69
+ def comment
70
+ @comment ||= begin
71
+ space = node.previous_sibling and
72
+ space.to_s.blank? && space.to_s.count("\n") == 1 and
73
+ comment_node = space.previous_sibling
74
+
75
+ if comment_node.is_a?(REXML::Comment)
76
+ doc.restore_erb_scriptlets(comment_node.to_s.strip)
77
+ end
78
+ end
79
+ end
80
+
81
+
82
+ def comment_intro
83
+ comment && comment =~ /(.*?)^#/m ? $1 : comment
84
+ end
85
+
86
+
87
+ def comment_rest
88
+ comment && comment[comment_intro.length..-1]
89
+ end
90
+
91
+ %w(comment comment_intro comment_rest).each do |m|
92
+ class_eval "def #{m}_html; Maruku.new(#{m}).to_html.gsub(/&amp;/, '&'); end"
93
+ end
94
+
95
+ def comment_html
96
+
97
+ end
98
+
99
+ def no_doc?
100
+ comment =~ /^nodoc\b/
101
+ end
102
+
103
+ # An array of the arrtibute names defined by this tag
104
+ def attributes
105
+ (node.attributes['attrs'] || "").split(/\s*,\s*/).where_not.blank?
106
+ end
107
+
108
+
109
+ # Returns a recursive array srtucture, where each item in the array is a pair: [parameter_name, sub_parameters]
110
+ # (sub-parameters is the same kind of structure)
111
+ def parameters(element=node)
112
+ result = []
113
+ element.elements.each do |e|
114
+ if (p = e.attributes['param'])
115
+ param_name = p == "&true" ? e.name : p
116
+ result << [param_name, parameters(e)]
117
+ else
118
+ result.concat(parameters(e))
119
+ end
120
+ end
121
+ result
122
+ end
123
+
124
+
125
+ # Is this the base definition of a polymorphic tag
126
+ def polymorphic?
127
+ node.attributes['polymorphic'].present?
128
+ end
129
+
130
+ # Is this an <extend>?
131
+ def extension?
132
+ node.name == "extend"
133
+ end
134
+
135
+
136
+ # The definition's 'for' attribute
137
+ def for_type
138
+ node.attributes['for']
139
+ end
140
+
141
+
142
+ # The name of the tag, if any, that this definition merges its parameters into
143
+ # That is, the tag with 'merge' or 'merge-params' declared
144
+ def merge_params
145
+ REXML::XPath.first(node, ".//*[@merge|@merge-params]")._?.name
146
+ end
147
+
148
+ # The name of the tag, if any, that this definition merges its attributes into
149
+ # That is, the tag with 'merge' or 'merge-attrs' declared
150
+ def merge_attrs
151
+ REXML::XPath.first(node, ".//*[@merge|@merge-attrs]")._?.name
152
+ end
153
+
154
+ end
155
+
156
+
157
+ end
158
+
159
+ end
160
+
161
+ end
@@ -13,6 +13,7 @@ module Hobo
13
13
  HEADER = "<!-- AUTOMATICALLY GENERATED FILE - DO NOT EDIT -->\n\n"
14
14
 
15
15
  def self.run
16
+ return if RAILS_ENV == "production"
16
17
  @generator ||= DrymlGenerator.new
17
18
  @generator.run
18
19
  end
@@ -137,15 +138,6 @@ module Hobo
137
138
  end
138
139
 
139
140
 
140
- def primary_collection_name(klass=model)
141
- dependent_collection_names = klass.reflections.values.select do |refl|
142
- refl.macro == :has_many && refl.options[:dependent]
143
- end.*.name
144
-
145
- (dependent_collection_names - through_collection_names(klass)).first
146
- end
147
-
148
-
149
141
  def through_collection_names(klass=model)
150
142
  klass.reflections.values.select do |refl|
151
143
  refl.macro == :has_many && refl.options[:through]
@@ -199,6 +191,23 @@ module Hobo
199
191
  end
200
192
 
201
193
 
194
+ def creators
195
+ defined?(model::Lifecycle) ? model::Lifecycle.publishable_creators : []
196
+ end
197
+
198
+ def transitions
199
+ defined?(model::Lifecycle) ? model::Lifecycle.publishable_transitions : []
200
+ end
201
+
202
+ def creator_names
203
+ creators.map { |c| c.name.to_s }
204
+ end
205
+
206
+ def transition_names
207
+ transitions.map { |t| t.name.to_s }.uniq
208
+ end
209
+
210
+
202
211
  def a_or_an(word)
203
212
  (word =~ /^[aeiou]/i ? "an " : "a ") + word
204
213
  end
@@ -7,7 +7,7 @@ module Hobo
7
7
 
8
8
  class TamperedWithPartContext < StandardError; end
9
9
 
10
- class Id < String; end
10
+ class TypedId < String; end
11
11
 
12
12
  class << self
13
13
  attr_accessor :secret, :digest
@@ -23,75 +23,109 @@ module Hobo
23
23
  "hoboParts['#{dom_id}'] = (#{code});\n"
24
24
  end.join
25
25
  end
26
-
27
-
28
- def initialize(part_name, this_id, locals)
29
- @part_name = part_name
30
- @this_id = this_id
31
- @locals = locals.map { |l| pre_marshal(l) }
32
- end
33
-
34
-
35
- def pre_marshal(x)
26
+
27
+
28
+ def self.pre_marshal(x)
36
29
  if x.is_a?(ActiveRecord::Base) && x.respond_to?(:typed_id)
37
- Id.new(x.typed_id)
30
+ TypedId.new(x.typed_id)
38
31
  else
39
32
  x
40
33
  end
41
34
  end
42
35
 
43
36
 
37
+ def self.for_call(part_name, environment, locals)
38
+ new do |c|
39
+ c.part_name = part_name
40
+ c.locals = locals.map { |l| pre_marshal(l) }
41
+ c.this_id = environment.typed_id
42
+ c.form_field_path = environment.form_field_path
43
+ end
44
+ end
45
+
46
+
47
+ def self.for_refresh(encoded_context, page_this, session)
48
+ new do |c|
49
+ c.unmarshal(encoded_context, page_this, session)
50
+ end
51
+ end
52
+
53
+
54
+ def initialize
55
+ yield self
56
+ end
57
+
58
+ attr_accessor :part_name, :locals, :this, :this_field, :this_id, :form_field_path
59
+
60
+
44
61
  def marshal(session)
45
62
  context = [@part_name, @this_id, @locals]
63
+ context << form_field_path if form_field_path
46
64
  data = Base64.encode64(Marshal.dump(context)).strip
47
- digest = self.class.generate_digest(data, session)
65
+ digest = generate_digest(data, session)
48
66
  "#{data}--#{digest}"
49
67
  end
50
68
 
51
69
 
52
- class << self
70
+ # Unmarshal part context to a hash and verify its integrity.
71
+ def unmarshal(client_store, page_this, session)
72
+ data, digest = CGI.unescape(client_store).strip.split('--')
53
73
 
54
- # Generate the HMAC keyed message digest. Uses SHA1 by default.
55
- def generate_digest(data, session)
56
- secret = self.secret || ActionController::Base.cached_session_options.first[:secret]
57
- key = secret.respond_to?(:call) ? secret.call(session) : secret
58
- OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(digest), key, data)
59
- end
74
+ raise TamperedWithPartContext unless digest == generate_digest(data, session)
60
75
 
76
+ context = Marshal.load(Base64.decode64(data))
77
+
78
+ part_name, this_id, locals, form_field_path = context
61
79
 
62
- # Unmarshal part context to a hash and verify its integrity.
63
- def unmarshal(client_store, this, session)
64
- data, digest = CGI.unescape(client_store).strip.split('--')
80
+ RAILS_DEFAULT_LOGGER.info "Call part: #{part_name}. this-id = #{this_id}, locals = #{locals.inspect}"
81
+ RAILS_DEFAULT_LOGGER.info " : form_field_path = #{form_field_path.inspect}" if form_field_path
82
+
83
+ self.part_name = part_name
84
+ self.this_id = this_id
85
+ self.locals = restore_locals(locals)
86
+ self.form_field_path = form_field_path
87
+
88
+ parse_this_id(page_this)
89
+ end
65
90
 
66
- raise TamperedWithPartContext unless digest == generate_digest(data, session)
67
91
 
68
- part_name, this_id, locals = Marshal.load(Base64.decode64(data))
69
- RAILS_DEFAULT_LOGGER.info("Call part: #{part_name}. this-id = #{this_id}, locals = #{locals.inspect}")
92
+ # Generate the HMAC keyed message digest. Uses SHA1 by default.
93
+ def generate_digest(data, session)
94
+ secret = self.class.secret || ActionController::Base.cached_session_options.first[:secret]
95
+ key = secret.respond_to?(:call) ? secret.call(session) : secret
96
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(self.class.digest), key, data)
97
+ end
70
98
 
71
- [part_name, restore_part_this(this_id, this), restore_locals(locals)]
72
- end
73
99
 
74
- def restore_part_this(this_id, this)
75
- if this_id == "this" or this_id.blank?
76
- this
77
- elsif this_id == "nil"
78
- nil
100
+
101
+ def parse_this_id(page_this)
102
+ if this_id == "this"
103
+ self.this = page_this
104
+ elsif this_id =~ /^this:(.*)/
105
+ self.this = page_this
106
+ self.this_field = $1
107
+ elsif this_id == "nil"
108
+ nil
109
+ else
110
+ parts = this_id.split(':')
111
+ if parts.length == 3
112
+ self.this = Hobo::Model.find_by_typed_id("#{parts[0]}:#{parts[1]}")
113
+ self.this_field = parts[2]
79
114
  else
80
- Hobo.object_from_dom_id(this_id)
115
+ self.this = Hobo::Model.find_by_typed_id(this_id)
81
116
  end
82
117
  end
118
+ end
83
119
 
84
120
 
85
- def restore_locals(locals)
86
- locals.map do |l|
87
- if l.is_a?(Id)
88
- Hobo.object_from_dom_id(l)
89
- else
90
- l
91
- end
121
+ def restore_locals(locals)
122
+ locals.map do |l|
123
+ if l.is_a?(TypedId)
124
+ Hobo::Model.find_by_typed_id(this_id)
125
+ else
126
+ l
92
127
  end
93
128
  end
94
-
95
129
  end
96
130
 
97
131
  end