google_apps_api 0.1.0 → 0.2.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.
- data/.gitignore +1 -1
- data/VERSION +1 -1
- data/google_apps_api.gemspec +9 -4
- data/lib/config/calendar.yml +36 -9
- data/lib/config/contacts.yml +35 -0
- data/lib/config/profiles.yml +1 -0
- data/lib/config/provisioning.yml +10 -3
- data/lib/google_apps_api/base_api.rb +120 -36
- data/lib/google_apps_api/calendar.rb +305 -30
- data/lib/google_apps_api/contacts.rb +83 -53
- data/lib/google_apps_api/provisioning.rb +66 -242
- data/test/example_connection_config.yml +4 -0
- data/test/google_apps_api_base_api_test.rb +76 -0
- data/test/google_apps_api_calendar_test.rb +225 -54
- data/test/google_apps_api_contacts_test.rb +103 -27
- data/test/google_apps_api_off_domain_calendar_test.rb +27 -0
- data/test/google_apps_api_provisioning_test.rb +3 -4
- data/test/test_helper.rb +5 -0
- metadata +9 -4
- data/private/gapps-config.yml +0 -5
- data/private/userscalendars.xml +0 -113
@@ -10,41 +10,20 @@ module GoogleAppsApi #:nodoc:
|
|
10
10
|
super(:provisioning, *args)
|
11
11
|
end
|
12
12
|
|
13
|
-
def retrieve_user(
|
14
|
-
|
13
|
+
def retrieve_user(user, *args)
|
14
|
+
username = user.kind_of?(UserEntity) ? user.id : user
|
15
|
+
|
16
|
+
options = args.extract_options!.merge(:username => username)
|
17
|
+
request(:retrieve_user, options)
|
15
18
|
end
|
16
19
|
|
17
20
|
|
18
|
-
def retrieve_all_users
|
19
|
-
|
21
|
+
def retrieve_all_users(*args)
|
22
|
+
options = args.extract_options!
|
23
|
+
request(:retrieve_all_users, options)
|
20
24
|
end
|
21
|
-
|
22
|
-
|
23
|
-
# # ex :
|
24
|
-
# # myapps = ProvisioningApi.new('root@mydomain.com','PaSsWoRd')
|
25
|
-
# # list= myapps.retrieve_page_of_users("jsmtih")
|
26
|
-
# # list.each{ |user| puts user.username}
|
27
|
-
# def retrieve_page_of_users(start_username)
|
28
|
-
# param='?startUsername='+start_username
|
29
|
-
# response = request(:user_retrieve_all,param,@headers)
|
30
|
-
# user_feed = Feed.new(response.elements["feed"], UserEntry)
|
31
|
-
# end
|
32
|
-
#
|
33
|
-
#
|
34
|
-
# def contacts_retrieve_all()
|
35
|
-
# response = request(:contacts_retrieve_all,nil, @headers)
|
36
|
-
# end
|
37
|
-
#
|
38
|
-
# Creates an account in your domain, returns a UserEntry instance
|
39
|
-
# params :
|
40
|
-
# username, given_name, family_name and password are required
|
41
|
-
# passwd_hash_function (optional) : nil (default) or "SHA-1"
|
42
|
-
# quota (optional) : nil (default) or integer for limit in MB
|
43
|
-
# ex :
|
44
|
-
# myapps = ProvisioningApi.new('root@mydomain.com','PaSsWoRd')
|
45
|
-
# user = myapps.create('jsmith', 'John', 'Smith', 'p455wD')
|
46
|
-
#
|
47
|
-
# By default, a new user must change his password at first login. Please use update_user if you want to change this just after the creation.
|
25
|
+
|
26
|
+
|
48
27
|
def create_user(username, *args)
|
49
28
|
options = args.extract_options!
|
50
29
|
options.each { |k,v| options[k] = escapeXML(v)}
|
@@ -52,243 +31,88 @@ module GoogleAppsApi #:nodoc:
|
|
52
31
|
res = <<-DESCXML
|
53
32
|
<?xml version="1.0" encoding="UTF-8"?>
|
54
33
|
<atom:entry xmlns:atom="http://www.w3.org/2005/Atom"
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
34
|
+
xmlns:apps="http://schemas.google.com/apps/2006">
|
35
|
+
<atom:category scheme="http://schemas.google.com/g/2005#kind"
|
36
|
+
term="http://schemas.google.com/apps/2006#user"/>
|
37
|
+
<apps:login userName="#{escapeXML(username)}"
|
38
|
+
password="#{options[:password]}" suspended="false"/>
|
39
|
+
<apps:name familyName="#{options[:family_name]}" givenName="#{options[:given_name]}"/>
|
61
40
|
</atom:entry>
|
62
41
|
|
63
42
|
DESCXML
|
64
43
|
|
65
|
-
|
66
|
-
request(:create_user, :body => res.strip)
|
44
|
+
|
45
|
+
request(:create_user, options.merge(:body => res.strip))
|
67
46
|
end
|
68
|
-
|
69
|
-
|
70
|
-
# # params :
|
71
|
-
# # username is required and can't be updated.
|
72
|
-
# # given_name and family_name are required, may be updated.
|
73
|
-
# # if set to nil, every other parameter won't update the attribute.
|
74
|
-
# # passwd_hash_function : string "SHA-1", "MD5" or nil (default)
|
75
|
-
# # admin : string "true" or string "false" or nil (no boolean : true or false).
|
76
|
-
# # suspended : string "true" or string "false" or nil (no boolean : true or false)
|
77
|
-
# # change_passwd : string "true" or string "false" or nil (no boolean : true or false)
|
78
|
-
# # quota : limit en MB, ex : string "2048"
|
79
|
-
# # ex :
|
80
|
-
# # myapps = ProvisioningApi.new('root@mydomain.com','PaSsWoRd')
|
81
|
-
# # user = myapps.update('jsmith', 'John', 'Smith', nil, nil, "true", nil, "true", nil)
|
82
|
-
# # puts user.admin => "true"
|
47
|
+
|
48
|
+
|
83
49
|
def update_user(username, *args)
|
84
50
|
options = args.extract_options!
|
85
51
|
options.each { |k,v| options[k] = escapeXML(v)}
|
86
|
-
|
52
|
+
|
87
53
|
res = <<-DESCXML
|
88
54
|
<?xml version="1.0" encoding="UTF-8"?>
|
89
55
|
<atom:entry xmlns:atom="http://www.w3.org/2005/Atom"
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
56
|
+
xmlns:apps="http://schemas.google.com/apps/2006">
|
57
|
+
<atom:category scheme="http://schemas.google.com/g/2005#kind"
|
58
|
+
term="http://schemas.google.com/apps/2006#user"/>
|
59
|
+
<apps:name familyName="#{options[:family_name]}" givenName="#{options[:given_name]}"/>
|
94
60
|
</atom:entry>
|
95
|
-
|
61
|
+
|
96
62
|
DESCXML
|
97
|
-
request(:update_user, :username => username, :body => res.strip)
|
63
|
+
request(:update_user, options.merge(:username => username, :body => res.strip))
|
98
64
|
end
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
# # user = myapps.rename_user('jsmith','jdoe')
|
105
|
-
# #
|
106
|
-
# # It is recommended to log out rhe user from all browser sessions and service before renaming.
|
107
|
-
# # Once renamed, the old username becomes a nickname of the new username.
|
108
|
-
# # Note from Google: Google Talk will lose all remembered chat invitations after renaming.
|
109
|
-
# # The user must request permission to chat with friends again.
|
110
|
-
# # Also, when a user is renamed, the old username is retained as a nickname to ensure continuous mail delivery in the case of email forwarding settings.
|
111
|
-
# # To remove the nickname, you should issue an HTTP DELETE to the nicknames feed after renaming.
|
112
|
-
# def rename_user(username, new_username)
|
113
|
-
# msg = RequestMessage.new
|
114
|
-
# msg.about_login(new_username)
|
115
|
-
# msg.add_path('https://'+@@google_host+@action[:user_rename][:path]+username)
|
116
|
-
# response = request(:user_update,username,@headers, msg.to_s)
|
117
|
-
# end
|
118
|
-
#
|
119
|
-
# # Suspends an account in your domain, returns a UserEntry instance
|
120
|
-
# # ex :
|
121
|
-
# # myapps = ProvisioningApi.new('root@mydomain.com','PaSsWoRd')
|
122
|
-
# # user = myapps.suspend('jsmith')
|
123
|
-
# # puts user.suspended => "true"
|
124
|
-
# def suspend_user(username)
|
125
|
-
# msg = RequestMessage.new
|
126
|
-
# msg.about_login(username,nil,nil,nil,"true")
|
127
|
-
# msg.add_path('https://'+@@google_host+@action[:user_update][:path]+username)
|
128
|
-
# response = request(:user_update,username,@headers, msg.to_s)
|
129
|
-
# user_entry = UserEntry.new(response.elements["entry"])
|
130
|
-
# end
|
131
|
-
#
|
132
|
-
# # Restores a suspended account in your domain, returns a UserEntry instance
|
133
|
-
# # ex :
|
134
|
-
# # myapps = ProvisioningApi.new('root@mydomain.com','PaSsWoRd')
|
135
|
-
# # user = myapps.restore('jsmith')
|
136
|
-
# # puts user.suspended => "false"
|
137
|
-
# def restore_user(username)
|
138
|
-
# msg = RequestMessage.new
|
139
|
-
# msg.about_login(username,nil,nil,nil,"false")
|
140
|
-
# msg.add_path('https://'+@@google_host+@action[:user_update][:path]+username)
|
141
|
-
# response = request(:user_update,username,@headers, msg.to_s)
|
142
|
-
# user_entry = UserEntry.new(response.elements["entry"])
|
143
|
-
# end
|
144
|
-
#
|
145
|
-
# # Deletes an account in your domain
|
146
|
-
# # ex :
|
147
|
-
# # myapps = ProvisioningApi.new('root@mydomain.com','PaSsWoRd')
|
148
|
-
# # myapps.delete('jsmith')
|
149
|
-
def delete_user(username)
|
150
|
-
response = request(:delete_user, :username => username)
|
65
|
+
|
66
|
+
|
67
|
+
def delete_user(username, *args)
|
68
|
+
options = args.extract_options!.merge(:username => username)
|
69
|
+
request(:delete_user, options)
|
151
70
|
end
|
152
71
|
|
153
72
|
|
154
73
|
end
|
155
74
|
|
156
|
-
|
157
|
-
#
|
158
|
-
#
|
159
|
-
# class RequestMessage < Document #:nodoc:
|
160
|
-
# # Request message constructor.
|
161
|
-
# # parameter type : "user", "nickname" or "emailList"
|
162
|
-
#
|
163
|
-
# # creates the object and initiates the construction
|
164
|
-
# def initialize
|
165
|
-
# super '<?xml version="1.0" encoding="UTF-8"?>'
|
166
|
-
# self.add_element "atom:entry", {"xmlns:apps" => "http://schemas.google.com/apps/2006",
|
167
|
-
# "xmlns:gd" => "http://schemas.google.com/g/2005",
|
168
|
-
# "xmlns:atom" => "http://www.w3.org/2005/Atom"}
|
169
|
-
#
|
170
|
-
# self.elements["atom:entry"].add_element "atom:category", {"scheme" => "http://schemas.google.com/g/2005#kind"}
|
171
|
-
#
|
172
|
-
# end
|
173
|
-
#
|
174
|
-
# # adds <atom:id> element in the message body. Url is inserted as a text.
|
175
|
-
# def add_path(url)
|
176
|
-
# self.elements["atom:entry"].add_element "atom:id"
|
177
|
-
# self.elements["atom:entry/atom:id"].text = url
|
178
|
-
# end
|
179
|
-
#
|
180
|
-
# # adds <apps:emailList> element in the message body.
|
181
|
-
# def about_email_list(email_list)
|
182
|
-
# self.elements["atom:entry/atom:category"].add_attribute("term", "http://schemas.google.com/apps/2006#emailList")
|
183
|
-
# self.elements["atom:entry"].add_element "apps:emailList", {"name" => email_list }
|
184
|
-
# end
|
185
|
-
#
|
186
|
-
# # adds <apps:property> element in the message body for a group.
|
187
|
-
# def about_group(group_id, properties)
|
188
|
-
# self.elements["atom:entry/atom:category"].add_attribute("term", "http://schemas.google.com/apps/2006#emailList")
|
189
|
-
# self.elements["atom:entry"].add_element "apps:property", {"name" => "groupId", "value" => group_id }
|
190
|
-
# self.elements["atom:entry"].add_element "apps:property", {"name" => "groupName", "value" => properties[0] }
|
191
|
-
# self.elements["atom:entry"].add_element "apps:property", {"name" => "description", "value" => properties[1] }
|
192
|
-
# self.elements["atom:entry"].add_element "apps:property", {"name" => "emailPermission", "value" => properties[2] }
|
193
|
-
# end
|
194
|
-
#
|
195
|
-
# # adds <apps:property> element in the message body for a member.
|
196
|
-
# def about_member(email_address)
|
197
|
-
# self.elements["atom:entry/atom:category"].add_attribute("term", "http://schemas.google.com/apps/2006#user")
|
198
|
-
# self.elements["atom:entry"].add_element "apps:property", {"name" => "memberId", "value" => email_address }
|
199
|
-
# end
|
200
|
-
#
|
201
|
-
# # adds <apps:property> element in the message body for an owner.
|
202
|
-
# def about_owner(email_address)
|
203
|
-
# self.elements["atom:entry/atom:category"].add_attribute("term", "http://schemas.google.com/apps/2006#user")
|
204
|
-
# self.elements["atom:entry"].add_element "apps:property", {"name" => "email", "value" => email_address }
|
205
|
-
# end
|
206
|
-
#
|
207
|
-
#
|
208
|
-
# # adds <apps:login> element in the message body.
|
209
|
-
# # warning : if valued admin, suspended, or change_passwd_at_next_login must be the STRINGS "true" or "false", not the boolean true or false
|
210
|
-
# # when needed to construct the message, should always been used before other "about_" methods so that the category tag can be overwritten
|
211
|
-
# # only values permitted for hash_function_function_name : "SHA-1", "MD5" or nil
|
212
|
-
# def about_login(user_name, passwd=nil, hash_function_name=nil, admin=nil, suspended=nil, change_passwd_at_next_login=nil)
|
213
|
-
# self.elements["atom:entry/atom:category"].add_attribute("term", "http://schemas.google.com/apps/2006#user")
|
214
|
-
# self.elements["atom:entry"].add_element "apps:login", {"userName" => user_name }
|
215
|
-
# self.elements["atom:entry/apps:login"].add_attribute("password", passwd) if not passwd.nil?
|
216
|
-
# self.elements["atom:entry/apps:login"].add_attribute("hashFunctionName", hash_function_name) if not hash_function_name.nil?
|
217
|
-
# self.elements["atom:entry/apps:login"].add_attribute("admin", admin) if not admin.nil?
|
218
|
-
# self.elements["atom:entry/apps:login"].add_attribute("suspended", suspended) if not suspended.nil?
|
219
|
-
# self.elements["atom:entry/apps:login"].add_attribute("changePasswordAtNextLogin", change_passwd_at_next_login) if not change_passwd_at_next_login.nil?
|
220
|
-
# return self
|
221
|
-
# end
|
222
|
-
#
|
223
|
-
# # adds <apps:quota> in the message body.
|
224
|
-
# # limit in MB: integer
|
225
|
-
# def about_quota(limit)
|
226
|
-
# self.elements["atom:entry"].add_element "apps:quota", {"limit" => limit }
|
227
|
-
# return self
|
228
|
-
# end
|
229
|
-
#
|
230
|
-
# # adds <apps:name> in the message body.
|
231
|
-
# def about_name(family_name, given_name)
|
232
|
-
# self.elements["atom:entry"].add_element "apps:name", {"familyName" => family_name, "givenName" => given_name }
|
233
|
-
# return self
|
234
|
-
# end
|
235
|
-
#
|
236
|
-
# # adds <apps:nickname> in the message body.
|
237
|
-
# def about_nickname(name)
|
238
|
-
# self.elements["atom:entry/atom:category"].add_attribute("term", "http://schemas.google.com/apps/2006#nickname")
|
239
|
-
# self.elements["atom:entry"].add_element "apps:nickname", {"name" => name}
|
240
|
-
# return self
|
241
|
-
# end
|
242
|
-
#
|
243
|
-
# # adds <gd:who> in the message body.
|
244
|
-
# def about_who(email)
|
245
|
-
# self.elements["atom:entry"].add_element "gd:who", {"email" => email }
|
246
|
-
# return self
|
247
|
-
# end
|
248
|
-
#
|
249
|
-
# end
|
250
|
-
# end
|
251
75
|
end
|
252
|
-
|
253
76
|
|
254
77
|
|
255
|
-
class
|
256
|
-
attr_accessor :given_name, :family_name, :username, :suspended, :ip_whitelisted, :admin, :change_password_at_next_login, :agreed_to_terms, :quota_limit
|
257
|
-
|
258
|
-
def initialize(
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
@
|
263
|
-
@
|
264
|
-
@
|
265
|
-
|
266
|
-
@
|
267
|
-
@
|
268
|
-
@
|
78
|
+
class UserEntity < Entity
|
79
|
+
attr_accessor :given_name, :family_name, :username, :suspended, :ip_whitelisted, :admin, :change_password_at_next_login, :agreed_to_terms, :quota_limit, :domain
|
80
|
+
|
81
|
+
def initialize(*args)
|
82
|
+
options = args.extract_options!
|
83
|
+
if (_xml = options[:xml])
|
84
|
+
xml = _xml.at_css("entry") || _xml
|
85
|
+
@kind = "user"
|
86
|
+
@id = xml.at_css("apps|login").attribute("userName").content
|
87
|
+
@domain = xml.at_css("id").content.gsub(/^.+\/feeds\/([^\/]+)\/.+$/,"\\1")
|
88
|
+
|
89
|
+
@family_name = xml.at_css("apps|name").attribute("familyName").content
|
90
|
+
@given_name = xml.at_css("apps|name").attribute("givenName").content
|
91
|
+
@suspended = xml.at_css("apps|login").attribute("suspended").content
|
92
|
+
@ip_whitelisted = xml.at_css("apps|login").attribute("ipWhitelisted").content
|
93
|
+
@admin = xml.at_css("apps|login").attribute("admin").content
|
94
|
+
@change_password_at_next_login = xml.at_css("apps|login").attribute("changePasswordAtNextLogin").content
|
95
|
+
@agreed_to_terms = xml.at_css("apps|login").attribute("agreedToTerms").content
|
96
|
+
@quota_limit = xml.at_css("apps|quota").attribute("limit").content
|
97
|
+
else
|
98
|
+
if args.first.kind_of?(String)
|
99
|
+
super(:user => args.first)
|
100
|
+
else
|
101
|
+
super(options.merge(:kind => "user"))
|
102
|
+
end
|
269
103
|
end
|
270
104
|
end
|
271
|
-
|
272
|
-
def
|
273
|
-
|
105
|
+
|
106
|
+
def entity_for_base_calendar
|
107
|
+
CalendarEntity.new(self.full_id)
|
274
108
|
end
|
275
|
-
|
276
|
-
def
|
277
|
-
|
109
|
+
|
110
|
+
def get_base_calendar(c_api, *args)
|
111
|
+
c_api.retrieve_calendar_for_user(self.entity_for_base_calendar, self, *args)
|
278
112
|
end
|
279
|
-
|
280
|
-
def
|
281
|
-
|
113
|
+
|
114
|
+
def get_calendars(c_api, *args)
|
115
|
+
c_api.retrieve_calendars_for_user(self, *args)
|
282
116
|
end
|
283
|
-
#
|
284
|
-
# def add_message
|
285
|
-
# Nokogiri::XML::Builder.new { |xml|
|
286
|
-
# xml.entry(:xmlns => "http://www.w3.org/2005/Atom") {
|
287
|
-
# xml.id_ {
|
288
|
-
# xml.text id.to_s
|
289
|
-
# }
|
290
|
-
# }
|
291
|
-
# }.to_xml
|
292
|
-
# end
|
293
117
|
end
|
294
118
|
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class GoogleAppsApiBaseApiTest < Test::Unit::TestCase
|
4
|
+
include GoogleAppsApi
|
5
|
+
|
6
|
+
context "given an example entity" do
|
7
|
+
setup do
|
8
|
+
@en = Entity.new(:kind => "user", :id => "test1", :domain => "ocelot.cul.columbia.edu")
|
9
|
+
@c_en = Entity.new(:kind => "calendar", :id => "js235", :domain => "ocelot.cul.columbia.edu")
|
10
|
+
@d_en = Entity.new(:kind => "domain", :id => "ocelot.cul.columbia.edu")
|
11
|
+
@co_en = Entity.new(:kind => "contact", :id => "12345")
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
should "reject items without kind and id" do
|
16
|
+
assert_raises ArgumentError do
|
17
|
+
Entity.new(:id => "test1", :domain => "oc")
|
18
|
+
end
|
19
|
+
|
20
|
+
assert_raises ArgumentError do
|
21
|
+
Entity.new(:kind => "user", :domain => "oc")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
should "accept a user argument" do
|
26
|
+
u_en = Entity.new(:user => "test1", :domain => "ocelot.cul.columbia.edu")
|
27
|
+
|
28
|
+
assert_equal @en, u_en
|
29
|
+
end
|
30
|
+
|
31
|
+
should "split domains out" do
|
32
|
+
u_en = Entity.new(:user => "test1@ocelot.cul.columbia.edu")
|
33
|
+
|
34
|
+
assert_equal @en, u_en
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
should "split encoded domains out" do
|
39
|
+
u_en = Entity.new(:user => "test1%40ocelot.cul.columbia.edu")
|
40
|
+
|
41
|
+
assert_equal @en, u_en
|
42
|
+
end
|
43
|
+
|
44
|
+
should "be able to display the encoded and non-encoded versions" do
|
45
|
+
assert_equal "test1@ocelot.cul.columbia.edu", @en.full_id
|
46
|
+
assert_equal "test1%40ocelot.cul.columbia.edu", @en.full_id_escaped
|
47
|
+
assert_equal "ocelot.cul.columbia.edu", @d_en.full_id
|
48
|
+
assert_equal "ocelot.cul.columbia.edu", @d_en.full_id_escaped
|
49
|
+
end
|
50
|
+
|
51
|
+
should "be able to create user entities" do
|
52
|
+
assert_equal @en, UserEntity.new("test1@ocelot.cul.columbia.edu")
|
53
|
+
assert_equal @en, UserEntity.new("test1%40ocelot.cul.columbia.edu")
|
54
|
+
assert_equal @en, UserEntity.new(:id => "test1", :domain => "ocelot.cul.columbia.edu")
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
should "be able to create calendar entities" do
|
59
|
+
assert_equal @c_en, CalendarEntity.new("js235@ocelot.cul.columbia.edu")
|
60
|
+
assert_equal @c_en, CalendarEntity.new("js235%40ocelot.cul.columbia.edu")
|
61
|
+
assert_equal @c_en, CalendarEntity.new(:id => "js235", :domain => "ocelot.cul.columbia.edu")
|
62
|
+
end
|
63
|
+
|
64
|
+
should "be able to derive a calendar entity from a user entity" do
|
65
|
+
assert_equal @c_en, UserEntity.new("js235@ocelot.cul.columbia.edu").entity_for_base_calendar
|
66
|
+
end
|
67
|
+
|
68
|
+
should "be able to display the qualified id, escape and nonescaped" do
|
69
|
+
assert_equal "user:test1@ocelot.cul.columbia.edu", @en.qualified_id
|
70
|
+
assert_equal "user%3Atest1%40ocelot.cul.columbia.edu", @en.qualified_id_escaped
|
71
|
+
assert_equal "domain:ocelot.cul.columbia.edu", @d_en.qualified_id
|
72
|
+
assert_equal "domain%3Aocelot.cul.columbia.edu", @d_en.qualified_id_escaped
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|