puffer 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. data/Gemfile +1 -4
  2. data/Gemfile.lock +1 -1
  3. data/VERSION +1 -1
  4. data/app/views/puffer/index.html.erb +7 -7
  5. data/app/views/puffer/show.html.erb +3 -3
  6. data/lib/generators/puffer/install/install_generator.rb +9 -0
  7. data/lib/generators/puffer/install/templates/puffer/javascripts/application.js +2 -0
  8. data/lib/generators/puffer/install/templates/puffer/javascripts/controls.js +965 -0
  9. data/lib/generators/puffer/install/templates/puffer/javascripts/dragdrop.js +974 -0
  10. data/lib/generators/puffer/install/templates/puffer/javascripts/effects.js +1123 -0
  11. data/lib/generators/puffer/install/templates/puffer/javascripts/prototype.js +6001 -0
  12. data/lib/generators/puffer/install/templates/puffer/javascripts/rails.js +175 -0
  13. data/lib/generators/puffer/install/templates/puffer.rb +0 -0
  14. data/lib/puffer/base.rb +1 -1
  15. data/lib/puffer/controller/dsl.rb +9 -10
  16. data/lib/puffer/controller/helpers.rb +1 -9
  17. data/lib/puffer/controller/mutate.rb +0 -4
  18. data/lib/puffer/fields/field.rb +58 -0
  19. data/lib/puffer/fields.rb +17 -0
  20. data/lib/puffer/resource/scoping.rb +6 -0
  21. data/lib/puffer/resource.rb +5 -5
  22. data/puffer.gemspec +17 -8
  23. data/spec/dummy/app/controllers/admin/categories_controller.rb +10 -0
  24. data/spec/dummy/app/controllers/admin/posts_controller.rb +12 -0
  25. data/spec/dummy/app/controllers/admin/profiles_controller.rb +14 -0
  26. data/spec/dummy/app/controllers/admin/tags_controller.rb +8 -0
  27. data/spec/lib/fields_spec.rb +54 -0
  28. data/spec/lib/params_spec.rb +1 -1
  29. data/spec/lib/resource_spec.rb +37 -3
  30. data/spec/spec_helper.rb +2 -1
  31. metadata +30 -19
  32. data/lib/puffer/field.rb +0 -88
