bright 0.1.0 → 1.1
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.
- checksums.yaml +5 -5
- data/LICENSE.txt +1 -1
- data/bright.gemspec +7 -5
- data/lib/bright/address.rb +6 -8
- data/lib/bright/connection.rb +52 -44
- data/lib/bright/contact.rb +53 -0
- data/lib/bright/cursor_response_collection.rb +45 -0
- data/lib/bright/email_address.rb +7 -0
- data/lib/bright/errors.rb +13 -3
- data/lib/bright/helpers/blank_helper.rb +55 -0
- data/lib/bright/helpers/boolean_parser_helper.rb +13 -0
- data/lib/bright/phone_number.rb +20 -0
- data/lib/bright/response_collection.rb +20 -19
- data/lib/bright/school.rb +13 -4
- data/lib/bright/sis_apis/aeries.rb +36 -35
- data/lib/bright/sis_apis/base.rb +31 -5
- data/lib/bright/sis_apis/bright_sis.rb +305 -0
- data/lib/bright/sis_apis/infinite_campus.rb +376 -18
- data/lib/bright/sis_apis/power_school.rb +112 -69
- data/lib/bright/sis_apis/skyward.rb +276 -0
- data/lib/bright/sis_apis/synergy.rb +2 -2
- data/lib/bright/sis_apis/tsis.rb +44 -42
- data/lib/bright/student.rb +57 -14
- data/lib/bright/version.rb +1 -1
- data/lib/bright.rb +28 -14
- metadata +50 -15
@@ -2,29 +2,29 @@ module Bright
|
|
2
2
|
module SisApi
|
3
3
|
class Aeries < Base
|
4
4
|
DATE_FORMAT = '%Y-%m-%dT%H:%M:%S'
|
5
|
-
|
5
|
+
|
6
6
|
@@description = "Connects to the Aeries API for accessing student information"
|
7
7
|
@@doc_url = "http://www.aeries.com/downloads/docs.1234/TechnicalSpecs/Aeries_API_Documentation.pdf"
|
8
8
|
@@api_version = ""
|
9
|
-
|
9
|
+
|
10
10
|
attr_accessor :connection_options
|
11
|
-
|
11
|
+
|
12
12
|
def initialize(options = {})
|
13
13
|
self.connection_options = options[:connection] || {}
|
14
14
|
# {
|
15
|
-
# :certficate => "",
|
15
|
+
# :certficate => "",
|
16
16
|
# :uri => ""
|
17
17
|
# }
|
18
18
|
end
|
19
|
-
|
19
|
+
|
20
20
|
def get_student_by_api_id(api_id)
|
21
21
|
get_students({:api_id => api_id, :limit => 1}).first
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
24
|
def get_student(params)
|
25
25
|
get_students(params.merge(:limit => 1)).first
|
26
26
|
end
|
27
|
-
|
27
|
+
|
28
28
|
def get_students(params)
|
29
29
|
if params.has_key?(:school) or params.has_key?(:school_api_id)
|
30
30
|
school_api_id = params.delete(:school) || params.delete(:school_api_id)
|
@@ -32,7 +32,7 @@ module Bright
|
|
32
32
|
else
|
33
33
|
threads = []
|
34
34
|
get_schools.each do |school|
|
35
|
-
threads << Thread.new do
|
35
|
+
threads << Thread.new do
|
36
36
|
get_students_by_school(school, params)
|
37
37
|
end
|
38
38
|
end
|
@@ -40,7 +40,7 @@ module Bright
|
|
40
40
|
end
|
41
41
|
filter_students_by_params(students, params)
|
42
42
|
end
|
43
|
-
|
43
|
+
|
44
44
|
def get_students_by_school(school, params = {})
|
45
45
|
school_api_id = school.is_a?(School) ? school.api_id : school
|
46
46
|
if params.has_key?(:api_id)
|
@@ -53,21 +53,21 @@ module Bright
|
|
53
53
|
students_response_hash = self.request(:get, path, self.map_student_search_params(params))
|
54
54
|
students_response_hash.collect{|shsh| Student.new(convert_to_student_data(shsh))}
|
55
55
|
end
|
56
|
-
|
57
|
-
def create_student(student
|
56
|
+
|
57
|
+
def create_student(student)
|
58
58
|
raise NotImplementedError
|
59
59
|
end
|
60
|
-
|
61
|
-
def update_student(student
|
60
|
+
|
61
|
+
def update_student(student)
|
62
62
|
raise NotImplementedError
|
63
63
|
end
|
64
|
-
|
64
|
+
|
65
65
|
def get_schools(params = {})
|
66
66
|
schools_response_hash = self.request(:get, 'api/v2/schools', params)
|
67
|
-
|
67
|
+
|
68
68
|
schools_response_hash.collect{|h| School.new(convert_to_school_data(h))}
|
69
69
|
end
|
70
|
-
|
70
|
+
|
71
71
|
def request(method, path, params = {})
|
72
72
|
uri = "#{self.connection_options[:uri]}/#{path}"
|
73
73
|
body = nil
|
@@ -78,11 +78,12 @@ module Bright
|
|
78
78
|
body = JSON.dump(params)
|
79
79
|
end
|
80
80
|
|
81
|
-
|
81
|
+
response = connection_retry_wrapper {
|
82
|
+
connection = Bright::Connection.new(uri)
|
83
|
+
headers = self.headers_for_auth
|
84
|
+
connection.request(method, body, headers)
|
85
|
+
}
|
82
86
|
|
83
|
-
connection = Bright::Connection.new(uri)
|
84
|
-
response = connection.request(method, body, headers)
|
85
|
-
|
86
87
|
if !response.error?
|
87
88
|
response_hash = JSON.parse(response.body)
|
88
89
|
else
|
@@ -91,55 +92,55 @@ module Bright
|
|
91
92
|
end
|
92
93
|
response_hash
|
93
94
|
end
|
94
|
-
|
95
|
+
|
95
96
|
protected
|
96
|
-
|
97
|
+
|
97
98
|
def map_student_search_params(attrs)
|
98
99
|
attrs
|
99
100
|
end
|
100
|
-
|
101
|
+
|
101
102
|
def convert_to_student_data(attrs)
|
102
103
|
cattrs = {}
|
103
|
-
|
104
|
+
|
104
105
|
cattrs[:first_name] = attrs["FirstName"]
|
105
106
|
cattrs[:middle_name] = attrs["MiddleName"]
|
106
107
|
cattrs[:last_name] = attrs["LastName"]
|
107
|
-
|
108
|
+
|
108
109
|
cattrs[:api_id] = attrs["PermanentID"]
|
109
110
|
cattrs[:sis_student_id] = attrs["StudentNumber"]
|
110
111
|
cattrs[:state_student_id] = attrs["StateStudentID"]
|
111
|
-
|
112
|
+
|
112
113
|
cattrs[:gender] = attrs["Sex"]
|
113
114
|
if attrs["Birthdate"]
|
114
|
-
begin
|
115
|
+
begin
|
115
116
|
cattrs[:birth_date] = Date.strptime(attrs["Birthdate"], DATE_FORMAT)
|
116
117
|
rescue => e
|
117
118
|
puts "#{e.inspect} #{bd}"
|
118
119
|
end
|
119
120
|
end
|
120
|
-
|
121
|
+
|
121
122
|
#SchoolCode
|
122
|
-
|
123
|
+
|
123
124
|
cattrs.reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?}
|
124
125
|
end
|
125
|
-
|
126
|
+
|
126
127
|
def convert_to_school_data(attrs)
|
127
128
|
cattrs = {}
|
128
|
-
|
129
|
+
|
129
130
|
cattrs[:api_id] = attrs["SchoolCode"]
|
130
131
|
cattrs[:name] = attrs["Name"]
|
131
132
|
cattrs[:number] = attrs["SchoolCode"]
|
132
|
-
|
133
|
+
|
133
134
|
cattrs.reject{|k,v| v.respond_to?(:empty?) ? v.empty? : v.nil?}
|
134
135
|
end
|
135
|
-
|
136
|
+
|
136
137
|
def headers_for_auth
|
137
138
|
{
|
138
139
|
'AERIES-CERT' => self.connection_options[:certificate],
|
139
140
|
'Content-Type' => "application/json"
|
140
141
|
}
|
141
142
|
end
|
142
|
-
|
143
|
+
|
143
144
|
end
|
144
145
|
end
|
145
|
-
end
|
146
|
+
end
|
data/lib/bright/sis_apis/base.rb
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
module Bright
|
2
2
|
module SisApi
|
3
3
|
class Base
|
4
|
-
|
4
|
+
|
5
5
|
def filter_students_by_params(students, params)
|
6
6
|
total = params[:limit]
|
7
7
|
count = 0
|
8
8
|
found = []
|
9
|
-
|
9
|
+
|
10
10
|
keys = (Student.attribute_names & params.keys.collect(&:to_sym))
|
11
11
|
puts "filtering on #{keys.join(",")}"
|
12
12
|
students.each do |student|
|
13
13
|
break if total and count >= total
|
14
|
-
|
14
|
+
|
15
15
|
should = (keys).all? do |m|
|
16
16
|
student.send(m) =~ Regexp.new(Regexp.escape(params[m]), Regexp::IGNORECASE)
|
17
17
|
end
|
@@ -20,7 +20,33 @@ module Bright
|
|
20
20
|
end
|
21
21
|
found
|
22
22
|
end
|
23
|
-
|
23
|
+
|
24
|
+
def connection_retry_wrapper(&block)
|
25
|
+
retry_attempts = connection_options[:retry_attempts] || 2
|
26
|
+
retries = 0
|
27
|
+
begin
|
28
|
+
yield
|
29
|
+
rescue Bright::ResponseError => e
|
30
|
+
retries += 1
|
31
|
+
if e.server_error? && retries <= retry_attempts.to_i
|
32
|
+
puts "retrying #{retries}: #{e.class.to_s} - #{e.to_s}"
|
33
|
+
sleep(retries * 3)
|
34
|
+
retry
|
35
|
+
else
|
36
|
+
raise
|
37
|
+
end
|
38
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Net::ReadTimeout, Net::OpenTimeout, EOFError => e
|
39
|
+
retries += 1
|
40
|
+
if retries <= retry_attempts.to_i
|
41
|
+
puts "retrying #{retries}: #{e.class.to_s} - #{e.to_s}"
|
42
|
+
sleep(retries * 3)
|
43
|
+
retry
|
44
|
+
else
|
45
|
+
raise
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
24
50
|
end
|
25
51
|
end
|
26
|
-
end
|
52
|
+
end
|
@@ -0,0 +1,305 @@
|
|
1
|
+
module Bright
|
2
|
+
module SisApi
|
3
|
+
class BrightSis < Base
|
4
|
+
|
5
|
+
@@description = "Connects to the Bright SIS Data Store"
|
6
|
+
@@doc_url = ""
|
7
|
+
@@api_version = "v1"
|
8
|
+
|
9
|
+
attr_accessor :connection_options
|
10
|
+
|
11
|
+
DEMOGRAPHICS_CONVERSION = {
|
12
|
+
"I"=>"American Indian Or Alaska Native",
|
13
|
+
"A"=>"Asian",
|
14
|
+
"B"=>"Black Or African American",
|
15
|
+
"P"=>"Native Hawaiian Or Other Pacific Islander",
|
16
|
+
"W"=>"White",
|
17
|
+
"M"=>"Other"
|
18
|
+
}
|
19
|
+
|
20
|
+
def initialize(options = {})
|
21
|
+
self.connection_options = options[:connection] || {}
|
22
|
+
# {
|
23
|
+
# :access_token => ""
|
24
|
+
# :uri => ""
|
25
|
+
# }
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_student_by_api_id(api_id, options = {})
|
29
|
+
self.get_students({"uuid" => api_id}, options.merge(:limit => 1, :wrap_in_collection => false)).first
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_student(params = {}, options = {})
|
33
|
+
self.get_students(params, options.merge(:limit => 1, :wrap_in_collection => false)).first
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_students(params = {}, options = {})
|
37
|
+
params[:limit] = params[:limit] || options[:limit] || 100
|
38
|
+
students_response_hash = self.request(:get, 'students', self.map_student_search_params(params))
|
39
|
+
total_results = students_response_hash[:response_headers]["total"].to_i
|
40
|
+
if students_response_hash and students_response_hash["students"]
|
41
|
+
students_hash = [students_response_hash["students"]].flatten
|
42
|
+
|
43
|
+
students = students_hash.compact.collect {|st_hsh|
|
44
|
+
Student.new(convert_to_student_data(st_hsh))
|
45
|
+
}
|
46
|
+
end
|
47
|
+
if options[:wrap_in_collection] != false
|
48
|
+
api = self
|
49
|
+
load_more_call = proc { |page|
|
50
|
+
# pages start at one, so add a page here
|
51
|
+
params[:offset] = (params[:limit].to_i * page)
|
52
|
+
api.get_students(params, {:wrap_in_collection => false})
|
53
|
+
}
|
54
|
+
ResponseCollection.new({
|
55
|
+
:seed_page => students,
|
56
|
+
:total => total_results,
|
57
|
+
:per_page => params[:limit],
|
58
|
+
:load_more_call => load_more_call,
|
59
|
+
:no_threads => options[:no_threads]
|
60
|
+
})
|
61
|
+
else
|
62
|
+
students
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def create_student(student)
|
67
|
+
raise NotImplementedError
|
68
|
+
end
|
69
|
+
|
70
|
+
def update_student(student)
|
71
|
+
raise NotImplementedError
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_schools(params = {}, options = {})
|
75
|
+
params[:limit] = params[:limit] || options[:limit] || 100
|
76
|
+
schools_response_hash = self.request(:get, 'schools', self.map_school_search_params(params))
|
77
|
+
total_results = schools_response_hash[:response_headers]["total"].to_i
|
78
|
+
if schools_response_hash and schools_response_hash["schools"]
|
79
|
+
schools_hash = [schools_response_hash["schools"]].flatten
|
80
|
+
|
81
|
+
schools = schools_hash.compact.collect {|st_hsh|
|
82
|
+
School.new(convert_to_school_data(st_hsh))
|
83
|
+
}
|
84
|
+
end
|
85
|
+
if options[:wrap_in_collection] != false
|
86
|
+
api = self
|
87
|
+
load_more_call = proc { |page|
|
88
|
+
# pages start at one, so add a page here
|
89
|
+
params[:offset] = (params[:limit].to_i * page)
|
90
|
+
api.get_schools(params, {:wrap_in_collection => false})
|
91
|
+
}
|
92
|
+
ResponseCollection.new({
|
93
|
+
:seed_page => schools,
|
94
|
+
:total => total_results,
|
95
|
+
:per_page => params[:limit],
|
96
|
+
:load_more_call => load_more_call,
|
97
|
+
:no_threads => options[:no_threads]
|
98
|
+
})
|
99
|
+
else
|
100
|
+
schools
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def request(method, path, params = {})
|
105
|
+
uri = "#{self.connection_options[:uri]}/#{path}"
|
106
|
+
body = nil
|
107
|
+
if method == :get
|
108
|
+
query = URI.encode_www_form(params)
|
109
|
+
uri += "?#{query}" unless query.strip == ""
|
110
|
+
else
|
111
|
+
body = JSON.dump(params)
|
112
|
+
end
|
113
|
+
|
114
|
+
response = connection_retry_wrapper {
|
115
|
+
connection = Bright::Connection.new(uri)
|
116
|
+
headers = self.headers_for_auth
|
117
|
+
connection.request(method, body, headers)
|
118
|
+
}
|
119
|
+
|
120
|
+
if !response.error?
|
121
|
+
response_hash = JSON.parse(response.body)
|
122
|
+
response_hash[:response_headers] = response.headers
|
123
|
+
else
|
124
|
+
puts "#{response.inspect}"
|
125
|
+
puts "#{response.body}"
|
126
|
+
end
|
127
|
+
response_hash
|
128
|
+
end
|
129
|
+
|
130
|
+
protected
|
131
|
+
|
132
|
+
def headers_for_auth(uri)
|
133
|
+
{"Authorization" => "Token token=#{self.connection_options[:access_token]}"}
|
134
|
+
end
|
135
|
+
|
136
|
+
def map_student_search_params(attrs)
|
137
|
+
filter_params = {}
|
138
|
+
attrs.each do |k,v|
|
139
|
+
case k.to_s
|
140
|
+
when "api_id"
|
141
|
+
filter_params["uuid"] = v
|
142
|
+
when "sis_student_id"
|
143
|
+
filter_params["student_number"] = v
|
144
|
+
when "state_student_id"
|
145
|
+
filter_params["state_id"] = v
|
146
|
+
else
|
147
|
+
filter_params[k] = v
|
148
|
+
end
|
149
|
+
end
|
150
|
+
return filter_params
|
151
|
+
end
|
152
|
+
|
153
|
+
def convert_to_student_data(student_params)
|
154
|
+
return {} if student_params.nil?
|
155
|
+
student_data_hsh = {
|
156
|
+
:api_id => student_params["uuid"],
|
157
|
+
:first_name => student_params["first_name"],
|
158
|
+
:middle_name => student_params["middle_name"],
|
159
|
+
:last_name => student_params["last_name"],
|
160
|
+
:sis_student_id => student_params["student_number"],
|
161
|
+
:state_student_id => student_params["state_id"],
|
162
|
+
:grade => student_params["grade"],
|
163
|
+
:grade_school_year => student_params["grade_school_year"],
|
164
|
+
:projected_graduation_year => student_params["graduation_year"],
|
165
|
+
:gender => student_params["gender"],
|
166
|
+
:frl_status => student_params["frl_status"],
|
167
|
+
:image => student_params["picture"],
|
168
|
+
:hispanic_ethnicity => student_params["hispanic_latino"],
|
169
|
+
:last_modified => student_params["updated_at"]
|
170
|
+
}.reject{|k,v| v.blank?}
|
171
|
+
unless student_params["birthdate"].blank?
|
172
|
+
student_data_hsh[:birth_date] = Date.parse(student_params["birthdate"]).to_s
|
173
|
+
end
|
174
|
+
|
175
|
+
DEMOGRAPHICS_CONVERSION.each do |demographics_key, demographics_value|
|
176
|
+
if student_params["race"].to_s.upcase.include?(demographics_key)
|
177
|
+
student_data_hsh[:race] ||= []
|
178
|
+
student_data_hsh[:race] << demographics_value
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
unless student_params["addresses"].blank?
|
183
|
+
student_data_hsh[:addresses] = student_params["addresses"].collect do |address_params|
|
184
|
+
convert_to_address_data(address_params)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
unless student_params["phone_numbers"].blank?
|
189
|
+
student_data_hsh[:phone_numbers] = student_params["phone_numbers"].collect do |phone_params|
|
190
|
+
convert_to_phone_number_data(phone_params)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
unless student_params["email_addresses"].blank?
|
195
|
+
student_data_hsh[:email_address] = {
|
196
|
+
:email_address => student_params["email_addresses"].first["email_address"]
|
197
|
+
}
|
198
|
+
end
|
199
|
+
|
200
|
+
unless student_params["school"].blank?
|
201
|
+
student_data_hsh[:school] = convert_to_school_data(student_params["school"])
|
202
|
+
end
|
203
|
+
|
204
|
+
unless student_params["contacts"].blank?
|
205
|
+
student_data_hsh[:contacts] = student_params["contacts"].collect do |contact_params|
|
206
|
+
contact_data_hsh = {
|
207
|
+
:api_id => contact_params["uuid"],
|
208
|
+
:first_name => contact_params["first_name"],
|
209
|
+
:middle_name => contact_params["middle_name"],
|
210
|
+
:last_name => contact_params["last_name"],
|
211
|
+
:relationship_type => contact_params["relationship"],
|
212
|
+
:sis_student_id => contact_params["sis_id"],
|
213
|
+
:last_modified => contact_params["updated_at"]
|
214
|
+
}
|
215
|
+
unless contact_params["addresses"].blank?
|
216
|
+
contact_data_hsh[:addresses] = contact_params["addresses"].collect do |address_params|
|
217
|
+
convert_to_address_data(address_params)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
unless contact_params["phone_numbers"].blank?
|
221
|
+
contact_data_hsh[:phone_numbers] = contact_params["phone_numbers"].collect do |phone_params|
|
222
|
+
convert_to_phone_number_data(phone_params)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
unless contact_params["email_addresses"].blank?
|
226
|
+
contact_data_hsh[:email_address] = {
|
227
|
+
:email_address => contact_params["email_addresses"].first["email_address"]
|
228
|
+
}
|
229
|
+
end
|
230
|
+
contact_data_hsh.reject{|k,v| v.blank?}
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
return student_data_hsh
|
235
|
+
end
|
236
|
+
|
237
|
+
def map_school_search_params(attrs)
|
238
|
+
filter_params = {}
|
239
|
+
attrs.each do |k,v|
|
240
|
+
case k.to_s
|
241
|
+
when "api_id"
|
242
|
+
filter_params["id"] = v
|
243
|
+
else
|
244
|
+
filter_params[k] = v
|
245
|
+
end
|
246
|
+
end
|
247
|
+
return filter_params
|
248
|
+
end
|
249
|
+
|
250
|
+
def convert_to_phone_number_data(phone_number_params)
|
251
|
+
return {} if phone_number_params.nil?
|
252
|
+
{
|
253
|
+
:phone_number => phone_number_params["phone_number"],
|
254
|
+
:type => phone_number_params["phone_type"]
|
255
|
+
}.reject{|k,v| v.blank?}
|
256
|
+
end
|
257
|
+
|
258
|
+
def convert_to_address_data(address_params)
|
259
|
+
return {} if address_params.nil?
|
260
|
+
{
|
261
|
+
:street => address_params["street"],
|
262
|
+
:apt => address_params["street_line_2"],
|
263
|
+
:city => address_params["city"],
|
264
|
+
:state => address_params["state"],
|
265
|
+
:postal_code => address_params["zip"],
|
266
|
+
:lattitude => address_params["latitude"],
|
267
|
+
:longitude => address_params["longitude"],
|
268
|
+
:type => address_params["address_type"]
|
269
|
+
}.reject{|k,v| v.blank?}
|
270
|
+
end
|
271
|
+
|
272
|
+
def convert_to_school_data(school_params)
|
273
|
+
return {} if school_params.nil?
|
274
|
+
|
275
|
+
school_data_hsh = {
|
276
|
+
:api_id => school_params["id"],
|
277
|
+
:name => school_params["name"],
|
278
|
+
:number => school_params["number"],
|
279
|
+
:state_id => school_params["state_id"],
|
280
|
+
:low_grade => school_params["low_grade"],
|
281
|
+
:high_grade => school_params["high_grade"],
|
282
|
+
:last_modified => school_params["updated_at"]
|
283
|
+
}
|
284
|
+
|
285
|
+
unless school_params["school_address"].blank?
|
286
|
+
school_data_hsh[:address] = {
|
287
|
+
:street => school_params["school_address"],
|
288
|
+
:city => school_params["school_city"],
|
289
|
+
:state => school_params["school_state"],
|
290
|
+
:postal_code => school_params["school_zip"]
|
291
|
+
}
|
292
|
+
end
|
293
|
+
|
294
|
+
unless school_params["school_phone"].blank?
|
295
|
+
school_data_hsh[:phone_number] = {
|
296
|
+
:phone_number => school_params["school_phone"]
|
297
|
+
}
|
298
|
+
end
|
299
|
+
|
300
|
+
return school_data_hsh
|
301
|
+
end
|
302
|
+
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|