pfeed 0.1.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/.document +5 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +20 -0
- data/LICENSE.txt +20 -0
- data/MIT-LICENSE +20 -0
- data/README.markdown +115 -0
- data/README.rdoc +231 -0
- data/Rakefile +44 -0
- data/VERSION +1 -0
- data/app/models/pfeed_delivery.rb +4 -0
- data/app/models/pfeed_item.rb +172 -0
- data/app/models/pfeeds/user_updated_attribute.rb +14 -0
- data/app/views/pfeeds/_pfeed.html.erb +14 -0
- data/app/views/pfeeds/_pfeed_item.html.erb +3 -0
- data/app/views/pfeeds/_user_updated_attribute.html.erb +4 -0
- data/db/migrate/0000_create_pfeed_items.rb +18 -0
- data/db/migrate/0001_create_pfeed_deliveries.rb +15 -0
- data/init.rb +18 -0
- data/install.rb +1 -0
- data/lib/generator/pfeed_customization/USAGE +10 -0
- data/lib/generator/pfeed_customization/pfeed_customization_generator.rb +29 -0
- data/lib/generator/pfeed_customization/templates/pfeed_model.rb +5 -0
- data/lib/generator/pfeed_customization/templates/pfeed_view.html.erb +5 -0
- data/lib/pfeed.rb +29 -0
- data/lib/pfeed/pfeed.rb +102 -0
- data/lib/pfeed/pfeed_utils.rb +21 -0
- data/lib/pfeed_utils.rb +21 -0
- data/lib/tasks/pfeed.rake +54 -0
- data/pfeed.gemspec +93 -0
- data/pfeed/.document +5 -0
- data/pfeed/Gemfile +13 -0
- data/pfeed/LICENSE.txt +20 -0
- data/pfeed/Rakefile +53 -0
- data/pfeed/test/helper.rb +18 -0
- data/pfeed/test/test_pfeed.rb +7 -0
- data/test/bk_lib/pfeed_test.rb +57 -0
- data/test/bk_lib/pfeed_utils_test.rb +11 -0
- data/test/helper.rb +20 -0
- data/test/lib/pfeed_test.rb +57 -0
- data/test/lib/pfeed_utils_test.rb +11 -0
- data/test/test_helper.rb +71 -0
- data/test/test_pfeed.rb +9 -0
- data/uninstall.rb +1 -0
- metadata +164 -0
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,172 @@
|
|
1
|
+
class PfeedItem < ActiveRecord::Base
|
2
|
+
|
3
|
+
serialize :data, Hash
|
4
|
+
serialize :participants, Array
|
5
|
+
|
6
|
+
belongs_to :originator, :polymorphic => true
|
7
|
+
belongs_to :participant, :polymorphic => true
|
8
|
+
|
9
|
+
has_many :pfeed_deliveries, :dependent => :destroy
|
10
|
+
|
11
|
+
attr_accessor :temp_references # this is an temporary Hash to hold references to temporary Objects
|
12
|
+
|
13
|
+
def self.log(ar_obj,method_name,method_name_in_past_tense,returned_result,*args_supplied_to_method,&block_supplied_to_method)
|
14
|
+
|
15
|
+
#puts "#{ar_obj.class.to_s},#{method_name},#{method_name_in_past_tense},#{returned_result},#{args_supplied_to_method.length}"
|
16
|
+
|
17
|
+
# optional :if => :test, or :unless => :test
|
18
|
+
if if_cond = ar_obj.pfeed_options[:if]
|
19
|
+
return unless ar_obj.send(if_cond)
|
20
|
+
elsif unless_cond = ar_obj.pfeed_options[:unless]
|
21
|
+
return if ar_obj.send(unless_cond)
|
22
|
+
end
|
23
|
+
|
24
|
+
raise ArgumentError, "originator object must to be saved" if ar_obj.new_record?
|
25
|
+
|
26
|
+
temp_references = Hash.new
|
27
|
+
temp_references[:originator] = ar_obj
|
28
|
+
temp_references[:participant] = nil
|
29
|
+
temp_references[:participant] = args_supplied_to_method[0] if args_supplied_to_method && args_supplied_to_method.length >= 1 && args_supplied_to_method[0].class < ActiveRecord::Base
|
30
|
+
|
31
|
+
pfeed_class_name = "#{ar_obj.class.to_s.underscore}_#{method_name_in_past_tense}".camelize # may be I could use .classify
|
32
|
+
constructor_options = { :originator_id => temp_references[:originator].id , :originator_type => temp_references[:originator].class.to_s , :participant_id => (temp_references[:participant] ? temp_references[:participant].id : nil) , :participant_type => (temp_references[:participant] ? temp_references[:participant].class.to_s : nil) } # there is a reason why I didnt use {:originator => temp_references[:originator]} , if originator is new record it might get saved here un intentionally
|
33
|
+
|
34
|
+
|
35
|
+
p_item = new_pfeed_item(pfeed_class_name, constructor_options, temp_references)
|
36
|
+
p_item.pack_data(method_name,method_name_in_past_tense,returned_result,*args_supplied_to_method,&block_supplied_to_method)
|
37
|
+
|
38
|
+
|
39
|
+
p_item.save!
|
40
|
+
#puts "Trying to deliver to #{ar_obj} #{ar_obj.pfeed_audience_hash[method_name.to_sym]}"
|
41
|
+
p_item.attempt_delivery(ar_obj,ar_obj.pfeed_audience_hash[method_name.to_sym]) # attempting the delivery of the feed
|
42
|
+
end
|
43
|
+
|
44
|
+
@@dj = (defined? Delayed) == "constant" && (instance_methods.include? 'send_later') #this means Delayed_job exists , so make use of asynchronous delivery of pfeed
|
45
|
+
|
46
|
+
def attempt_delivery (ar_obj,method_name_arr)
|
47
|
+
return if method_name_arr.empty?
|
48
|
+
|
49
|
+
if @@dj
|
50
|
+
send_later(:deliver,ar_obj,method_name_arr)
|
51
|
+
else # regular instant delivery
|
52
|
+
send(:deliver,ar_obj,method_name_arr)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def deliver(ar_obj,method_name_arr)
|
57
|
+
method_name_arr.map { |method_name|
|
58
|
+
ar_obj.send(method_name)
|
59
|
+
}.flatten.uniq.map {|o| deliver_to(o) }.compact
|
60
|
+
end
|
61
|
+
|
62
|
+
def deliver_to(result_obj)
|
63
|
+
return nil unless result_obj != nil && begin
|
64
|
+
result_obj.is_pfeed_receiver
|
65
|
+
rescue NoMethodError
|
66
|
+
raise NoMethodError, "you must use the receives_pfeed macro for class: #{result_obj.class}"
|
67
|
+
end
|
68
|
+
|
69
|
+
if !result_obj.new_record?
|
70
|
+
delivery = PfeedDelivery.new
|
71
|
+
delivery.pfeed_item = self
|
72
|
+
delivery.pfeed_receiver = result_obj
|
73
|
+
delivery.save!
|
74
|
+
end
|
75
|
+
|
76
|
+
return result_obj
|
77
|
+
end
|
78
|
+
|
79
|
+
def accessible?
|
80
|
+
true
|
81
|
+
end
|
82
|
+
|
83
|
+
def view_template_name
|
84
|
+
"#{self.class.to_s.underscore}".split("/").last
|
85
|
+
end
|
86
|
+
|
87
|
+
def audience
|
88
|
+
# return list of objects to whom feed gets delivered
|
89
|
+
end
|
90
|
+
|
91
|
+
def pack_data(method_name,method_name_in_past_tense,returned_result,*args_supplied_to_method,&block_supplied_to_method)
|
92
|
+
self.data = {} if ! self.data
|
93
|
+
action_string = method_name_in_past_tense.humanize.downcase
|
94
|
+
hash_to_be_merged = {:action_string => action_string, :originator_identity => guess_identification(originator)}
|
95
|
+
|
96
|
+
if current_user = Thread.current[:current_user]
|
97
|
+
hash_to_be_merged.merge!(:current_user_identity => guess_identification(current_user))
|
98
|
+
end
|
99
|
+
|
100
|
+
self.data.merge! hash_to_be_merged
|
101
|
+
end
|
102
|
+
|
103
|
+
IDENTIFICATIONS = {}
|
104
|
+
def guess_identification(ar_obj)
|
105
|
+
if identifier = ar_obj.respond_to?(:pfeed_options) && ar_obj.pfeed_options[:pfeed_identification]
|
106
|
+
return ar_obj.send(identifier)
|
107
|
+
end
|
108
|
+
|
109
|
+
if attribute = IDENTIFICATIONS[ar_obj.class]
|
110
|
+
if (identi = ar_obj.read_attribute(attribute)).blank?
|
111
|
+
identi = ar_obj.send(attribute) rescue nil
|
112
|
+
end
|
113
|
+
return identi if identi
|
114
|
+
end
|
115
|
+
|
116
|
+
possible_attributes = ["username","login","name","company_name","first_name","last_name","login_name","login_id","given_name","nick_name","nick","short_name"]
|
117
|
+
|
118
|
+
possible_attributes = self.data[:config][:identifications] + possible_attributes if self.data[:config] && self.data[:config][:identifications] && self.data[:config][:identifications].is_a?(Array)
|
119
|
+
matched_name = ar_obj.attribute_names & possible_attributes # intersection of two sets
|
120
|
+
|
121
|
+
identi = nil
|
122
|
+
matched_name.each do |attribute|
|
123
|
+
result = ar_obj.read_attribute(attribute)
|
124
|
+
next unless result.present? && result.kind_of?(String)
|
125
|
+
IDENTIFICATIONS[ar_obj.class] = attribute
|
126
|
+
identi = result
|
127
|
+
break
|
128
|
+
end
|
129
|
+
|
130
|
+
if identi.blank?
|
131
|
+
possible_attributes.each do |attribute|
|
132
|
+
next unless ar_obj.respond_to? attribute
|
133
|
+
result = ar_obj.send(attribute) rescue nil
|
134
|
+
next unless result.present? && result.kind_of?(String)
|
135
|
+
IDENTIFICATIONS[ar_obj.class] = attribute
|
136
|
+
identi = result
|
137
|
+
break
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
identi = "#{ar_obj.class.to_s}(\##{ar_obj.id})" if identi.blank?
|
142
|
+
identi
|
143
|
+
end
|
144
|
+
|
145
|
+
# look for custom pfeed class, with or withour Pfeed:: prefix
|
146
|
+
CUSTOM_CLASSES = {}
|
147
|
+
def self.new_pfeed_item(pfeed_class_name, constructor_options, temp_references)
|
148
|
+
if (klass = CUSTOM_CLASSES[pfeed_class_name]).nil?
|
149
|
+
retried = false
|
150
|
+
begin
|
151
|
+
#puts "Attempting to create object of #{pfeed_class_name} "
|
152
|
+
klass = pfeed_class_name.constantize
|
153
|
+
(CUSTOM_CLASSES[pfeed_class_name] = klass).new(
|
154
|
+
constructor_options.merge(:temp_references => temp_references))
|
155
|
+
rescue NameError
|
156
|
+
unless retried
|
157
|
+
CUSTOM_CLASSES[pfeed_class_name] = false
|
158
|
+
retried = true
|
159
|
+
pfeed_class_name = "Pfeeds::"+pfeed_class_name
|
160
|
+
retry
|
161
|
+
end
|
162
|
+
PfeedItem.new(constructor_options)
|
163
|
+
end
|
164
|
+
else
|
165
|
+
if !klass
|
166
|
+
PfeedItem.new(constructor_options)
|
167
|
+
else
|
168
|
+
klass.new(constructor_options)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class Pfeeds::UserUpdatedAttribute < PfeedItem
|
2
|
+
|
3
|
+
def pack_data(method_name,method_name_in_past_tense,returned_result,*args_supplied_to_method,&block_supplied_to_method)
|
4
|
+
super
|
5
|
+
self.data = {} if ! self.data
|
6
|
+
attribute_name = args_supplied_to_method[0].to_s.humanize
|
7
|
+
hash_to_be_merged = {:attribute_name => attribute_name}
|
8
|
+
|
9
|
+
self.data.merge! hash_to_be_merged
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<ul class="pfeed_container">
|
2
|
+
<li>
|
3
|
+
<%=
|
4
|
+
begin
|
5
|
+
render(:partial => "pfeeds/#{pfeed.view_template_name}", :object => pfeed)
|
6
|
+
rescue
|
7
|
+
"<!-- error in #{pfeed.view_template_name}: #{$!.to_s.split("\n").join("-->\n<!--")} -->"
|
8
|
+
end
|
9
|
+
%>
|
10
|
+
</li>
|
11
|
+
</ul>
|
12
|
+
|
13
|
+
|
14
|
+
|
@@ -0,0 +1,4 @@
|
|
1
|
+
|
2
|
+
<font color="blue"><%= user_updated_attribute.guess_identification(user_updated_attribute.originator) %></font>
|
3
|
+
<%= user_updated_attribute.data[:action_string] %> <%= user_updated_attribute.data[:attribute_name] %>
|
4
|
+
about <%= time_ago_in_words(user_updated_attribute.created_at) %> ago
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreatePfeedItems < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :pfeed_items do |t|
|
4
|
+
t.string :type
|
5
|
+
t.integer :originator_id
|
6
|
+
t.string :originator_type
|
7
|
+
t.integer :participant_id
|
8
|
+
t.string :participant_type
|
9
|
+
t.text :data
|
10
|
+
t.datetime :expiry
|
11
|
+
t.timestamps
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.down
|
16
|
+
drop_table :pfeed_items
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class CreatePfeedDeliveries < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
|
4
|
+
create_table :pfeed_deliveries do |t|
|
5
|
+
t.integer :pfeed_receiver_id
|
6
|
+
t.string :pfeed_receiver_type
|
7
|
+
t.integer :pfeed_item_id
|
8
|
+
t.timestamps
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.down
|
13
|
+
drop_table :pfeed_deliveries
|
14
|
+
end
|
15
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Author: Er Abhishek Parolkar
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + '/lib/pfeed'
|
4
|
+
require File.dirname(__FILE__) + '/lib/pfeed_utils'
|
5
|
+
ActiveRecord::Base.send(:include, ParolkarInnovationLab::SocialNet)
|
6
|
+
|
7
|
+
ActionController::Base.helper do
|
8
|
+
def pfeed_content(pfeed) #FIXME: interesting idea , but currently un-supported
|
9
|
+
controller.send('render_to_string',
|
10
|
+
:partial => "pfeeds/#{pfeed.view_template_name}.html.erb", :locals => {:object => pfeed})
|
11
|
+
end
|
12
|
+
|
13
|
+
def pfeed_item_url(pfeed_item)
|
14
|
+
# same as: polymorphic_url pfeed_item.originator
|
15
|
+
# but no need to query the database
|
16
|
+
send(pfeed_item.originator_type.underscore + '_url', pfeed_item.originator_id)
|
17
|
+
end
|
18
|
+
end
|
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class PfeedCustomizationGenerator < Rails::Generators::Base
|
2
|
+
attr_reader :past_classname
|
3
|
+
attr_reader :past_varname
|
4
|
+
|
5
|
+
source_root File.expand_path('../templates', __FILE__)
|
6
|
+
argument :model, :type => :string, :required => true
|
7
|
+
argument :action_name, :type => :string,:required => true
|
8
|
+
|
9
|
+
def initialize_pfeed_customization
|
10
|
+
raise "#{model.to_s.classify} must define '#{action_name.underscore}' method" unless model.to_s.classify.constantize.methods.include? action_name.underscore
|
11
|
+
|
12
|
+
@model = model
|
13
|
+
@current_action = action_name.to_s.underscore
|
14
|
+
@past_action = ParolkarInnovationLab::SocialNet::PfeedUtils.attempt_pass_tense(@current_action)
|
15
|
+
@past = @model.downcase + '_' + @past_action
|
16
|
+
@past_classname = @model.capitalize + @past_action.capitalize
|
17
|
+
@past_varname = @model.downcase + '_' + @past_action.downcase
|
18
|
+
@model_filename = @past + '.rb'
|
19
|
+
@view_filename = '_' + @past + '.html.erb'
|
20
|
+
end
|
21
|
+
|
22
|
+
def manifest
|
23
|
+
template('pfeed_model.rb', "app/models/pfeeds/#{@model_filename}")
|
24
|
+
template('pfeed_view.html.erb', "app/views/pfeeds/#{@view_filename}")
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
end
|
data/lib/pfeed.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Author: Er Abhishek Parolkar
|
2
|
+
if defined?(Rails)
|
3
|
+
module AuditTrail
|
4
|
+
require "rails"
|
5
|
+
require 'rubygems'
|
6
|
+
require 'rails'
|
7
|
+
require 'active_record'
|
8
|
+
require 'action_controller'
|
9
|
+
require 'active_support'
|
10
|
+
require 'pfeed/pfeed'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
|
16
|
+
ActiveRecord::Base.send(:include, ParolkarInnovationLab::SocialNet)
|
17
|
+
|
18
|
+
ActionController::Base.helper do
|
19
|
+
def pfeed_content(pfeed) #FIXME: interesting idea , but currently un-supported
|
20
|
+
controller.send('render_to_string',
|
21
|
+
:partial => "pfeeds/#{pfeed.view_template_name}.html.erb", :locals => {:object => pfeed})
|
22
|
+
end
|
23
|
+
|
24
|
+
def pfeed_item_url(pfeed_item)
|
25
|
+
# same as: polymorphic_url pfeed_item.originator
|
26
|
+
# but no need to query the database
|
27
|
+
send(pfeed_item.originator_type.underscore + '_url', pfeed_item.originator_id)
|
28
|
+
end
|
29
|
+
end
|
data/lib/pfeed/pfeed.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
#snippet: https://gist.github.com/89e92409ca9016d2d919
|
2
|
+
|
3
|
+
module ParolkarInnovationLab
|
4
|
+
module SocialNet
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ParolkarInnovationLab::SocialNet::ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
def emits_pfeeds arg_hash # {:on => [] , :for => [:itself , :all_in_its_class], :identified_by => :name, :if => :passes_test?}
|
12
|
+
arg_hash.assert_valid_keys(:on,:for,:if,:unless,:identified_by)
|
13
|
+
[:on, :for].each do |argument|
|
14
|
+
raise ArgumentError, "Expected an argument: #{argument}" if !arg_hash[argument]
|
15
|
+
end
|
16
|
+
|
17
|
+
include ParolkarInnovationLab::SocialNet::InstanceMethods
|
18
|
+
|
19
|
+
method_name_array = [*arg_hash[:on]]
|
20
|
+
class_inheritable_hash :pfeed_audience_hash
|
21
|
+
|
22
|
+
method_name_array.each{|method_name| register_pfeed_audience(method_name,[*arg_hash[:for]].compact) }
|
23
|
+
|
24
|
+
class_inheritable_hash :pfeed_options
|
25
|
+
write_inheritable_hash :pfeed_options, arg_hash.slice(:if,:unless,:identified_by)
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
method_name_array.each { |method_name|
|
30
|
+
method, symbol = method_name.to_s.split /(\!|\?)/
|
31
|
+
symbol = '' if symbol.nil?
|
32
|
+
|
33
|
+
method_to_define = method + '_with_pfeed' + symbol
|
34
|
+
method_to_be_called = method + '_without_pfeed' + symbol
|
35
|
+
eval %[
|
36
|
+
|
37
|
+
module ::ParolkarInnovationLab::SocialNet::PfeedTemp::#{self.to_s}
|
38
|
+
def #{method_to_define}(*a, &b)
|
39
|
+
returned_result = #{method_to_be_called}(*a , &b)
|
40
|
+
method_name_in_past_tense = "#{ParolkarInnovationLab::SocialNet::PfeedUtils.attempt_pass_tense(method)}"
|
41
|
+
PfeedItem.log(self,"#{method_name}",method_name_in_past_tense,returned_result,*a,&b)
|
42
|
+
returned_result
|
43
|
+
end
|
44
|
+
end
|
45
|
+
]
|
46
|
+
|
47
|
+
}
|
48
|
+
|
49
|
+
#TODO : Pfeed.log(self,"#{method_name}",method_name_in_past_tense,returned_result,*a,*b) : this is to be done in a different thread in bg to boost performance & also needs exception handling such that parent call never breaks
|
50
|
+
|
51
|
+
include "::ParolkarInnovationLab::SocialNet::PfeedTemp::#{self.to_s}".constantize # why this? because "define_method((method + '_with_pfeed' + symbol).to_sym) do |*a , &b|" generates syntax error in ruby < 1.8.7
|
52
|
+
|
53
|
+
method_name_array.each { |method_name|
|
54
|
+
method, symbol = method_name.to_s.split /(\!|\?)/
|
55
|
+
symbol = '' if symbol.nil?
|
56
|
+
alias_method_chain (method + symbol), :pfeed
|
57
|
+
}
|
58
|
+
|
59
|
+
has_many :pfeed_items , :as => :originator , :dependent => :destroy #when originator is deleted the pfeed_items gets destroyed too
|
60
|
+
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
def receives_pfeed
|
65
|
+
has_many :pfeed_deliveries , :as => :pfeed_receiver
|
66
|
+
has_many :pfeed_inbox, :class_name => 'PfeedItem', :foreign_key => "pfeed_item_id" , :through => :pfeed_deliveries , :source => :pfeed_item
|
67
|
+
|
68
|
+
write_inheritable_attribute(:is_pfeed_receiver,true)
|
69
|
+
class_inheritable_reader :is_pfeed_receiver
|
70
|
+
end
|
71
|
+
|
72
|
+
def register_pfeed_audience(method_name,audience_arr)
|
73
|
+
write_inheritable_hash(:pfeed_audience_hash, { method_name.to_sym => audience_arr }) # this does a merge
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
module PfeedTemp
|
78
|
+
# Required for temporarily injecting new methods
|
79
|
+
end
|
80
|
+
module InstanceMethods
|
81
|
+
|
82
|
+
def itself
|
83
|
+
self
|
84
|
+
end
|
85
|
+
def all_in_its_class
|
86
|
+
self.class.find :all
|
87
|
+
end
|
88
|
+
|
89
|
+
def pfeed_recent_item_timestamp
|
90
|
+
self.pfeed_deliveries.last.created_at
|
91
|
+
rescue
|
92
|
+
nil
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
#let private methods come here
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
|