@@ -0,0 +1,175 @@
1
+ (function() {
2
+ // Technique from Juriy Zaytsev
3
+ // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
4
+ function isEventSupported(eventName) {
5
+ var el = document.createElement('div');
6
+ eventName = 'on' + eventName;
7
+ var isSupported = (eventName in el);
8
+ if (!isSupported) {
9
+ el.setAttribute(eventName, 'return;');
10
+ isSupported = typeof el[eventName] == 'function';
11
+ }
12
+ el = null;
13
+ return isSupported;
14
+ }
15
+
16
+ function isForm(element) {
17
+ return Object.isElement(element) && element.nodeName.toUpperCase() == 'FORM'
18
+ }
19
+
20
+ function isInput(element) {
21
+ if (Object.isElement(element)) {
22
+ var name = element.nodeName.toUpperCase()
23
+ return name == 'INPUT' || name == 'SELECT' || name == 'TEXTAREA'
24
+ }
25
+ else return false
26
+ }
27
+
28
+ var submitBubbles = isEventSupported('submit'),
29
+ changeBubbles = isEventSupported('change')
30
+
31
+ if (!submitBubbles || !changeBubbles) {
32
+ // augment the Event.Handler class to observe custom events when needed
33
+ Event.Handler.prototype.initialize = Event.Handler.prototype.initialize.wrap(
34
+ function(init, element, eventName, selector, callback) {
35
+ init(element, eventName, selector, callback)
36
+ // is the handler being attached to an element that doesn't support this event?
37
+ if ( (!submitBubbles && this.eventName == 'submit' && !isForm(this.element)) ||
38
+ (!changeBubbles && this.eventName == 'change' && !isInput(this.element)) ) {
39
+ // "submit" => "emulated:submit"
40
+ this.eventName = 'emulated:' + this.eventName
41
+ }
42
+ }
43
+ )
44
+ }
45
+
46
+ if (!submitBubbles) {
47
+ // discover forms on the page by observing focus events which always bubble
48
+ document.on('focusin', 'form', function(focusEvent, form) {
49
+ // special handler for the real "submit" event (one-time operation)
50
+ if (!form.retrieve('emulated:submit')) {
51
+ form.on('submit', function(submitEvent) {
52
+ var emulated = form.fire('emulated:submit', submitEvent, true)
53
+ // if custom event received preventDefault, cancel the real one too
54
+ if (emulated.returnValue === false) submitEvent.preventDefault()
55
+ })
56
+ form.store('emulated:submit', true)
57
+ }
58
+ })
59
+ }
60
+
61
+ if (!changeBubbles) {
62
+ // discover form inputs on the page
63
+ document.on('focusin', 'input, select, texarea', function(focusEvent, input) {
64
+ // special handler for real "change" events
65
+ if (!input.retrieve('emulated:change')) {
66
+ input.on('change', function(changeEvent) {
67
+ input.fire('emulated:change', changeEvent, true)
68
+ })
69
+ input.store('emulated:change', true)
70
+ }
71
+ })
72
+ }
73
+
74
+ function handleRemote(element) {
75
+ var method, url, params;
76
+
77
+ var event = element.fire("ajax:before");
78
+ if (event.stopped) return false;
79
+
80
+ if (element.tagName.toLowerCase() === 'form') {
81
+ method = element.readAttribute('method') || 'post';
82
+ url = element.readAttribute('action');
83
+ params = element.serialize();
84
+ } else {
85
+ method = element.readAttribute('data-method') || 'get';
86
+ url = element.readAttribute('href');
87
+ params = {};
88
+ }
89
+
90
+ new Ajax.Request(url, {
91
+ method: method,
92
+ parameters: params,
93
+ evalScripts: true,
94
+
95
+ onComplete: function(request) { element.fire("ajax:complete", request); },
96
+ onSuccess: function(request) { element.fire("ajax:success", request); },
97
+ onFailure: function(request) { element.fire("ajax:failure", request); }
98
+ });
99
+
100
+ element.fire("ajax:after");
101
+ }
102
+
103
+ function handleMethod(element) {
104
+ var method = element.readAttribute('data-method'),
105
+ url = element.readAttribute('href'),
106
+ csrf_param = $$('meta[name=csrf-param]')[0],
107
+ csrf_token = $$('meta[name=csrf-token]')[0];
108
+
109
+ var form = new Element('form', { method: "POST", action: url, style: "display: none;" });
110
+ element.parentNode.insert(form);
111
+
112
+ if (method !== 'post') {
113
+ var field = new Element('input', { type: 'hidden', name: '_method', value: method });
114
+ form.insert(field);
115
+ }
116
+
117
+ if (csrf_param) {
118
+ var param = csrf_param.readAttribute('content'),
119
+ token = csrf_token.readAttribute('content'),
120
+ field = new Element('input', { type: 'hidden', name: param, value: token });
121
+ form.insert(field);
122
+ }
123
+
124
+ form.submit();
125
+ }
126
+
127
+
128
+ document.on("click", "*[data-confirm]", function(event, element) {
129
+ var message = element.readAttribute('data-confirm');
130
+ if (!confirm(message)) event.stop();
131
+ });
132
+
133
+ document.on("click", "a[data-remote]", function(event, element) {
134
+ if (event.stopped) return;
135
+ handleRemote(element);
136
+ event.stop();
137
+ });
138
+
139
+ document.on("click", "a[data-method]", function(event, element) {
140
+ if (event.stopped) return;
141
+ handleMethod(element);
142
+ event.stop();
143
+ });
144
+
145
+ document.on("submit", function(event) {
146
+ var element = event.findElement(),
147
+ message = element.readAttribute('data-confirm');
148
+ if (message && !confirm(message)) {
149
+ event.stop();
150
+ return false;
151
+ }
152
+
153
+ var inputs = element.select("input[type=submit][data-disable-with]");
154
+ inputs.each(function(input) {
155
+ input.disabled = true;
156
+ input.writeAttribute('data-original-value', input.value);
157
+ input.value = input.readAttribute('data-disable-with');
158
+ });
159
+
160
+ var element = event.findElement("form[data-remote]");
161
+ if (element) {
162
+ handleRemote(element);
163
+ event.stop();
164
+ }
165
+ });
166
+
167
+ document.on("ajax:after", "form", function(event, element) {
168
+ var inputs = element.select("input[type=submit][disabled=true][data-disable-with]");
169
+ inputs.each(function(input) {
170
+ input.value = input.readAttribute('data-original-value');
171
+ input.removeAttribute('data-original-value');
172
+ input.disabled = false;
173
+ });
174
+ });
175
+ })();
File without changes
data/lib/puffer/base.rb CHANGED
@@ -2,7 +2,7 @@ module Puffer
2
2
  class Base < ApplicationController
