openstax_accounts 8.0.1 → 9.0.2
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 +4 -4
- data/app/controllers/openstax/accounts/application_controller.rb +0 -4
- data/app/controllers/openstax/accounts/dev/accounts_controller.rb +16 -10
- data/app/controllers/openstax/accounts/dev/base_controller.rb +1 -5
- data/app/controllers/openstax/accounts/sessions_controller.rb +2 -5
- data/app/handlers/openstax/accounts/accounts_search.rb +3 -5
- data/app/handlers/openstax/accounts/dev/accounts_create.rb +1 -7
- data/app/handlers/openstax/accounts/dev/accounts_search.rb +0 -2
- data/app/handlers/openstax/accounts/sessions_callback.rb +2 -4
- data/app/models/openstax/accounts/account.rb +1 -19
- data/app/models/openstax/accounts/anonymous_account.rb +1 -2
- data/app/representers/openstax/accounts/api/v1/unclaimed_account_representer.rb +1 -7
- data/app/routines/openstax/accounts/dev/create_account.rb +23 -21
- data/app/routines/openstax/accounts/find_or_create_account.rb +16 -25
- data/app/routines/openstax/accounts/find_or_create_from_sso.rb +2 -8
- data/app/routines/openstax/accounts/sync_accounts.rb +1 -6
- data/app/views/layouts/openstax/accounts/application.html.erb +2 -2
- data/app/views/openstax/accounts/dev/accounts/_search_results.html.erb +9 -10
- data/app/views/openstax/accounts/dev/accounts/index.html.erb +7 -10
- data/app/views/openstax/accounts/dev/accounts/{search.js.erb → index.js.erb} +0 -0
- data/app/views/openstax/accounts/shared/_attention.html.erb +9 -1
- data/app/views/openstax/accounts/shared/accounts/_search.html.erb +12 -8
- data/config/routes.rb +2 -5
- data/db/migrate/14_drop_openstax_uid_and_username_uniqueness.rb +9 -0
- data/db/migrate/15_drop_accounts_groups.rb +42 -0
- data/lib/omniauth/strategies/openstax.rb +1 -1
- data/lib/openstax/accounts/api.rb +0 -149
- data/lib/openstax/accounts/current_user_manager.rb +1 -5
- data/lib/openstax/accounts/engine.rb +1 -1
- data/lib/openstax/accounts/sso.rb +4 -3
- data/lib/openstax/accounts/version.rb +1 -1
- data/lib/tasks/sync.rake +0 -8
- metadata +10 -26
- data/app/models/openstax/accounts/application_group.rb +0 -7
- data/app/models/openstax/accounts/group.rb +0 -169
- data/app/models/openstax/accounts/group_member.rb +0 -37
- data/app/models/openstax/accounts/group_nesting.rb +0 -55
- data/app/models/openstax/accounts/group_owner.rb +0 -37
- data/app/representers/openstax/accounts/api/v1/application_group_representer.rb +0 -48
- data/app/representers/openstax/accounts/api/v1/application_groups_representer.rb +0 -20
- data/app/representers/openstax/accounts/api/v1/group_nesting_representer.rb +0 -31
- data/app/representers/openstax/accounts/api/v1/group_representer.rb +0 -71
- data/app/representers/openstax/accounts/api/v1/group_user_representer.rb +0 -34
- data/app/routines/openstax/accounts/create_group.rb +0 -26
- data/app/routines/openstax/accounts/sync_groups.rb +0 -67
- data/app/routines/openstax/accounts/update_group_caches.rb +0 -27
- data/lib/openstax/accounts/has_many_through_groups/active_record/base.rb +0 -51
- data/spec/factories/openstax_accounts_group.rb +0 -7
- data/spec/factories/openstax_accounts_group_member.rb +0 -6
- data/spec/factories/openstax_accounts_group_nesting.rb +0 -6
- data/spec/factories/openstax_accounts_group_owner.rb +0 -6
@@ -1,23 +1,17 @@
|
|
1
1
|
module OpenStax
|
2
2
|
module Accounts
|
3
3
|
class FindOrCreateFromSso
|
4
|
-
|
5
4
|
lev_routine express_output: :account
|
6
5
|
|
7
6
|
def exec(attrs)
|
8
7
|
attrs.stringify_keys!
|
9
8
|
uid = attrs.delete('id')
|
10
9
|
uuid = attrs.delete('uuid')
|
11
|
-
account = Account.find_or_initialize_by(
|
12
|
-
|
13
|
-
)
|
14
|
-
account.update_attributes!(
|
15
|
-
attrs.slice(*Account.column_names)
|
16
|
-
)
|
10
|
+
account = Account.find_or_initialize_by(uuid: uuid)
|
11
|
+
account.update_attributes!(attrs.slice(*Account.column_names))
|
17
12
|
transfer_errors_from(account, {type: :verbatim})
|
18
13
|
outputs.account = account
|
19
14
|
end
|
20
|
-
|
21
15
|
end
|
22
16
|
end
|
23
17
|
end
|
@@ -4,9 +4,7 @@
|
|
4
4
|
|
5
5
|
module OpenStax
|
6
6
|
module Accounts
|
7
|
-
|
8
7
|
class SyncAccounts
|
9
|
-
|
10
8
|
lev_routine transaction: :no_transaction
|
11
9
|
|
12
10
|
protected
|
@@ -27,7 +25,7 @@ module OpenStax
|
|
27
25
|
updated_app_accounts = []
|
28
26
|
app_accounts.each do |app_account|
|
29
27
|
account = OpenStax::Accounts::Account.find_by(
|
30
|
-
|
28
|
+
uuid: app_account.account.uuid
|
31
29
|
) || app_account.account
|
32
30
|
account.syncing = true
|
33
31
|
|
@@ -45,10 +43,7 @@ module OpenStax
|
|
45
43
|
end
|
46
44
|
|
47
45
|
OpenStax::Accounts::Api.mark_account_updates_as_read(updated_app_accounts)
|
48
|
-
|
49
46
|
end
|
50
|
-
|
51
47
|
end
|
52
|
-
|
53
48
|
end
|
54
49
|
end
|
@@ -2,8 +2,8 @@
|
|
2
2
|
<html>
|
3
3
|
<head>
|
4
4
|
<title>Accounts</title>
|
5
|
-
<%= stylesheet_link_tag
|
6
|
-
<%= javascript_include_tag
|
5
|
+
<%= stylesheet_link_tag 'openstax/accounts/application', media: 'all' %>
|
6
|
+
<%= javascript_include_tag 'openstax/accounts/application' %>
|
7
7
|
<%= csrf_meta_tags %>
|
8
8
|
</head>
|
9
9
|
<body>
|
@@ -1,5 +1,5 @@
|
|
1
|
-
<% total_count = @handler_result.outputs
|
2
|
-
accounts = @handler_result.outputs[
|
1
|
+
<% total_count = @handler_result.outputs.total_count
|
2
|
+
accounts = @handler_result.outputs.items || [] %>
|
3
3
|
|
4
4
|
<div id='search-results-count'>
|
5
5
|
<%= pluralize(total_count, 'user') %> found.
|
@@ -9,16 +9,15 @@
|
|
9
9
|
<%= osu.action_list(
|
10
10
|
records: accounts,
|
11
11
|
list: {
|
12
|
-
headings: ['UID', 'Username (click to sign in as)', 'Name'],
|
13
|
-
widths: ['20%', '
|
12
|
+
headings: ['UID', 'UUID', 'Username (click to sign in as)', 'Name'],
|
13
|
+
widths: ['20%', '20%', '30%', '30%'],
|
14
14
|
data_procs: [
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
), method: :post
|
15
|
+
->(account) { account.openstax_uid },
|
16
|
+
->(account) { account.uuid },
|
17
|
+
->(account) {
|
18
|
+
link_to account.username, become_dev_account_path(account.id), method: :post
|
20
19
|
},
|
21
|
-
|
20
|
+
->(account) { account.name || '---' }
|
22
21
|
]
|
23
22
|
}
|
24
23
|
) %>
|
@@ -1,5 +1,4 @@
|
|
1
1
|
<div class="openstax-accounts development-login">
|
2
|
-
|
3
2
|
<%= render partial: 'openstax/accounts/shared/attention' %>
|
4
3
|
|
5
4
|
<h3>Create an Account</h3>
|
@@ -11,7 +10,7 @@
|
|
11
10
|
|
12
11
|
|
13
12
|
<div class="form-group">
|
14
|
-
<%= f.text_field :username, placeholder:
|
13
|
+
<%= f.text_field :username, placeholder: 'Username' %>
|
15
14
|
<%= f.select :role, OpenStax::Accounts::Account.roles.keys.map{|rr| [rr, rr]} %>
|
16
15
|
<%= f.submit 'Create', class: 'btn btn-primary' %>
|
17
16
|
</div>
|
@@ -23,15 +22,13 @@
|
|
23
22
|
<p>You need to login, but we're not connected to the Accounts server.<br>
|
24
23
|
Search for a user below and click the sign in link next to him or her.</p>
|
25
24
|
|
26
|
-
<%= render :
|
27
|
-
:
|
28
|
-
:
|
29
|
-
:
|
30
|
-
:
|
25
|
+
<%= render partial: 'openstax/accounts/shared/accounts/search',
|
26
|
+
locals: {
|
27
|
+
search_action_path: openstax_accounts.dev_accounts_path,
|
28
|
+
search_results_partial: 'openstax/accounts/dev/accounts/search_results',
|
29
|
+
remote: true,
|
30
|
+
method: :get
|
31
31
|
} %>
|
32
32
|
|
33
33
|
<br>
|
34
|
-
|
35
|
-
<div id="search-results"></div>
|
36
|
-
|
37
34
|
</div>
|
File without changes
|
@@ -1,3 +1,11 @@
|
|
1
1
|
<% handler_errors.each do |error| %>
|
2
|
-
|
2
|
+
<div class="error"><%= error.translate %></div>
|
3
|
+
<% end %>
|
4
|
+
|
5
|
+
<% if flash.alert %>
|
6
|
+
<div class="alert"><%= flash.alert %></div>
|
7
|
+
<% end %>
|
8
|
+
|
9
|
+
<% if flash.notice %>
|
10
|
+
<div class="notice"><%= flash.notice %></div>
|
3
11
|
<% end %>
|
@@ -1,12 +1,11 @@
|
|
1
1
|
<%
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
'First Name', 'Last Name', 'Email']
|
2
|
+
# Clients of this partial can override the following variables:
|
3
|
+
search_action_path ||= nil
|
4
|
+
search_results_partial ||= nil
|
5
|
+
method ||= :get
|
6
|
+
remote ||= false
|
7
|
+
form_html ||= {id: 'search-form', class: 'form-inline'}
|
8
|
+
search_types ||= ['Any', 'Username', 'Name', 'First Name', 'Last Name', 'Email']
|
10
9
|
%>
|
11
10
|
|
12
11
|
<%= lev_form_for :search,
|
@@ -26,6 +25,11 @@
|
|
26
25
|
|
27
26
|
<% end %>
|
28
27
|
|
28
|
+
<div id="search-results">
|
29
|
+
<% unless search_results_partial.blank? %>
|
30
|
+
<%= render partial: search_results_partial %>
|
31
|
+
<% end %>
|
32
|
+
</div>
|
29
33
|
|
30
34
|
<script>
|
31
35
|
var input = $('input[name="search[query]"]');
|
data/config/routes.rb
CHANGED
@@ -1,18 +1,16 @@
|
|
1
1
|
OpenStax::Accounts::Engine.routes.draw do
|
2
|
-
|
3
2
|
# Redirect here if we don't know what to do (theoretically should not happen)
|
4
3
|
root to: 'sessions#new'
|
5
4
|
|
6
5
|
# Shortcut to OmniAuth route that redirects to the Accounts server
|
7
6
|
# This is provided by OmniAuth and is not in the SessionsController
|
8
|
-
get '/auth/openstax', as: 'openstax_login'
|
7
|
+
get '/auth/openstax', to: ->(env) { [ 404, {}, [ '' ] ] }, as: 'openstax_login'
|
9
8
|
|
10
|
-
# User profile route
|
11
9
|
if OpenStax::Accounts.configuration.enable_stubbing?
|
10
|
+
# User profile route
|
12
11
|
namespace :dev do
|
13
12
|
resources :accounts, only: [:index, :create] do
|
14
13
|
post 'become', on: :member
|
15
|
-
get 'search', on: :collection
|
16
14
|
end
|
17
15
|
end
|
18
16
|
end
|
@@ -27,5 +25,4 @@ OpenStax::Accounts::Engine.routes.draw do
|
|
27
25
|
via: OpenStax::Accounts.configuration.logout_via
|
28
26
|
get 'profile', action: :profile # Redirects to profile path or stub
|
29
27
|
end
|
30
|
-
|
31
28
|
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
class DropOpenStaxUidAndUsernameUniqueness < ActiveRecord::Migration[5.2]
|
2
|
+
def change
|
3
|
+
remove_index :openstax_accounts_accounts, column: [ :openstax_uid ], unique: true
|
4
|
+
remove_index :openstax_accounts_accounts, column: [ :username ], unique: true
|
5
|
+
|
6
|
+
add_index :openstax_accounts_accounts, :openstax_uid
|
7
|
+
add_index :openstax_accounts_accounts, :username
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class DropAccountsGroups < ActiveRecord::Migration[5.2]
|
2
|
+
def change
|
3
|
+
drop_table "openstax_accounts_group_members" do |t|
|
4
|
+
t.integer "group_id", null: false
|
5
|
+
t.integer "user_id", null: false
|
6
|
+
t.datetime "created_at", null: false
|
7
|
+
t.datetime "updated_at", null: false
|
8
|
+
t.index ["group_id", "user_id"], name: "index_openstax_accounts_group_members_on_group_id_and_user_id", unique: true
|
9
|
+
t.index ["user_id"], name: "index_openstax_accounts_group_members_on_user_id"
|
10
|
+
end
|
11
|
+
|
12
|
+
drop_table "openstax_accounts_group_nestings" do |t|
|
13
|
+
t.integer "member_group_id", null: false
|
14
|
+
t.integer "container_group_id", null: false
|
15
|
+
t.datetime "created_at", null: false
|
16
|
+
t.datetime "updated_at", null: false
|
17
|
+
t.index ["container_group_id"], name: "index_openstax_accounts_group_nestings_on_container_group_id"
|
18
|
+
t.index ["member_group_id"], name: "index_openstax_accounts_group_nestings_on_member_group_id", unique: true
|
19
|
+
end
|
20
|
+
|
21
|
+
drop_table "openstax_accounts_group_owners" do |t|
|
22
|
+
t.integer "group_id", null: false
|
23
|
+
t.integer "user_id", null: false
|
24
|
+
t.datetime "created_at", null: false
|
25
|
+
t.datetime "updated_at", null: false
|
26
|
+
t.index ["group_id", "user_id"], name: "index_openstax_accounts_group_owners_on_group_id_and_user_id", unique: true
|
27
|
+
t.index ["user_id"], name: "index_openstax_accounts_group_owners_on_user_id"
|
28
|
+
end
|
29
|
+
|
30
|
+
drop_table "openstax_accounts_groups" do |t|
|
31
|
+
t.integer "openstax_uid", null: false
|
32
|
+
t.boolean "is_public", default: false, null: false
|
33
|
+
t.string "name"
|
34
|
+
t.text "cached_subtree_group_ids"
|
35
|
+
t.text "cached_supertree_group_ids"
|
36
|
+
t.datetime "created_at", null: false
|
37
|
+
t.datetime "updated_at", null: false
|
38
|
+
t.index ["is_public"], name: "index_openstax_accounts_groups_on_is_public"
|
39
|
+
t.index ["openstax_uid"], name: "index_openstax_accounts_groups_on_openstax_uid", unique: true
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -97,155 +97,6 @@ module OpenStax
|
|
97
97
|
request(:put, 'application_users/updated', options.merge(body: application_users.to_json))
|
98
98
|
end
|
99
99
|
|
100
|
-
# Retrieves information about groups that have been
|
101
|
-
# recently updated.
|
102
|
-
# Results are limited to groups that users of the current app
|
103
|
-
# have access to.
|
104
|
-
# Takes an options hash.
|
105
|
-
# On failure, throws an Exception, just like the request method.
|
106
|
-
# On success, returns an OAuth2::Response object.
|
107
|
-
def self.get_application_group_updates(options = {})
|
108
|
-
request(:get, 'application_groups/updates', options)
|
109
|
-
end
|
110
|
-
|
111
|
-
# Marks group updates as "read".
|
112
|
-
# The application_groups parameter is an array of hashes.
|
113
|
-
# Each hash has 2 required fields: 'id', which should contain the
|
114
|
-
# application_group's id, and 'read_updates', which should contain
|
115
|
-
# the last received value of unread_updates for that application_group.
|
116
|
-
# Can only be called for application_groups that belong to the current app.
|
117
|
-
# Also takes an options hash.
|
118
|
-
# On failure, throws an Exception, just like the request method.
|
119
|
-
# On success, returns an OAuth2::Response object.
|
120
|
-
def self.mark_group_updates_as_read(application_groups, options = {})
|
121
|
-
request(:put, 'application_groups/updated', options.merge(
|
122
|
-
body: application_groups.to_json
|
123
|
-
))
|
124
|
-
end
|
125
|
-
|
126
|
-
# Creates a group in the Accounts server.
|
127
|
-
# The given account will be the owner of the group.
|
128
|
-
# Also takes an options hash.
|
129
|
-
# On failure, throws an Exception, just like the request method.
|
130
|
-
# On success, returns a hash containing the group attributes
|
131
|
-
def self.create_group(account, group, options = {})
|
132
|
-
response = ActiveSupport::JSON.decode(
|
133
|
-
request_for_account(account, :post, 'groups', options.merge(
|
134
|
-
body: group.attributes.slice('name', 'is_public').to_json
|
135
|
-
)).body
|
136
|
-
)
|
137
|
-
group.openstax_uid = response['id']
|
138
|
-
response
|
139
|
-
end
|
140
|
-
|
141
|
-
# Updates a group in the Accounts server.
|
142
|
-
# The given account must own the group.
|
143
|
-
# Also takes an options hash.
|
144
|
-
# On failure, throws an Exception, just like the request method.
|
145
|
-
# On success, returns an OAuth2::Response object.
|
146
|
-
def self.update_group(account, group, options = {})
|
147
|
-
request_for_account(account, :put, "groups/#{group.openstax_uid}",
|
148
|
-
options.merge(
|
149
|
-
body: group.attributes.slice('name', 'is_public').to_json
|
150
|
-
)
|
151
|
-
)
|
152
|
-
end
|
153
|
-
|
154
|
-
# Deletes a group from the Accounts server.
|
155
|
-
# The given account must own the group.
|
156
|
-
# Also takes an options hash.
|
157
|
-
# On failure, throws an Exception, just like the request method.
|
158
|
-
# On success, returns an OAuth2::Response object.
|
159
|
-
def self.destroy_group(account, group, options = {})
|
160
|
-
request_for_account(account, :delete, "groups/#{group.openstax_uid}", options)
|
161
|
-
end
|
162
|
-
|
163
|
-
# Creates a group_member in the Accounts server.
|
164
|
-
# The given account must own the group.
|
165
|
-
# Also takes an options hash.
|
166
|
-
# On failure, throws an Exception, just like the request method.
|
167
|
-
# On success, returns an OAuth2::Response object.
|
168
|
-
def self.create_group_member(account, group_member, options = {})
|
169
|
-
request_for_account(
|
170
|
-
account,
|
171
|
-
:post,
|
172
|
-
"groups/#{group_member.group_id}/members/#{group_member.user_id}",
|
173
|
-
options
|
174
|
-
)
|
175
|
-
end
|
176
|
-
|
177
|
-
# Deletes a group_member from the Accounts server.
|
178
|
-
# The given account must own the group.
|
179
|
-
# Also takes an options hash.
|
180
|
-
# On failure, throws an Exception, just like the request method.
|
181
|
-
# On success, returns an OAuth2::Response object.
|
182
|
-
def self.destroy_group_member(account, group_member, options = {})
|
183
|
-
request_for_account(
|
184
|
-
account,
|
185
|
-
:delete,
|
186
|
-
"groups/#{group_member.group_id}/members/#{group_member.user_id}",
|
187
|
-
options
|
188
|
-
)
|
189
|
-
end
|
190
|
-
|
191
|
-
# Creates a group_owner in the Accounts server.
|
192
|
-
# The given account must own the group.
|
193
|
-
# Also takes an options hash.
|
194
|
-
# On failure, throws an Exception, just like the request method.
|
195
|
-
# On success, returns an OAuth2::Response object.
|
196
|
-
def self.create_group_owner(account, group_owner, options = {})
|
197
|
-
request_for_account(
|
198
|
-
account,
|
199
|
-
:post,
|
200
|
-
"groups/#{group_owner.group_id}/owners/#{group_owner.user_id}",
|
201
|
-
options
|
202
|
-
)
|
203
|
-
end
|
204
|
-
|
205
|
-
# Deletes a group_owner from the Accounts server.
|
206
|
-
# The given account must own the group.
|
207
|
-
# Also takes an options hash.
|
208
|
-
# On failure, throws an Exception, just like the request method.
|
209
|
-
# On success, returns an OAuth2::Response object.
|
210
|
-
def self.destroy_group_owner(account, group_owner, options = {})
|
211
|
-
request_for_account(
|
212
|
-
account,
|
213
|
-
:delete,
|
214
|
-
"groups/#{group_owner.group_id}/owners/#{group_owner.user_id}",
|
215
|
-
options
|
216
|
-
)
|
217
|
-
end
|
218
|
-
|
219
|
-
# Creates a group_nesting in the Accounts server.
|
220
|
-
# The given account must own both groups.
|
221
|
-
# Also takes an an options hash.
|
222
|
-
# On failure, throws an Exception, just like the request method.
|
223
|
-
# On success, returns an OAuth2::Response object.
|
224
|
-
def self.create_group_nesting(account, group_nesting, options = {})
|
225
|
-
request_for_account(
|
226
|
-
account,
|
227
|
-
:post,
|
228
|
-
"groups/#{group_nesting.container_group_id}/nestings/#{
|
229
|
-
group_nesting.member_group_id}",
|
230
|
-
options
|
231
|
-
)
|
232
|
-
end
|
233
|
-
|
234
|
-
# Deletes a group_nesting from the Accounts server.
|
235
|
-
# The given account must own either group.
|
236
|
-
# Also takes an options hash.
|
237
|
-
# On failure, throws an Exception, just like the request method.
|
238
|
-
# On success, returns an OAuth2::Response object.
|
239
|
-
def self.destroy_group_nesting(account, group_nesting, options = {})
|
240
|
-
request_for_account(
|
241
|
-
account,
|
242
|
-
:delete,
|
243
|
-
"groups/#{group_nesting.container_group_id}/nestings/#{
|
244
|
-
group_nesting.member_group_id}",
|
245
|
-
options
|
246
|
-
)
|
247
|
-
end
|
248
|
-
|
249
100
|
# Finds an account matching the provided attributes or creates a new
|
250
101
|
# account. Also takes an options hash.
|
251
102
|
# On failure, throws an Exception, just like the request method.
|
@@ -1,7 +1,6 @@
|
|
1
1
|
module OpenStax
|
2
2
|
module Accounts
|
3
3
|
class CurrentUserManager
|
4
|
-
|
5
4
|
def initialize(request, session, cookies)
|
6
5
|
@request = request
|
7
6
|
@session = session
|
@@ -22,9 +21,7 @@ module OpenStax
|
|
22
21
|
|
23
22
|
# Signs in the given user or account
|
24
23
|
def sign_in!(user)
|
25
|
-
user.is_a?(Account) ?
|
26
|
-
self.current_account = user :
|
27
|
-
self.current_user = user
|
24
|
+
user.is_a?(Account) ? self.current_account = user : self.current_user = user
|
28
25
|
end
|
29
26
|
|
30
27
|
alias_method :sign_in, :sign_in!
|
@@ -126,7 +123,6 @@ module OpenStax
|
|
126
123
|
def account_user_mapper
|
127
124
|
OpenStax::Accounts.configuration.account_user_mapper
|
128
125
|
end
|
129
|
-
|
130
126
|
end
|
131
127
|
end
|
132
128
|
end
|