clickmeetings 0.1.3.1 → 0.1.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +1 -1
- data/README.md +303 -7
- data/clickmeetings.gemspec +5 -3
- data/lib/clickmeetings.rb +6 -8
- data/lib/clickmeetings/client.rb +7 -8
- data/lib/clickmeetings/config.rb +2 -1
- data/lib/clickmeetings/engine.rb +0 -1
- data/lib/clickmeetings/model.rb +29 -19
- data/lib/clickmeetings/models/open/chat.rb +19 -0
- data/lib/clickmeetings/models/open/concerns/with_conference.rb +35 -0
- data/lib/clickmeetings/models/open/concerns/with_locale.rb +20 -0
- data/lib/clickmeetings/models/open/conference.rb +72 -0
- data/lib/clickmeetings/models/open/contact.rb +14 -0
- data/lib/clickmeetings/models/open/file_library.rb +43 -0
- data/lib/clickmeetings/models/open/invitation.rb +30 -0
- data/lib/clickmeetings/models/open/login_hash.rb +50 -0
- data/lib/clickmeetings/models/open/model.rb +38 -0
- data/lib/clickmeetings/models/open/phone_gateway.rb +9 -0
- data/lib/clickmeetings/models/open/recording.rb +22 -0
- data/lib/clickmeetings/models/open/registration.rb +20 -0
- data/lib/clickmeetings/models/open/session.rb +56 -0
- data/lib/clickmeetings/models/open/time_zone.rb +15 -0
- data/lib/clickmeetings/models/open/token.rb +35 -0
- data/lib/clickmeetings/models/privatelabel/account.rb +3 -1
- data/lib/clickmeetings/models/privatelabel/conference.rb +20 -11
- data/lib/clickmeetings/models/privatelabel/model.rb +5 -4
- data/lib/clickmeetings/models/privatelabel/profile.rb +2 -2
- data/lib/clickmeetings/storage.rb +10 -0
- data/lib/clickmeetings/version.rb +1 -1
- data/spec/clickmeetings_spec.rb +46 -0
- data/spec/client_spec.rb +27 -4
- data/spec/fixtures/delete_conferences_1_recordings.json +3 -0
- data/spec/fixtures/get_chats_1.zip +0 -0
- data/spec/fixtures/get_conferences.json +48 -0
- data/spec/fixtures/get_conferences_1.json +45 -0
- data/spec/fixtures/get_conferences_1_recordings.json +10 -0
- data/spec/fixtures/get_conferences_1_registrations.json +42 -0
- data/spec/fixtures/get_conferences_1_sessions.json +16 -0
- data/spec/fixtures/get_conferences_1_sessions_1.json +54 -0
- data/spec/fixtures/get_conferences_1_sessions_1_attendees.json +38 -0
- data/spec/fixtures/get_conferences_1_sessions_1_generate-pdf_en.json +4 -0
- data/spec/fixtures/get_conferences_1_sessions_1_generate-pdf_pl.json +5 -0
- data/spec/fixtures/get_conferences_1_sessions_1_generate-pdf_ru.json +4 -0
- data/spec/fixtures/get_conferences_1_sessions_1_registrations.json +42 -0
- data/spec/fixtures/get_conferences_1_tokens.json +254 -0
- data/spec/fixtures/get_conferences_2.json +45 -0
- data/spec/fixtures/get_conferences_active.json +1 -0
- data/spec/fixtures/get_conferences_inactive.json +1 -0
- data/spec/fixtures/get_conferences_skins.json +57 -0
- data/spec/fixtures/get_file-library_conferences_1.json +13 -0
- data/spec/fixtures/get_time_zone_list.json +422 -0
- data/spec/fixtures/get_time_zone_list_ru.json +26 -0
- data/spec/fixtures/post_conferences_1_invitation_email_en.json +1 -0
- data/spec/fixtures/post_conferences_1_invitation_email_ru.json +1 -0
- data/spec/fixtures/post_conferences_1_registration.json +6 -0
- data/spec/fixtures/post_conferences_1_room_autologin_hash.json +3 -0
- data/spec/fixtures/post_conferences_1_tokens.json +14 -0
- data/spec/fixtures/post_conferences_2_invitation_email_en.json +1 -0
- data/spec/fixtures/post_contacts.json +3 -0
- data/spec/fixtures/post_file-library.json +10 -0
- data/spec/fixtures/presentation.pdf +0 -0
- data/spec/helpers/fixtures_helpers.rb +1 -1
- data/spec/models/open/chat_spec.rb +25 -0
- data/spec/models/open/concerns/with_conference_spec.rb +55 -0
- data/spec/models/open/concerns/with_locale_spec.rb +23 -0
- data/spec/models/open/conference_spec.rb +132 -0
- data/spec/models/open/contact_spec.rb +17 -0
- data/spec/models/open/file_spec.rb +46 -0
- data/spec/models/open/invitation_spec.rb +43 -0
- data/spec/models/open/login_hash_spec.rb +59 -0
- data/spec/models/open/model_spec.rb +55 -0
- data/spec/models/open/recording_spec.rb +21 -0
- data/spec/models/open/registration_spec.rb +25 -0
- data/spec/models/open/session_spec.rb +73 -0
- data/spec/models/open/time_zone_spec.rb +27 -0
- data/spec/models/open/token_spec.rb +54 -0
- data/spec/models/privatelabel/conference_spec.rb +25 -7
- data/spec/shared_examples/tokens_examples.rb +6 -0
- data/spec/spec_helper.rb +7 -0
- metadata +147 -8
- data/lib/clickmeetings/models/open_api/.keep +0 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
module Clickmeetings
|
2
|
+
module Open
|
3
|
+
class Recording < Model
|
4
|
+
include WithConference
|
5
|
+
|
6
|
+
attr_accessor :recording_duration, :recording_file_size, :recording_started, :recording_url,
|
7
|
+
:recording_start_date
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def destroy_all
|
11
|
+
res = all
|
12
|
+
|
13
|
+
Clickmeetings.with_client(client_options) do
|
14
|
+
Clickmeetings.client.delete remote_url(__method__), default_params, default_headers
|
15
|
+
end
|
16
|
+
|
17
|
+
res
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Clickmeetings
|
2
|
+
module Open
|
3
|
+
class Registration < Model
|
4
|
+
include WithConference
|
5
|
+
|
6
|
+
attr_accessor :registration_date, :registration_confirmed, :fields, :session_id,
|
7
|
+
:email, :visitor_nickname, :url, :r, :http_referer, :country, :city
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def for_session(session_id: nil)
|
11
|
+
Session.by_conference(conference_id: conference_id).new(id: session_id).registrations
|
12
|
+
end
|
13
|
+
|
14
|
+
def create(params = {})
|
15
|
+
Conference.new(id: conference_id).register(params)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Clickmeetings
|
2
|
+
module Open
|
3
|
+
class Session < Model
|
4
|
+
include WithConference
|
5
|
+
include WithLocale
|
6
|
+
|
7
|
+
attr_accessor :total_visitors, :max_vistors, :start_date, :end_date, :attendees, :pdf,
|
8
|
+
:associations_api_key
|
9
|
+
delegate :locale, :with_locale, :find, to: :class
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def find(id)
|
13
|
+
obj = super
|
14
|
+
obj.id = id
|
15
|
+
obj
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def attendees
|
20
|
+
self.class.with_account account_api_key: associations_api_key do
|
21
|
+
Clickmeetings.with_client(client_options) do
|
22
|
+
Clickmeetings.client.get remote_url(__method__, id: id), default_params, default_headers
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def generate_pdf(lang = nil)
|
28
|
+
self.class.with_account account_api_key: associations_api_key do
|
29
|
+
with_locale lang do
|
30
|
+
Clickmeetings.with_client(client_options) do
|
31
|
+
Clickmeetings.client.get remote_url("generate-pdf/#{locale}", id: id),
|
32
|
+
default_params, default_headers
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_report(lang = nil)
|
39
|
+
gen_pdf_response = generate_pdf(lang)
|
40
|
+
return unless gen_pdf_response["status"] == "FINISHED"
|
41
|
+
gen_pdf_response["url"] # solve this
|
42
|
+
end
|
43
|
+
|
44
|
+
def registrations
|
45
|
+
self.class.with_account account_api_key: associations_api_key do
|
46
|
+
response = Clickmeetings.with_client(client_options) do
|
47
|
+
Clickmeetings.client.get remote_url(__method__, id: id), default_params, default_headers
|
48
|
+
end
|
49
|
+
Registration.by_conference(conference_id: conference_id) do
|
50
|
+
Registration.handle_response response
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Clickmeetings
|
2
|
+
module Open
|
3
|
+
class TimeZone < Model
|
4
|
+
set_resource_name 'time_zone_list'
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def all(country: nil)
|
8
|
+
Clickmeetings.with_client(client_options) do
|
9
|
+
Clickmeetings.client.get remote_url(country), default_params, default_headers
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Clickmeetings
|
2
|
+
module Open
|
3
|
+
class Token < Model
|
4
|
+
class NoConferenceError < ::Clickmeetings::ClickmeetingError; end
|
5
|
+
|
6
|
+
include WithConference
|
7
|
+
|
8
|
+
set_resource_name "tokens"
|
9
|
+
|
10
|
+
attr_accessor :token, :sent_to_email, :first_use_date
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def all
|
14
|
+
fail NoConferenceError if conference_id.nil?
|
15
|
+
response = Clickmeetings.with_client(client_options) do
|
16
|
+
Clickmeetings.client.get remote_url(__method__), default_params, default_headers
|
17
|
+
end
|
18
|
+
handle_response response["access_tokens"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def create(params = {})
|
22
|
+
fail NoConferenceError if conference_id.nil?
|
23
|
+
response = Clickmeetings.with_client(client_options) do
|
24
|
+
Clickmeetings.client.post remote_url(__method__), params.merge(default_params), default_headers
|
25
|
+
end
|
26
|
+
handle_response response["access_tokens"]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_hash(params = {})
|
31
|
+
LoginHash.create params.merge(conference_id: conference_id, token: token)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -20,7 +20,9 @@ module Clickmeetings
|
|
20
20
|
|
21
21
|
%w(enable disable).each do |m|
|
22
22
|
define_method m do
|
23
|
-
Clickmeetings.with_client(client_options)
|
23
|
+
Clickmeetings.with_client(client_options) do
|
24
|
+
client.put(remote_url(__method__, id: id), default_params)
|
25
|
+
end
|
24
26
|
@account_status = (m == "enable" ? "active" : "disabled")
|
25
27
|
self
|
26
28
|
end
|
@@ -5,36 +5,45 @@ module Clickmeetings
|
|
5
5
|
:access_type, :lobby_description, :status, :created_at,
|
6
6
|
:updated_at, :permanent_room, :ccc, :starts_at, :ends_at,
|
7
7
|
:access_role_hashes, :room_url, :phone_listener_pin,
|
8
|
-
:phone_presenter_pin, :embed_room_url, :recorder_list, :account_id
|
8
|
+
:phone_presenter_pin, :embed_room_url, :recorder_list, :account_id,
|
9
|
+
:password
|
9
10
|
|
10
11
|
class NoAccountError < ::Clickmeetings::ClickmeetingError; end
|
11
12
|
|
12
13
|
class << self
|
13
|
-
attr_reader :account_id
|
14
|
-
|
15
14
|
def by_account(account_id: nil)
|
16
|
-
|
17
|
-
|
15
|
+
Storage.cm_private_current_account = account_id
|
16
|
+
if block_given?
|
17
|
+
result = yield
|
18
|
+
Storage.cm_private_current_account = nil
|
19
|
+
result
|
20
|
+
else
|
21
|
+
self
|
22
|
+
end
|
18
23
|
end
|
19
24
|
|
20
25
|
def find(id)
|
21
|
-
fail Clickmeetings::PrivateLabel::Conference::NoAccountError if
|
26
|
+
fail Clickmeetings::PrivateLabel::Conference::NoAccountError if account_id.nil?
|
22
27
|
super
|
23
28
|
end
|
24
29
|
|
25
30
|
def all
|
26
|
-
fail Clickmeetings::PrivateLabel::Conference::NoAccountError if
|
31
|
+
fail Clickmeetings::PrivateLabel::Conference::NoAccountError if account_id.nil?
|
27
32
|
response = Clickmeetings.with_client(client_options) do
|
28
|
-
Clickmeetings.client.get remote_url(__method__)
|
33
|
+
Clickmeetings.client.get remote_url(__method__), default_params
|
29
34
|
end
|
30
|
-
response = response["active_conferences"] + response["inactive_conferences"]
|
35
|
+
response = response["active_conferences"].to_a + response["inactive_conferences"].to_a
|
31
36
|
handle_response response
|
32
37
|
end
|
33
38
|
|
34
39
|
def create(params = {})
|
35
|
-
fail Clickmeetings::PrivateLabel::Conference::NoAccountError if
|
40
|
+
fail Clickmeetings::PrivateLabel::Conference::NoAccountError if account_id.nil?
|
36
41
|
super
|
37
42
|
end
|
43
|
+
|
44
|
+
def account_id
|
45
|
+
Storage.cm_private_current_account
|
46
|
+
end
|
38
47
|
end
|
39
48
|
|
40
49
|
def initialize(params = {})
|
@@ -43,7 +52,7 @@ module Clickmeetings
|
|
43
52
|
end
|
44
53
|
|
45
54
|
def remote_url(action = nil, params = {})
|
46
|
-
|
55
|
+
"#{Account.remote_path(:find, id: @account_id)}/#{remote_path(action, params)}"
|
47
56
|
end
|
48
57
|
|
49
58
|
def update(params = {})
|
@@ -3,12 +3,13 @@ module Clickmeetings
|
|
3
3
|
class Model < ::Clickmeetings::Model
|
4
4
|
class << self
|
5
5
|
def client_options
|
6
|
-
{
|
7
|
-
url: Clickmeetings.config.privatelabel_host,
|
8
|
-
api_key: Clickmeetings.config.privatelabel_api_key
|
9
|
-
}
|
6
|
+
{ url: Clickmeetings.config.privatelabel_host }
|
10
7
|
end
|
11
8
|
end
|
9
|
+
|
10
|
+
def default_params
|
11
|
+
{ api_key: Clickmeetings.config.privatelabel_api_key }
|
12
|
+
end
|
12
13
|
end
|
13
14
|
end
|
14
15
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Clickmeetings
|
2
2
|
module PrivateLabel
|
3
|
-
class Profile < Model
|
3
|
+
class Profile < ::Clickmeetings::PrivateLabel::Model
|
4
4
|
attr_accessor :id, :account_manager_email, :email, :phone,
|
5
5
|
:account_manager_name, :account_manager_phone,
|
6
6
|
:name, :packages
|
@@ -11,7 +11,7 @@ module Clickmeetings
|
|
11
11
|
|
12
12
|
def get
|
13
13
|
response = Clickmeetings.with_client(client_options) do
|
14
|
-
Clickmeetings.client.get
|
14
|
+
Clickmeetings.client.get 'client', default_params, default_headers
|
15
15
|
end
|
16
16
|
handle_response response
|
17
17
|
end
|
data/spec/clickmeetings_spec.rb
CHANGED
@@ -4,4 +4,50 @@ describe Clickmeetings do
|
|
4
4
|
it 'has a version number' do
|
5
5
|
expect(Clickmeetings::VERSION).not_to be nil
|
6
6
|
end
|
7
|
+
|
8
|
+
describe '::configure' do
|
9
|
+
before do
|
10
|
+
described_class.configure do |config|
|
11
|
+
config.host = "http://teachbase.ru"
|
12
|
+
config.privatelabel_host = "http://go.teachbase.ru"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
subject { described_class.config }
|
17
|
+
|
18
|
+
it "sets config", :aggregate_failures do
|
19
|
+
expect(subject.host).to eq "http://teachbase.ru"
|
20
|
+
expect(subject.privatelabel_host).to eq "http://go.teachbase.ru"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '::reset' do
|
25
|
+
subject { described_class.reset }
|
26
|
+
|
27
|
+
context "config" do
|
28
|
+
before(:each) do
|
29
|
+
described_class.configure do |config|
|
30
|
+
config.host = "http://teachbase.ru"
|
31
|
+
config.privatelabel_host = "http://go.teachbase.ru"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
it "resets config" do
|
36
|
+
expect { subject }.to change { described_class.config.host }
|
37
|
+
.from("http://teachbase.ru").to("https://api.clickmeeting.com/v1")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context "client" do
|
42
|
+
before(:each) do
|
43
|
+
described_class::ClientRegistry.client =
|
44
|
+
described_class::Client.new(url: "http://teachbase.ru")
|
45
|
+
end
|
46
|
+
|
47
|
+
it "resets client" do
|
48
|
+
expect { subject }.to change { described_class.client.url }
|
49
|
+
.from("http://teachbase.ru").to("https://api.clickmeeting.com/v1")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
7
53
|
end
|
data/spec/client_spec.rb
CHANGED
@@ -2,12 +2,10 @@ require "spec_helper"
|
|
2
2
|
|
3
3
|
describe Clickmeetings::Client do
|
4
4
|
let(:client) do
|
5
|
-
described_class.new(url: Clickmeetings.config.privatelabel_host
|
6
|
-
api_key: Clickmeetings.config.privatelabel_api_key)
|
5
|
+
described_class.new(url: Clickmeetings.config.privatelabel_host)
|
7
6
|
end
|
8
7
|
|
9
|
-
it "should create client"
|
10
|
-
expect(subject.api_key).to eq Clickmeetings.config.api_key
|
8
|
+
it "should create client" do
|
11
9
|
expect(subject.url).to eq Clickmeetings.config.host
|
12
10
|
end
|
13
11
|
|
@@ -20,6 +18,25 @@ describe Clickmeetings::Client do
|
|
20
18
|
end
|
21
19
|
end
|
22
20
|
|
21
|
+
context "with header authorization" do
|
22
|
+
before do
|
23
|
+
stub_request(:get, "#{Clickmeetings.config.host}/ping")
|
24
|
+
.with(headers: {
|
25
|
+
'Accept'=>'*/*',
|
26
|
+
'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
|
27
|
+
'Content-Type'=>'application/x-www-form-urlencoded',
|
28
|
+
'User-Agent'=>'Faraday v0.9.2',
|
29
|
+
'X-Api-Key'=>'qwer'
|
30
|
+
})
|
31
|
+
.to_return(status: 200, body: "{\"ping\":\"pong\"}")
|
32
|
+
end
|
33
|
+
|
34
|
+
it "responds with pong" do
|
35
|
+
res = subject.get "ping", {}, {"X-Api-Key" => "qwer"}
|
36
|
+
expect(res).to eq({"ping" => "pong"})
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
23
40
|
{
|
24
41
|
400 => Clickmeetings::BadRequestError,
|
25
42
|
401 => Clickmeetings::Unauthorized,
|
@@ -34,4 +51,10 @@ describe Clickmeetings::Client do
|
|
34
51
|
specify { expect { subject }.to raise_error error_class }
|
35
52
|
end
|
36
53
|
end
|
54
|
+
|
55
|
+
context "#request when client doesn't respond method" do
|
56
|
+
subject { client.request :set, "client" }
|
57
|
+
|
58
|
+
specify { expect { subject }.to raise_error(Clickmeetings::UndefinedHTTPMethod) }
|
59
|
+
end
|
37
60
|
end
|
Binary file
|
@@ -0,0 +1,48 @@
|
|
1
|
+
{
|
2
|
+
"active_conferences":[
|
3
|
+
{
|
4
|
+
"id":880484,
|
5
|
+
"room_type":"webinar",
|
6
|
+
"room_pin":138489866,
|
7
|
+
"name":"name",
|
8
|
+
"name_url":"name",
|
9
|
+
"starts_at":"2016-11-18T00:00:00+03:00",
|
10
|
+
"ends_at":"2016-11-18T03:00:00+03:00",
|
11
|
+
"access_type":3,
|
12
|
+
"lobby_enabled":true,
|
13
|
+
"lobby_description":"",
|
14
|
+
"registration_enabled":0,
|
15
|
+
"status":"active",
|
16
|
+
"timezone":"Europe\/Moscow",
|
17
|
+
"timezone_offset":10800,
|
18
|
+
"created_at":"2016-11-16T17:43:47+03:00",
|
19
|
+
"updated_at":"2016-11-16T21:00:24+03:00",
|
20
|
+
"permanent_room":false,
|
21
|
+
"ccc":"2016-11-17 21:00:00",
|
22
|
+
"access_role_hashes":{
|
23
|
+
"listener":"9eaf5c20ceaabe7aee532e00582ccce2",
|
24
|
+
"presenter":"fc81cc9cec28543400713900582ccce2",
|
25
|
+
"host":"5212ebf5d5f631e5e04df000582ccce2"
|
26
|
+
},
|
27
|
+
"room_url":"http:\/\/qwerewrq.anysecond.com\/name",
|
28
|
+
"phone_presenter_pin":411582,
|
29
|
+
"phone_listener_pin":823866,
|
30
|
+
"embed_room_url":"http:\/\/embed.anysecond.com\/embed_conference.html?r=16536349880484",
|
31
|
+
"widgets_hash":"cic52b",
|
32
|
+
"recorder_list":[],
|
33
|
+
"settings":{
|
34
|
+
"show_on_personal_page":true,
|
35
|
+
"thank_you_emails_enabled":true,
|
36
|
+
"connection_tester_enabled":false,
|
37
|
+
"recorder_autostart_enabled":false,
|
38
|
+
"room_invite_button_enabled":true,
|
39
|
+
"social_media_sharing_enabled":false,
|
40
|
+
"connection_status_enabled":true
|
41
|
+
},
|
42
|
+
"autologin_hashes":{
|
43
|
+
"host":"BQtjAQt0DUjgsROxnaWyDTEdpzHhMTcNsP18DTEdpzIlnzIxDUjgsRONsP18DQHlZGWlo3Z1pGImAwZkpwIlZQEkpmNjZQH4ZaOjpUVlDUjgsRN__"
|
44
|
+
},
|
45
|
+
"autologin_hash":"BQtjAQt0DUjgsROxnaWyDTEdpzHhMTcNsP18DTEdpzIlnzIxDUjgsRONsP18DQHlZGWlo3Z1pGImAwZkpwIlZQEkpmNjZQH4ZaOjpUVlDUjgsRN__"
|
46
|
+
}
|
47
|
+
]
|
48
|
+
}
|