3
3
  unloadable
4
4
 
5
- respond_to :html
5
+ respond_to :html, :js
6
6
 
7
7
  include Puffer::Controller::Mutate
8
8
  include Puffer::Controller::Dsl
@@ -33,31 +33,30 @@ module Puffer
33
33
  end
34
34
 
35
35
  def index_fields
36
- puffer_fields[:index] || []
36
+ puffer_fields[:index] || Puffer::Fields.new
37
37
  end
38
38
 
39
39
  def show_fields
40
- puffer_fields[:show] || puffer_fields[:index] || []
40
+ puffer_fields[:show] || puffer_fields[:index] || Puffer::Fields.new
41
41
  end
42
42
 
43
43
  def form_fields
44
- puffer_fields[:form] || []
44
+ puffer_fields[:form] || Puffer::Fields.new
45
45
  end
46
46
 
47
47
  def create_fields
48
- puffer_fields[:create] || puffer_fields[:form] || []
48
+ puffer_fields[:create] || puffer_fields[:form] || Puffer::Fields.new
49
49
  end
50
50
 
51
51
  def update_fields
52
- puffer_fields[:update] || puffer_fields[:form] || []
52
+ puffer_fields[:update] || puffer_fields[:form] || Puffer::Fields.new
53
53
  end
54
54
 
55
55
  def field name, options = {}
56
- puffer_fields[@puffer_option] ||= []
57
- field = ::Puffer::Field.new(current_resource.model, name, options)
58
- generate_association_actions field if field.association?
59
- generate_change_actions field if field.toggable?
60
- puffer_fields[@puffer_option] << field
56
+ puffer_fields[@puffer_option] ||= Puffer::Fields.new
57
+ field = puffer_fields[@puffer_option].field(current_resource.model, name, options)
58
+ #generate_association_actions field if field.association?
59
+ #generate_change_actions field if field.toggable?
61
60
  end
62
61
 
63
62
  end
@@ -4,7 +4,7 @@ module Puffer
4
4
 
5
5
  def self.included base
6
6
  base.class_eval do
7
- helper_method :resource_session, :searchable_fields, :boolean_fields, :puffer_navigation
7
+ helper_method :resource_session, :puffer_navigation
8
8
  end
9
9
  end
10
10
 
@@ -21,14 +21,6 @@ module Puffer
21
21
  session[:resources][name]
22
22
  end
23
23
 
24
- def searchable_fields fields
25
- @searchable_fields ||= fields.map { |f| f if [:text, :string, :integer, :decimal, :float].include? f.type }.compact
26
- end
27
-
28
- def boolean_fields
29
- @boolean_fields ||= index_fields.map { |f| f if ['boolean'].include? f.type.to_s }.compact
30
- end
31
-
32
24
  end
33
25
  end
34
26
  end
@@ -37,10 +37,6 @@ module Puffer
37
37
  true
38
38
  end
39
39
 
40
- def configure &block
41
- block.bind(current_config).call
42
- end
43
-
44
40
  end
45
41
 
46
42
  end
