apn_on_rails 0.3.1 → 0.4.0
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.
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.specification +80 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +45 -0
- data/README +51 -9
- data/README.textile +198 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/apn_on_rails.gemspec +110 -0
- data/generators/templates/apn_migrations/004_create_apn_apps.rb +18 -0
- data/generators/templates/apn_migrations/005_create_groups.rb +23 -0
- data/generators/templates/apn_migrations/006_alter_apn_groups.rb +11 -0
- data/generators/templates/apn_migrations/007_create_device_groups.rb +27 -0
- data/generators/templates/apn_migrations/008_create_apn_group_notifications.rb +23 -0
- data/generators/templates/apn_migrations/009_create_pull_notifications.rb +16 -0
- data/lib/apn_on_rails/apn_on_rails.rb +22 -3
- data/lib/apn_on_rails/app/models/apn/app.rb +115 -0
- data/lib/apn_on_rails/app/models/apn/device.rb +3 -1
- data/lib/apn_on_rails/app/models/apn/device_grouping.rb +16 -0
- data/lib/apn_on_rails/app/models/apn/group.rb +12 -0
- data/lib/apn_on_rails/app/models/apn/group_notification.rb +79 -0
- data/lib/apn_on_rails/app/models/apn/notification.rb +6 -30
- data/lib/apn_on_rails/app/models/apn/pull_notification.rb +15 -0
- data/lib/apn_on_rails/libs/connection.rb +2 -1
- data/lib/apn_on_rails/libs/feedback.rb +6 -18
- data/lib/apn_on_rails/tasks/apn.rake +13 -4
- data/spec/active_record/setup_ar.rb +19 -0
- data/spec/apn_on_rails/app/models/apn/app_spec.rb +178 -0
- data/spec/apn_on_rails/app/models/apn/device_spec.rb +60 -0
- data/spec/apn_on_rails/app/models/apn/group_notification_spec.rb +66 -0
- data/spec/apn_on_rails/app/models/apn/notification_spec.rb +71 -0
- data/spec/apn_on_rails/app/models/apn/pull_notification_spec.rb +37 -0
- data/spec/apn_on_rails/libs/connection_spec.rb +40 -0
- data/spec/apn_on_rails/libs/feedback_spec.rb +45 -0
- data/spec/extensions/string.rb +10 -0
- data/spec/factories/app_factory.rb +27 -0
- data/spec/factories/device_factory.rb +29 -0
- data/spec/factories/device_grouping_factory.rb +22 -0
- data/spec/factories/group_factory.rb +27 -0
- data/spec/factories/group_notification_factory.rb +22 -0
- data/spec/factories/notification_factory.rb +22 -0
- data/spec/factories/pull_notification_factory.rb +22 -0
- data/spec/fixtures/hexa.bin +1 -0
- data/spec/fixtures/message_for_sending.bin +0 -0
- data/spec/rails_root/config/apple_push_notification_development.pem +19 -0
- data/spec/spec_helper.rb +55 -0
- metadata +214 -24
data/Rakefile
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
require 'jeweler'
|
13
|
+
Jeweler::Tasks.new do |gem|
|
14
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
15
|
+
gem.name = "apn_on_rails"
|
16
|
+
gem.summary = %Q{Apple Push Notifications on Rails}
|
17
|
+
|
18
|
+
gem.description = %Q{APN on Rails is a Ruby on Rails gem that allows you to
|
19
|
+
easily add Apple Push Notification (iPhone) support to your Rails application.
|
20
|
+
}
|
21
|
+
|
22
|
+
gem.email = "tech-team@prx.org"
|
23
|
+
gem.homepage = "http://github.com/PRX/apn_on_rails"
|
24
|
+
gem.authors = ["markbates", "Rebecca Nesson"]
|
25
|
+
end
|
26
|
+
#Jeweler::RubygemsDotOrgsTasks.new
|
27
|
+
|
28
|
+
require 'rspec/core'
|
29
|
+
require 'rspec/core/rake_task'
|
30
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
31
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
32
|
+
end
|
33
|
+
|
34
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
35
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
36
|
+
spec.rcov = true
|
37
|
+
end
|
38
|
+
|
39
|
+
task :default => :spec
|
40
|
+
|
41
|
+
require 'rake/rdoctask'
|
42
|
+
Rake::RDocTask.new do |rdoc|
|
43
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
44
|
+
|
45
|
+
rdoc.rdoc_dir = 'rdoc'
|
46
|
+
rdoc.title = "apn #{version}"
|
47
|
+
rdoc.rdoc_files.include('README*')
|
48
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
49
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.4.0
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{apn_on_rails}
|
8
|
+
s.version = "0.3.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["markbates", "Rebecca Nesson"]
|
12
|
+
s.date = %q{2010-10-06}
|
13
|
+
s.description = %q{APN on Rails is a Ruby on Rails gem that allows you to
|
14
|
+
easily add Apple Push Notification (iPhone) support to your Rails application.
|
15
|
+
}
|
16
|
+
s.email = %q{tech-team@prx.org}
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE",
|
19
|
+
"README",
|
20
|
+
"README.textile"
|
21
|
+
]
|
22
|
+
s.files = [
|
23
|
+
".gitignore",
|
24
|
+
".rspec",
|
25
|
+
"Gemfile",
|
26
|
+
"Gemfile.lock",
|
27
|
+
"LICENSE",
|
28
|
+
"README",
|
29
|
+
"README.textile",
|
30
|
+
"Rakefile",
|
31
|
+
"VERSION",
|
32
|
+
"apn_on_rails.gemspec",
|
33
|
+
"generators/apn_migrations_generator.rb",
|
34
|
+
"generators/templates/apn_migrations/001_create_apn_devices.rb",
|
35
|
+
"generators/templates/apn_migrations/002_create_apn_notifications.rb",
|
36
|
+
"generators/templates/apn_migrations/003_alter_apn_devices.rb",
|
37
|
+
"lib/apn_on_rails.rb",
|
38
|
+
"lib/apn_on_rails/apn_on_rails.rb",
|
39
|
+
"lib/apn_on_rails/app/models/apn/base.rb",
|
40
|
+
"lib/apn_on_rails/app/models/apn/device.rb",
|
41
|
+
"lib/apn_on_rails/app/models/apn/notification.rb",
|
42
|
+
"lib/apn_on_rails/libs/connection.rb",
|
43
|
+
"lib/apn_on_rails/libs/feedback.rb",
|
44
|
+
"lib/apn_on_rails/tasks/apn.rake",
|
45
|
+
"lib/apn_on_rails/tasks/db.rake",
|
46
|
+
"lib/apn_on_rails_tasks.rb",
|
47
|
+
"spec/active_record/setup_ar.rb",
|
48
|
+
"spec/apn_on_rails/app/models/apn/device_spec.rb",
|
49
|
+
"spec/apn_on_rails/app/models/apn/notification_spec.rb",
|
50
|
+
"spec/apn_on_rails/libs/connection_spec.rb",
|
51
|
+
"spec/apn_on_rails/libs/feedback_spec.rb",
|
52
|
+
"spec/extensions/string.rb",
|
53
|
+
"spec/factories/device_factory.rb",
|
54
|
+
"spec/factories/notification_factory.rb",
|
55
|
+
"spec/fixtures/hexa.bin",
|
56
|
+
"spec/fixtures/message_for_sending.bin",
|
57
|
+
"spec/rails_root/config/apple_push_notification_development.pem",
|
58
|
+
"spec/spec_helper.rb"
|
59
|
+
]
|
60
|
+
s.homepage = %q{http://github.com/PRX/apn_on_rails}
|
61
|
+
s.require_paths = ["lib"]
|
62
|
+
s.rubygems_version = %q{1.3.7}
|
63
|
+
s.summary = %q{Apple Push Notifications on Rails}
|
64
|
+
s.test_files = [
|
65
|
+
"spec/active_record/setup_ar.rb",
|
66
|
+
"spec/apn_on_rails/app/models/apn/device_spec.rb",
|
67
|
+
"spec/apn_on_rails/app/models/apn/notification_spec.rb",
|
68
|
+
"spec/apn_on_rails/libs/connection_spec.rb",
|
69
|
+
"spec/apn_on_rails/libs/feedback_spec.rb",
|
70
|
+
"spec/extensions/string.rb",
|
71
|
+
"spec/factories/device_factory.rb",
|
72
|
+
"spec/factories/notification_factory.rb",
|
73
|
+
"spec/spec_helper.rb"
|
74
|
+
]
|
75
|
+
|
76
|
+
if s.respond_to? :specification_version then
|
77
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
78
|
+
s.specification_version = 3
|
79
|
+
|
80
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
81
|
+
s.add_runtime_dependency(%q<configatron>, [">= 0"])
|
82
|
+
s.add_development_dependency(%q<sqlite3-ruby>, [">= 0"])
|
83
|
+
s.add_development_dependency(%q<rspec>, [">= 2.0.0.beta.19"])
|
84
|
+
s.add_development_dependency(%q<bundler>, [">= 1.0.0.rc.5"])
|
85
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.5.0.pre2"])
|
86
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
87
|
+
s.add_development_dependency(%q<actionpack>, ["~> 2.3.8"])
|
88
|
+
s.add_development_dependency(%q<activerecord>, ["~> 2.3.8"])
|
89
|
+
else
|
90
|
+
s.add_dependency(%q<configatron>, [">= 0"])
|
91
|
+
s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
|
92
|
+
s.add_dependency(%q<rspec>, [">= 2.0.0.beta.19"])
|
93
|
+
s.add_dependency(%q<bundler>, [">= 1.0.0.rc.5"])
|
94
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.0.pre2"])
|
95
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
96
|
+
s.add_dependency(%q<actionpack>, ["~> 2.3.8"])
|
97
|
+
s.add_dependency(%q<activerecord>, ["~> 2.3.8"])
|
98
|
+
end
|
99
|
+
else
|
100
|
+
s.add_dependency(%q<configatron>, [">= 0"])
|
101
|
+
s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
|
102
|
+
s.add_dependency(%q<rspec>, [">= 2.0.0.beta.19"])
|
103
|
+
s.add_dependency(%q<bundler>, [">= 1.0.0.rc.5"])
|
104
|
+
s.add_dependency(%q<jeweler>, ["~> 1.5.0.pre2"])
|
105
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
106
|
+
s.add_dependency(%q<actionpack>, ["~> 2.3.8"])
|
107
|
+
s.add_dependency(%q<activerecord>, ["~> 2.3.8"])
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateApnApps < ActiveRecord::Migration # :nodoc:
|
2
|
+
def self.up
|
3
|
+
create_table :apn_apps do |t|
|
4
|
+
t.text :apn_dev_cert
|
5
|
+
t.text :apn_prod_cert
|
6
|
+
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
|
10
|
+
add_column :apn_devices, :app_id, :integer
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.down
|
15
|
+
drop_table :apps
|
16
|
+
remove_column :apn_devices, :app_id
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class CreateGroups < ActiveRecord::Migration # :nodoc:
|
2
|
+
def self.up
|
3
|
+
create_table :apn_groups do |t|
|
4
|
+
t.column :name, :string
|
5
|
+
|
6
|
+
t.timestamps
|
7
|
+
end
|
8
|
+
|
9
|
+
create_table :apn_devices_apn_groups, :id => false do |t|
|
10
|
+
t.column :group_id, :integer
|
11
|
+
t.column :device_id, :integer
|
12
|
+
end
|
13
|
+
|
14
|
+
add_index :apn_devices_apn_groups, [:group_id, :device_id]
|
15
|
+
add_index :apn_devices_apn_groups, :device_id
|
16
|
+
add_index :apn_devices_apn_groups, :group_id
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.down
|
20
|
+
drop_table :apn_groups
|
21
|
+
drop_table :apn_devices_apn_groups
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class CreateDeviceGroups < ActiveRecord::Migration # :nodoc:
|
2
|
+
def self.up
|
3
|
+
drop_table :apn_devices_apn_groups
|
4
|
+
|
5
|
+
create_table :apn_device_groupings do |t|
|
6
|
+
t.column :group_id, :integer
|
7
|
+
t.column :device_id, :integer
|
8
|
+
end
|
9
|
+
|
10
|
+
add_index :apn_device_groupings, [:group_id, :device_id]
|
11
|
+
add_index :apn_device_groupings, :device_id
|
12
|
+
add_index :apn_device_groupings, :group_id
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.down
|
16
|
+
drop_table :apn_device_groupings
|
17
|
+
|
18
|
+
create_table :apn_devices_apn_groups, :id => false do |t|
|
19
|
+
t.column :group_id, :integer
|
20
|
+
t.column :device_id, :integer
|
21
|
+
end
|
22
|
+
|
23
|
+
add_index :apn_devices_apn_groups, [:group_id, :device_id]
|
24
|
+
add_index :apn_devices_apn_groups, :device_id
|
25
|
+
add_index :apn_devices_apn_groups, :group_id
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class CreateApnGroupNotifications < ActiveRecord::Migration # :nodoc:
|
2
|
+
|
3
|
+
def self.up
|
4
|
+
|
5
|
+
create_table :apn_group_notifications do |t|
|
6
|
+
t.integer :group_id, :null => false
|
7
|
+
t.string :device_language, :size => 5 # if you don't want to send localized strings
|
8
|
+
t.string :sound
|
9
|
+
t.string :alert, :size => 150
|
10
|
+
t.integer :badge
|
11
|
+
t.text :custom_properties
|
12
|
+
t.datetime :sent_at
|
13
|
+
t.timestamps
|
14
|
+
end
|
15
|
+
|
16
|
+
add_index :apn_group_notifications, :group_id
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.down
|
20
|
+
drop_table :apn_group_notifications
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class CreatePullNotifications < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :apn_pull_notifications do |t|
|
4
|
+
t.integer :app_id
|
5
|
+
t.string :title
|
6
|
+
t.string :content
|
7
|
+
t.string :link
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.down
|
14
|
+
drop_table :apn_pull_notifications
|
15
|
+
end
|
16
|
+
end
|
@@ -45,10 +45,19 @@ module APN # :nodoc:
|
|
45
45
|
|
46
46
|
end
|
47
47
|
|
48
|
+
class MissingCertificateError < StandardError
|
49
|
+
def initialize
|
50
|
+
super("This app has no certificate")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
48
54
|
end # Errors
|
49
55
|
|
50
56
|
end # APN
|
51
57
|
|
58
|
+
base = File.join(File.dirname(__FILE__), 'app', 'models', 'apn', 'base.rb')
|
59
|
+
require base
|
60
|
+
|
52
61
|
Dir.glob(File.join(File.dirname(__FILE__), 'app', 'models', 'apn', '*.rb')).sort.each do |f|
|
53
62
|
require f
|
54
63
|
end
|
@@ -57,6 +66,16 @@ end
|
|
57
66
|
path = File.join(File.dirname(__FILE__), 'app', dir)
|
58
67
|
$LOAD_PATH << path
|
59
68
|
# puts "Adding #{path}"
|
60
|
-
|
61
|
-
|
62
|
-
|
69
|
+
begin
|
70
|
+
if ActiveSupport::Dependencies.respond_to? :autoload_paths
|
71
|
+
ActiveSupport::Dependencies.autoload_paths << path
|
72
|
+
ActiveSupport::Dependencies.autoload_once_paths.delete(path)
|
73
|
+
else
|
74
|
+
ActiveSupport::Dependencies.load_paths << path
|
75
|
+
ActiveSupport::Dependencies.load_once_paths.delete(path)
|
76
|
+
end
|
77
|
+
rescue NameError
|
78
|
+
Dependencies.load_paths << path
|
79
|
+
Dependencies.load_once_paths.delete(path)
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
class APN::App < APN::Base
|
2
|
+
|
3
|
+
has_many :groups, :class_name => 'APN::Group', :dependent => :destroy
|
4
|
+
has_many :devices, :class_name => 'APN::Device', :dependent => :destroy
|
5
|
+
has_many :notifications, :through => :devices, :dependent => :destroy
|
6
|
+
has_many :unsent_notifications, :through => :devices
|
7
|
+
has_many :group_notifications, :through => :groups
|
8
|
+
has_many :unsent_group_notifications, :through => :groups
|
9
|
+
|
10
|
+
def cert
|
11
|
+
(RAILS_ENV == 'production' ? apn_prod_cert : apn_dev_cert)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Opens a connection to the Apple APN server and attempts to batch deliver
|
15
|
+
# an Array of group notifications.
|
16
|
+
#
|
17
|
+
#
|
18
|
+
# As each APN::GroupNotification is sent the <tt>sent_at</tt> column will be timestamped,
|
19
|
+
# so as to not be sent again.
|
20
|
+
#
|
21
|
+
def send_notifications
|
22
|
+
if self.cert.nil?
|
23
|
+
raise APN::Errors::MissingCertificateError.new
|
24
|
+
return
|
25
|
+
end
|
26
|
+
unless self.unsent_notifications.nil? || self.unsent_notifications.empty?
|
27
|
+
APN::Connection.open_for_delivery({:cert => self.cert}) do |conn, sock|
|
28
|
+
unsent_notifications.each do |noty|
|
29
|
+
conn.write(noty.message_for_sending)
|
30
|
+
noty.sent_at = Time.now
|
31
|
+
noty.save
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.send_notifications
|
38
|
+
apps = APN::App.all
|
39
|
+
apps.each do |app|
|
40
|
+
app.send_notifications
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def send_group_notifications
|
45
|
+
if self.cert.nil?
|
46
|
+
raise APN::Errors::MissingCertificateError.new
|
47
|
+
return
|
48
|
+
end
|
49
|
+
unless self.unsent_group_notifications.nil? || self.unsent_group_notifications.empty?
|
50
|
+
APN::Connection.open_for_delivery({:cert => self.cert}) do |conn, sock|
|
51
|
+
unsent_group_notifications.each do |gnoty|
|
52
|
+
puts "number of devices is #{gnoty.devices.size}"
|
53
|
+
gnoty.devices.each do |device|
|
54
|
+
conn.write(gnoty.message_for_sending(device))
|
55
|
+
end
|
56
|
+
gnoty.sent_at = Time.now
|
57
|
+
gnoty.save
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def send_group_notification(gnoty)
|
64
|
+
if self.cert.nil?
|
65
|
+
raise APN::Errors::MissingCertificateError.new
|
66
|
+
return
|
67
|
+
end
|
68
|
+
unless gnoty.nil?
|
69
|
+
APN::Connection.open_for_delivery({:cert => self.cert}) do |conn, sock|
|
70
|
+
gnoty.devices.each do |device|
|
71
|
+
conn.write(gnoty.message_for_sending(device))
|
72
|
+
end
|
73
|
+
gnoty.sent_at = Time.now
|
74
|
+
gnoty.save
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.send_group_notifications
|
80
|
+
apps = APN::App.all
|
81
|
+
apps.each do |app|
|
82
|
+
app.send_group_notifications
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Retrieves a list of APN::Device instnces from Apple using
|
87
|
+
# the <tt>devices</tt> method. It then checks to see if the
|
88
|
+
# <tt>last_registered_at</tt> date of each APN::Device is
|
89
|
+
# before the date that Apple says the device is no longer
|
90
|
+
# accepting notifications then the device is deleted. Otherwise
|
91
|
+
# it is assumed that the application has been re-installed
|
92
|
+
# and is available for notifications.
|
93
|
+
#
|
94
|
+
# This can be run from the following Rake task:
|
95
|
+
# $ rake apn:feedback:process
|
96
|
+
def process_devices
|
97
|
+
if self.cert.nil?
|
98
|
+
raise APN::Errors::MissingCertificateError.new
|
99
|
+
return
|
100
|
+
end
|
101
|
+
APN::Feedback.devices(self.cert).each do |device|
|
102
|
+
if device.last_registered_at < device.feedback_at
|
103
|
+
device.destroy
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end # process_devices
|
107
|
+
|
108
|
+
def self.process_devices
|
109
|
+
apps = APN::App.all
|
110
|
+
apps.each do |app|
|
111
|
+
app.process_devices
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|