@@ -0,0 +1,58 @@
1
+ module Puffer
2
+ class Fields
3
+ class Field
4
+
5
+ attr_accessor :resource, :field, :options
6
+
7
+ def initialize resource, field, options = {}
8
+ @resource = resource
9
+ @field = field.to_s
10
+ @options = options
11
+ end
12
+
13
+ def native?
14
+ model == resource
15
+ end
16
+
17
+ def name
18
+ field.split('.').last
19
+ end
20
+
21
+ def label
22
+ @label ||= options[:label] || model.human_attribute_name(name)
23
+ end
24
+
25
+ def order
26
+ @order ||= options[:order] || query_column
27
+ end
28
+
29
+ def type
30
+ @type ||= options[:type] ? options[:type].to_sym : (column ? column.type : :string)
31
+ end
32
+
33
+ def to_s
34
+ field
35
+ end
36
+
37
+ def model
38
+ unless @model
39
+ @model = resource
40
+ associations = field.split('.')
41
+ while @model.reflect_on_association(association = associations.shift.to_sym) do
42
+ @model = @model.reflect_on_association(association).klass
43
+ end
44
+ end
45
+ @model
46
+ end
47
+
48
+ def column
49
+ @column ||= model.columns_hash[name]
50
+ end
51
+
52
+ def query_column
53
+ "#{model.table_name}.#{name}" if column
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,17 @@
1
+ module Puffer
2
+ class Fields < Array
3
+
4
+ def field *args
5
+ push Field.new(*args)
6
+ end
7
+
8
+ def searchable
9
+ @searchable ||= reject { |f| ![:text, :string, :integer, :decimal, :float].include? f.type }
10
+ end
11
+
12
+ def boolean
13
+ @boolean ||= reject { |f| f.type != :boolean }
14
+ end
15
+
16
+ end
17
+ end
@@ -2,7 +2,13 @@ module Puffer
2
2
  class Resource
3
3
  module Scoping
4
4
 
5
+ def includes
5
6
 
7
+ end
8
+
9
+ def order
10
+
11
+ end
6
12
 
7
13
  end
8
14
  end
@@ -29,8 +29,8 @@ module Puffer
29
29
  params[:plural]
30
30
  end
31
31
 
32
- def human_name
33
- model_name.humanize
32
+ def human
33
+ model.model_name.human
34
34
  end
35
35
 
36
36
  def parent
@@ -92,7 +92,7 @@ module Puffer
92
92
 
93
93
  def collection
94
94
  scope = parent ? parent.member.send(model_name.pluralize) : model
95
- scope.paginate :page => params[:page]
95
+ scope.includes(includes).joins(includes).order(order).paginate :page => params[:page]
96
96
  end
97
97
 
98
98
  def member
@@ -112,7 +112,7 @@ module Puffer
112
112
  if plural?
113
113
  parent.member.send(model_name.pluralize).new attributes
114
114
  else
115
- parent.member.send("build_#{model_name}")
115
+ parent.member.send("build_#{model_name}", attributes)
116
116
  end
117
117
  else
118
118
  model.new attributes
@@ -131,7 +131,7 @@ module Puffer
131
131
  method = method.to_s
132
132
  if method.match(/path$/)
133
133
  options = args.extract_options!
134
- return send method.gsub(/path$/, 'url'), *(args << options.merge(:routing_type => :path))
134
+ return send method.gsub(/path$/, 'url'), *(args << options.merge(:routing_type => :path)) if defined? method.gsub(/path$/, 'url')
135
135
  end
136
136
  model.send method, *args, &block
137
137
  end
data/puffer.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{puffer}
8
- s.version = "0.0.3"
8
+ s.version = "0.0.4"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["pyromaniac"]
12
- s.date = %q{2010-12-30}
12
+ s.date = %q{2011-01-03}
13
13
  s.description = %q{In Soviet Russia puffer admins you}
14
14
  s.email = %q{kinwizard@gmail.com}
15
15
  s.extra_rdoc_files = [
@@ -43,6 +43,13 @@ Gem::Specification.new do |s|
43
43
  "lib/generators/puffer/controller/templates/controller.rb",
44
44
  "lib/generators/puffer/install/USAGE",
45
45
  "lib/generators/puffer/install/install_generator.rb",
46
+ "lib/generators/puffer/install/templates/puffer.rb",
47
+ "lib/generators/puffer/install/templates/puffer/javascripts/application.js",
48
+ "lib/generators/puffer/install/templates/puffer/javascripts/controls.js",
49
+ "lib/generators/puffer/install/templates/puffer/javascripts/dragdrop.js",
50
+ "lib/generators/puffer/install/templates/puffer/javascripts/effects.js",
51
+ "lib/generators/puffer/install/templates/puffer/javascripts/prototype.js",
52
+ "lib/generators/puffer/install/templates/puffer/javascripts/rails.js",
46
53
  "lib/puffer.rb",
47
54
  "lib/puffer/base.rb",
48
55
  "lib/puffer/controller/actions.rb",
@@ -54,7 +61,8 @@ Gem::Specification.new do |s|
54
61
  "lib/puffer/extensions/controller.rb",
55
62
  "lib/puffer/extensions/core.rb",
56
63
  "lib/puffer/extensions/mapper.rb",
57
- "lib/puffer/field.rb",
64
+ "lib/puffer/fields.rb",
65
+ "lib/puffer/fields/field.rb",
58
66
  "lib/puffer/railtie.rb",
59
67
  "lib/puffer/resource.rb",
60
68
  "lib/puffer/resource/routing.rb",
@@ -117,6 +125,7 @@ Gem::Specification.new do |s|
117
125
  "spec/fabricators/tags_fabricator.rb",
118
126
  "spec/fabricators/users_fabricator.rb",
119
127
  "spec/integration/navigation_spec.rb",
128
+ "spec/lib/fields_spec.rb",
120
129
  "spec/lib/params_spec.rb",
121
130
  "spec/lib/render_fallback_spec.rb",
122
131
  "spec/lib/resource/routing_spec.rb",
@@ -126,7 +135,7 @@ Gem::Specification.new do |s|
126
135
  ]
127
136
  s.homepage = %q{http://github.com/puffer/puffer}
128
137
  s.require_paths = ["lib"]
129
- s.rubygems_version = %q{1.3.7}
138
+ s.rubygems_version = %q{1.4.1}
130
139
  s.summary = %q{Admin interface builder}
131
140
  s.test_files = [
132
141
  "spec/dummy/app/controllers/admin/categories_controller.rb",
@@ -169,6 +178,7 @@ Gem::Specification.new do |s|
169
178
  "spec/fabricators/tags_fabricator.rb",
170
179
  "spec/fabricators/users_fabricator.rb",
171
180
  "spec/integration/navigation_spec.rb",
181
+ "spec/lib/fields_spec.rb",
172
182
  "spec/lib/params_spec.rb",
173
183
  "spec/lib/render_fallback_spec.rb",
174
184
  "spec/lib/resource/routing_spec.rb",
@@ -178,12 +188,11 @@ Gem::Specification.new do |s|
178
188
  ]
179
189
 
180
190
  if s.respond_to? :specification_version then
181
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
182
191
  s.specification_version = 3
183
192
 
184
193
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
185
194
  s.add_runtime_dependency(%q<rails>, ["~> 3.0.3"])
186
- s.add_runtime_dependency(%q<will_paginate>, ["~> 3.0.beta"])
195
+ s.add_runtime_dependency(%q<will_paginate>, ["~> 3.0.pre2"])
187
196
  s.add_development_dependency(%q<capybara>, [">= 0.4.0"])
188
197
  s.add_development_dependency(%q<sqlite3-ruby>, [">= 0"])
189
198
  s.add_development_dependency(%q<rspec-rails>, [">= 0"])
@@ -193,7 +202,7 @@ Gem::Specification.new do |s|
193
202
  s.add_development_dependency(%q<jeweler>, [">= 0"])
194
203
  else
195
204
  s.add_dependency(%q<rails>, ["~> 3.0.3"])
196
- s.add_dependency(%q<will_paginate>, ["~> 3.0.beta"])
205
+ s.add_dependency(%q<will_paginate>, ["~> 3.0.pre2"])
197
206
  s.add_dependency(%q<capybara>, [">= 0.4.0"])
198
207
  s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
199
208
  s.add_dependency(%q<rspec-rails>, [">= 0"])
@@ -204,7 +213,7 @@ Gem::Specification.new do |s|
204
213
  end
205
214
  else
206
215
  s.add_dependency(%q<rails>, ["~> 3.0.3"])
207
- s.add_dependency(%q<will_paginate>, ["~> 3.0.beta"])
216
+ s.add_dependency(%q<will_paginate>, ["~> 3.0.pre2"])
208
217
  s.add_dependency(%q<capybara>, [">= 0.4.0"])
209
218
  s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
210
219
  s.add_dependency(%q<rspec-rails>, [">= 0"])
@@ -1,3 +1,13 @@
1
1
  class Admin::CategoriesController < Puffer::Base
2
2
 
3
+ index do
4
+ field :id
5
+ field :title
6
+ end
7
+
8
+ form do
9
+ field :id
10
+ field :title
11
+ end
12
+
3
13
  end
@@ -1,3 +1,15 @@
1
1
  class Admin::PostsController < Puffer::Base
2
2
 
3
+ index do
4
+ field :user
5
+ field :title
6
+ field :body
7
+ end
8
+
9
+ form do
10
+ field :user
11
+ field :title
12
+ field :body
13
+ end
14
+
3
15
  end
@@ -1,3 +1,17 @@
1
1
  class Admin::ProfilesController < Puffer::Base
2
2
 
3
+ index do
4
+ field :user
5
+ field :name
6
+ field :surname
7
+ field :birth_date
8
+ end
9
+
10
+ form do
11
+ field :user
12
+ field :name
13
+ field :surname
14
+ field :birth_date
15
+ end
16
+
3
17
  end
@@ -1,3 +1,11 @@
1
1
  class Admin::TagsController < Puffer::Base
2
2
 
3
+ index do
4
+ field :name
5
+ end
6
+
7
+ form do
8
+ field :name
9
+ end
10
+
3
11
  end
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Fields" do
4
+
5
+ end
6
+
7
+ describe "Field" do
8
+
9
+ it "#model" do
10
+ field = Puffer::Fields::Field.new Post, 'user.profile.name'
11
+ field.model.should == Profile
12
+ field = Puffer::Fields::Field.new Post, 'user.email'
13
+ field.model.should == User
14
+ field = Puffer::Fields::Field.new Post, 'title'
15
+ field.model.should == Post
16
+ end
17
+
18
+ it "#name" do
19
+ field = Puffer::Fields::Field.new Post, 'user.profile.name'
20
+ field.name.should == 'name'
21
+ end
22
+
23
+ it "#query_column" do
24
+ field = Puffer::Fields::Field.new Post, 'user.profile.name'
25
+ field.query_column.should == 'profiles.name'
26
+ field = Puffer::Fields::Field.new Post, 'user.email'
27
+ field.query_column.should == 'users.email'
28
+ field = Puffer::Fields::Field.new Post, 'user.full_name'
29
+ field.query_column.should == nil
30
+ end
31
+
32
+ it "#column" do
33
+ field = Puffer::Fields::Field.new Post, 'user.profile.name'
34
+ field.column.name.should == 'name'
35
+ field = Puffer::Fields::Field.new Post, 'user.full_name'
36
+ field.column.should == nil
37
+ end
38
+
39
+ it '#type' do
40
+ field = Puffer::Fields::Field.new Post, 'user.created_at'
41
+ field.type.should == :datetime
42
+ end
43
+
44
+ it '#type with virtual field' do
45
+ field = Puffer::Fields::Field.new Post, 'user.full_name'
46
+ field.type.should == :string
47
+ end
48
+
49
+ it '#type was specified' do
50
+ field = Puffer::Fields::Field.new Post, 'user.full_name', :type => :text
51
+ field.type.should == :text
52
+ end
53
+
54
+ end
@@ -23,7 +23,7 @@ describe "Params" do
23
23
  =end
24
24
 
25
25
  before :each do
26
- @resource = mock(Puffer::Resource, :collection => [], :member => nil, :template => {:nothing => true})
26
+ @resource = mock(Puffer::Resource, :collection => [], :member => nil, :template => {:nothing => true}, :includes => nil)
27
27
  Puffer::Resource.stub(:new) {@resource}
28
28
  end
29
29