qrapi-wrapper 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/qrapi.rb +79 -0
- data/lib/qrapi/client.rb +58 -0
- data/lib/qrapi/error_handling_resourceable.rb +22 -0
- data/lib/qrapi/mappings/contact_mapping.rb +22 -0
- data/lib/qrapi/mappings/distribution_mapping.rb +68 -0
- data/lib/qrapi/mappings/division_mapping.rb +27 -0
- data/lib/qrapi/mappings/error_mapping.rb +19 -0
- data/lib/qrapi/mappings/group_mapping.rb +27 -0
- data/lib/qrapi/mappings/library_message_mapping.rb +20 -0
- data/lib/qrapi/mappings/mailing_list_mapping.rb +20 -0
- data/lib/qrapi/mappings/organization_mapping.rb +19 -0
- data/lib/qrapi/mappings/response_export_mapping.rb +14 -0
- data/lib/qrapi/mappings/responses_mapping.rb +45 -0
- data/lib/qrapi/mappings/survey_mapping.rb +33 -0
- data/lib/qrapi/mappings/user_mapping.rb +40 -0
- data/lib/qrapi/models/base_model.rb +22 -0
- data/lib/qrapi/models/distribution.rb +45 -0
- data/lib/qrapi/models/distribution/headers.rb +27 -0
- data/lib/qrapi/models/distribution/message.rb +26 -0
- data/lib/qrapi/models/distribution/recipients.rb +26 -0
- data/lib/qrapi/models/distribution/survey_link.rb +26 -0
- data/lib/qrapi/models/division.rb +32 -0
- data/lib/qrapi/models/group.rb +31 -0
- data/lib/qrapi/models/library_message.rb +27 -0
- data/lib/qrapi/models/mailing_list.rb +27 -0
- data/lib/qrapi/models/mailing_list/contact.rb +33 -0
- data/lib/qrapi/models/organization.rb +29 -0
- data/lib/qrapi/models/response.rb +40 -0
- data/lib/qrapi/models/response_export.rb +71 -0
- data/lib/qrapi/models/survey.rb +45 -0
- data/lib/qrapi/models/survey/expiration.rb +25 -0
- data/lib/qrapi/models/user.rb +44 -0
- data/lib/qrapi/resources/distribution_resource.rb +47 -0
- data/lib/qrapi/resources/division_resource.rb +24 -0
- data/lib/qrapi/resources/group_resource.rb +44 -0
- data/lib/qrapi/resources/library_message_resource.rb +41 -0
- data/lib/qrapi/resources/mailing_list_resource.rb +51 -0
- data/lib/qrapi/resources/organization_resource.rb +12 -0
- data/lib/qrapi/resources/response_export_resource.rb +49 -0
- data/lib/qrapi/resources/response_import_resource.rb +26 -0
- data/lib/qrapi/resources/survey_resource.rb +27 -0
- data/lib/qrapi/resources/user_resource.rb +35 -0
- data/lib/qrapi/utils/kartograph_optional_properties.rb +21 -0
- data/lib/qrapi/version.rb +3 -0
- metadata +271 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6f55c877f28d86f3eef311c12ca12259e4cc657f9e62ace2f9276dae6f81a460
|
4
|
+
data.tar.gz: 4ae23eaeb89fb89ba8e08cb4247828c9b30326865abfdcb99bc32de6631fd5fe
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 91bfc6a270ee30f6cf7b27707271c8806994090fde5bcb5ea12b0ae169c8c3a84aec0c0554214da6277d3741eb1a5139253e860bd5aa759e13e69af7fcb7a537
|
7
|
+
data.tar.gz: eda8bb8744cd27b4e5a0ca1c740beaffb6e4d62fdea5e6d3372b08f3318c5427660e118f150f73e5245f3952c62678eea98e735ce4246823f9c775223fcd66fc
|
data/lib/qrapi.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require "zip"
|
2
|
+
require "json"
|
3
|
+
require "kartograph"
|
4
|
+
require "resource_kit"
|
5
|
+
require "qrapi/version"
|
6
|
+
require "active_model"
|
7
|
+
require "active_support/all"
|
8
|
+
|
9
|
+
module QRAPI
|
10
|
+
autoload :Client, "qrapi/client"
|
11
|
+
|
12
|
+
# Models
|
13
|
+
autoload :BaseModel, "qrapi/models/base_model"
|
14
|
+
autoload :Organization, "qrapi/models/organization"
|
15
|
+
autoload :Division, "qrapi/models/division"
|
16
|
+
autoload :Group, "qrapi/models/group"
|
17
|
+
autoload :Survey, "qrapi/models/survey"
|
18
|
+
Survey.autoload :Expiration, "qrapi/models/survey/expiration"
|
19
|
+
autoload :User, "qrapi/models/user"
|
20
|
+
autoload :LibraryMessage, "qrapi/models/library_message"
|
21
|
+
autoload :Distribution, "qrapi/models/distribution"
|
22
|
+
Distribution.autoload :Headers, "qrapi/models/distribution/headers"
|
23
|
+
Distribution.autoload :Message, "qrapi/models/distribution/message"
|
24
|
+
Distribution.autoload :Recipients, "qrapi/models/distribution/recipients"
|
25
|
+
Distribution.autoload :SurveyLink, "qrapi/models/distribution/survey_link"
|
26
|
+
autoload :ResponseExport, "qrapi/models/response_export"
|
27
|
+
autoload :Response, "qrapi/models/response"
|
28
|
+
autoload :MailingList, "qrapi/models/mailing_list"
|
29
|
+
MailingList.autoload :Contact, "qrapi/models/mailing_list/contact"
|
30
|
+
|
31
|
+
# Resources
|
32
|
+
autoload :OrganizationResource, "qrapi/resources/organization_resource"
|
33
|
+
autoload :DivisionResource, "qrapi/resources/division_resource"
|
34
|
+
autoload :GroupResource, "qrapi/resources/group_resource"
|
35
|
+
autoload :SurveyResource, "qrapi/resources/survey_resource"
|
36
|
+
autoload :UserResource, "qrapi/resources/user_resource"
|
37
|
+
autoload :LibraryMessageResource, "qrapi/resources/library_message_resource"
|
38
|
+
autoload :DistributionResource, "qrapi/resources/distribution_resource"
|
39
|
+
autoload :ResponseExportResource, "qrapi/resources/response_export_resource"
|
40
|
+
autoload :ResponseImportResource, "qrapi/resources/response_import_resource"
|
41
|
+
autoload :MailingListResource, "qrapi/resources/mailing_list_resource"
|
42
|
+
|
43
|
+
# JSON Maps
|
44
|
+
autoload :OrganizationMapping, "qrapi/mappings/organization_mapping"
|
45
|
+
autoload :DivisionMapping, "qrapi/mappings/division_mapping"
|
46
|
+
autoload :GroupMapping, "qrapi/mappings/group_mapping"
|
47
|
+
autoload :SurveyMapping, "qrapi/mappings/survey_mapping"
|
48
|
+
autoload :ExpirationMapping, "qrapi/mappings/expiration_mapping"
|
49
|
+
autoload :UserMapping, "qrapi/mappings/user_mapping"
|
50
|
+
autoload :LibraryMessageMapping, "qrapi/mappings/library_message_mapping"
|
51
|
+
autoload :DistributionMapping, "qrapi/mappings/distribution_mapping"
|
52
|
+
autoload :ResponseExportMapping, "qrapi/mappings/response_export_mapping"
|
53
|
+
autoload :ResponsesMapping, "qrapi/mappings/responses_mapping"
|
54
|
+
autoload :MailingListMapping, "qrapi/mappings/mailing_list_mapping"
|
55
|
+
autoload :ContactMapping, "qrapi/mappings/contact_mapping"
|
56
|
+
|
57
|
+
# Utils
|
58
|
+
autoload :ErrorHandlingResourceable, "qrapi/error_handling_resourceable"
|
59
|
+
autoload :Kartograph, "qrapi/utils/kartograph_optional_properties"
|
60
|
+
|
61
|
+
# Errors
|
62
|
+
autoload :ErrorMapping, "qrapi/mappings/error_mapping"
|
63
|
+
Error = Class.new(StandardError)
|
64
|
+
FailedCreate = Class.new(QRAPI::Error)
|
65
|
+
FailedUpdate = Class.new(QRAPI::Error)
|
66
|
+
|
67
|
+
class RateLimitReached < QRAPI::Error
|
68
|
+
attr_accessor :reset_at
|
69
|
+
attr_writer :limit, :remaining
|
70
|
+
|
71
|
+
def limit
|
72
|
+
@limit.to_i if @limit
|
73
|
+
end
|
74
|
+
|
75
|
+
def remaining
|
76
|
+
@remaning.to_i if @remaning
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/qrapi/client.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require "faraday"
|
2
|
+
|
3
|
+
module QRAPI
|
4
|
+
class Client
|
5
|
+
attr_reader :api_token, :data_center_id
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@api_token = options[:api_token]
|
9
|
+
@data_center_id = options[:data_center_id]
|
10
|
+
end
|
11
|
+
|
12
|
+
def connection
|
13
|
+
Faraday.new(connection_options) do |req|
|
14
|
+
req.adapter :net_http
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.resources
|
19
|
+
{
|
20
|
+
organization: OrganizationResource,
|
21
|
+
divisions: DivisionResource,
|
22
|
+
groups: GroupResource,
|
23
|
+
users: UserResource,
|
24
|
+
surveys: SurveyResource,
|
25
|
+
response_exports: ResponseExportResource,
|
26
|
+
response_imports: ResponseImportResource,
|
27
|
+
library_messages: LibraryMessageResource,
|
28
|
+
distributions: DistributionResource,
|
29
|
+
mailing_lists: MailingListResource
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_missing(name, *args, &block)
|
34
|
+
if self.class.resources.keys.include?(name)
|
35
|
+
resources[name] ||= self.class.resources[name].new(connection: connection)
|
36
|
+
resources[name]
|
37
|
+
else
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def resources
|
43
|
+
@resources ||= {}
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def connection_options
|
49
|
+
{
|
50
|
+
url: "https://#{@data_center_id}.qualtrics.com",
|
51
|
+
headers: {
|
52
|
+
content_type: "application/json",
|
53
|
+
X_API_TOKEN: api_token
|
54
|
+
}
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ErrorHandlingResourceable
|
2
|
+
def self.included(base)
|
3
|
+
base.send(:resources) do
|
4
|
+
default_handler do |response|
|
5
|
+
if (200...299).include?(response.status)
|
6
|
+
next
|
7
|
+
elsif response.status == 429
|
8
|
+
error = QRAPI::RateLimitReached.new("#{response.status}: #{response.body}")
|
9
|
+
error.limit = response.headers["RateLimit-Limit"]
|
10
|
+
error.remaining = response.headers["RateLimit-Remaining"]
|
11
|
+
error.reset_at = response.headers["RateLimit-Reset"]
|
12
|
+
raise error
|
13
|
+
else
|
14
|
+
body = JSON.parse(response.body)
|
15
|
+
error_message = body["meta"]["error"]["errorMessage"]
|
16
|
+
error_code = body["meta"]["error"]["errorCode"]
|
17
|
+
raise QRAPI::Error.new("Status: #{response.status} Error: #{error_code} - #{error_message}")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module QRAPI
|
2
|
+
class ContactMapping
|
3
|
+
include Kartograph::DSL
|
4
|
+
|
5
|
+
kartograph do
|
6
|
+
mapping MailingList::Contact
|
7
|
+
root_key plural: "elements", scopes: [:read]
|
8
|
+
|
9
|
+
property :id, scopes: [:read]
|
10
|
+
|
11
|
+
scoped :read, :update do
|
12
|
+
property :first_name, key: "firstName", optional: true
|
13
|
+
property :last_name, key: "lastName", optional: true
|
14
|
+
property :email, optional: true
|
15
|
+
property :external_data_reference, key: "externalDataReference", optional: true
|
16
|
+
property :language, optional: true
|
17
|
+
property :embedded_data, key: "embeddedData", optional: true
|
18
|
+
property :unsubscribed, optional: true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module QRAPI
|
2
|
+
class DistributionMapping
|
3
|
+
include Kartograph::DSL
|
4
|
+
|
5
|
+
kartograph do
|
6
|
+
mapping Distribution
|
7
|
+
root_key singular: "result", plural: "elements", scopes: [:read]
|
8
|
+
|
9
|
+
property :send_date, scopes: [:read, :create, :create_child], key: "sendDate"
|
10
|
+
|
11
|
+
property :recipients, scopes: [:read, :create] do
|
12
|
+
mapping Distribution::Recipients
|
13
|
+
|
14
|
+
property :mailing_list_id, scopes: [:read, :create], key: "mailingListId"
|
15
|
+
property :contact_id, scopes: [:read, :create], key: "contactId", optional: true
|
16
|
+
end
|
17
|
+
|
18
|
+
property :headers, scopes: [:read, :create, :create_child] do
|
19
|
+
mapping Distribution::Headers
|
20
|
+
|
21
|
+
property :from_email, scopes: [:read, :create, :create_child], key: "fromEmail"
|
22
|
+
property :from_name, scopes: [:read, :create, :create_child], key: "fromName"
|
23
|
+
property :reply_to_email, scopes: [:read, :create, :create_child], key: "replyToEmail"
|
24
|
+
property :subject, scopes: [:read, :create, :create_child]
|
25
|
+
end
|
26
|
+
|
27
|
+
property :message, scopes: [:read, :create], optional: true do
|
28
|
+
mapping Distribution::Message
|
29
|
+
|
30
|
+
property :message_id, scopes: [:read, :create], key: "messageId"
|
31
|
+
property :library_id, scopes: [:read, :create], key: "libraryId"
|
32
|
+
property :message_text, scopes: [:read, :create], key: "messageText"
|
33
|
+
end
|
34
|
+
|
35
|
+
property :survey_link, scopes: [:read, :create], optional: true, key: "surveyLink" do
|
36
|
+
mapping Distribution::SurveyLink
|
37
|
+
|
38
|
+
property :survey_id, scopes: [:read, :create], key: "surveyId"
|
39
|
+
property :expiration_date, scopes: [:read, :create], key: "expirationDate"
|
40
|
+
property :type, scopes: [:read, :create]
|
41
|
+
end
|
42
|
+
|
43
|
+
scoped :read do
|
44
|
+
property :id
|
45
|
+
property :parent_distribution_id, key: "parentDistributionId"
|
46
|
+
property :owner_id, key: "ownerId"
|
47
|
+
property :organization_id, key: "organizationId"
|
48
|
+
property :request_status, key: "requestStatus"
|
49
|
+
property :request_type, key: "requestType"
|
50
|
+
property :created_date, key: "createdDate"
|
51
|
+
property :modified_date, key: "modifiedDate"
|
52
|
+
property :stats
|
53
|
+
end
|
54
|
+
|
55
|
+
scoped :create_child do
|
56
|
+
property :message do
|
57
|
+
mapping Distribution::Message
|
58
|
+
|
59
|
+
scoped :create_child do
|
60
|
+
property :message_id, key: "messageId"
|
61
|
+
property :library_id, key: "libraryId"
|
62
|
+
property :message_text, key: "messageText"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module QRAPI
|
2
|
+
class DivisionMapping
|
3
|
+
include Kartograph::DSL
|
4
|
+
|
5
|
+
kartograph do
|
6
|
+
mapping Division
|
7
|
+
root_key singular: "result", scopes: [:read]
|
8
|
+
|
9
|
+
property :name, scopes: [:read, :update], optional: true
|
10
|
+
property :status, scopes: [:read, :update], optional: true
|
11
|
+
# property :permissions, scopes: [:read, :create, :update], optional: true
|
12
|
+
|
13
|
+
scoped :read do
|
14
|
+
property :id
|
15
|
+
property :organization_id, key: "organizationId"
|
16
|
+
property :creation_date, key: "creationDate"
|
17
|
+
property :creator_id, key: "creatorId"
|
18
|
+
property :response_counts, key: "responseCounts"
|
19
|
+
end
|
20
|
+
|
21
|
+
scoped :create do
|
22
|
+
property :name
|
23
|
+
property :division_admins, key: "divisionAdmins", optional: true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module QRAPI
|
2
|
+
class ErrorMapping
|
3
|
+
Error = Struct.new(:message, :id)
|
4
|
+
|
5
|
+
include Kartograph::DSL
|
6
|
+
|
7
|
+
kartograph do
|
8
|
+
mapping Error
|
9
|
+
|
10
|
+
property :id, scopes: [:read]
|
11
|
+
property :message, scopes: [:read]
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.fail_with(klass, content)
|
15
|
+
error = extract_single(content, :read)
|
16
|
+
raise klass, error.message
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module QRAPI
|
2
|
+
class GroupMapping
|
3
|
+
include Kartograph::DSL
|
4
|
+
|
5
|
+
kartograph do
|
6
|
+
mapping Group
|
7
|
+
root_key singular: "result", plural: "elements", scopes: [:read]
|
8
|
+
|
9
|
+
property :type, scopes: [:read, :create]
|
10
|
+
property :name, scopes: [:read, :create]
|
11
|
+
property :division_id, scopes: [:read, :update], key: "divisionId", optional: true
|
12
|
+
|
13
|
+
scoped :read do
|
14
|
+
property :id
|
15
|
+
property :organization_id, key: "organizationId"
|
16
|
+
property :all_users, key: "allUsers"
|
17
|
+
property :creation_date, key: "creationDate"
|
18
|
+
property :creator_id, key: "creatorId"
|
19
|
+
end
|
20
|
+
|
21
|
+
scoped :update do
|
22
|
+
property :type, optional: true
|
23
|
+
property :name, optional: true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module QRAPI
|
2
|
+
class LibraryMessageMapping
|
3
|
+
include Kartograph::DSL
|
4
|
+
|
5
|
+
kartograph do
|
6
|
+
mapping LibraryMessage
|
7
|
+
root_key singular: "result", plural: "elements", scopes: [:read]
|
8
|
+
|
9
|
+
property :id, scopes: [:read]
|
10
|
+
property :category, scopes: [:read, :create]
|
11
|
+
property :messages, scopes: [:read, :update], optional: true
|
12
|
+
property :description, scopes: [:read, :update], optional: true
|
13
|
+
|
14
|
+
scoped :create do
|
15
|
+
property :description
|
16
|
+
property :messages
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module QRAPI
|
2
|
+
class MailingListMapping
|
3
|
+
include Kartograph::DSL
|
4
|
+
|
5
|
+
kartograph do
|
6
|
+
mapping MailingList
|
7
|
+
root_key singular: "result", plural: "elements", scopes: [:read]
|
8
|
+
|
9
|
+
property :id, scopes: [:read]
|
10
|
+
property :name, scopes: [:read, :create]
|
11
|
+
property :library_id, scopes: [:read, :create], key: "libraryId"
|
12
|
+
property :category, scopes: [:read, :create, :update], optional: true
|
13
|
+
|
14
|
+
scoped :update do
|
15
|
+
property :name, optional: true
|
16
|
+
property :library_id, key: "libraryId", optional: true
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module QRAPI
|
2
|
+
class OrganizationMapping
|
3
|
+
include Kartograph::DSL
|
4
|
+
|
5
|
+
kartograph do
|
6
|
+
mapping Organization
|
7
|
+
root_key singular: "result", scopes: [:read]
|
8
|
+
|
9
|
+
scoped :read do
|
10
|
+
property :id
|
11
|
+
property :name
|
12
|
+
property :base_url
|
13
|
+
property :type
|
14
|
+
property :status
|
15
|
+
property :stats
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module QRAPI
|
2
|
+
class ResponseExportMapping
|
3
|
+
include Kartograph::DSL
|
4
|
+
|
5
|
+
kartograph do
|
6
|
+
mapping ResponseExport
|
7
|
+
root_key singular: "result", scopes: [:read]
|
8
|
+
|
9
|
+
scoped :read do
|
10
|
+
property :percent_complete, key: "percentComplete"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module QRAPI
|
2
|
+
class ResponsesMapping
|
3
|
+
# Maps each of the responses to a QRAPI::Response model
|
4
|
+
#
|
5
|
+
# @return [QRAPI::Response]
|
6
|
+
def self.from_json_or_xml(serialized_response, f_type)
|
7
|
+
case f_type
|
8
|
+
when :json
|
9
|
+
responses_hash = JSON.parse(serialized_response)
|
10
|
+
unmapped_responses = responses_hash["responses"]
|
11
|
+
when :xml
|
12
|
+
responses_hash = Hash.from_xml(serialized_response)
|
13
|
+
unmapped_responses = responses_hash["xml"]["Response"]
|
14
|
+
end
|
15
|
+
|
16
|
+
responses = []
|
17
|
+
unmapped_responses.each do |response|
|
18
|
+
r = Response.new
|
19
|
+
r.id = response["ResponseID"]
|
20
|
+
r.response_set = response["ResponseSet"]
|
21
|
+
r.ip_address = response["IPAddress"]
|
22
|
+
r.start_date = response["StartDate"]
|
23
|
+
r.end_date = response["EndDate"]
|
24
|
+
r.last_name = response["RecipientLastName"]
|
25
|
+
r.first_name = response["RecipientFirstName"]
|
26
|
+
r.email = response["RecipientEmail"]
|
27
|
+
r.external_data_reference = response["ExternalDataReference"]
|
28
|
+
r.finished = response["Finished"]
|
29
|
+
r.status = response["Status"]
|
30
|
+
r.location_latitude = response["LocationLatitude"]
|
31
|
+
r.location_longitude = response["LocationLongitude"]
|
32
|
+
r.location_accuracy = response["LocationAccuracy"]
|
33
|
+
|
34
|
+
r.questions = {}
|
35
|
+
response.keys.grep(/Q([0-9xy_]+)/).each do |key|
|
36
|
+
r.questions[key] = response[key]
|
37
|
+
end
|
38
|
+
|
39
|
+
responses << r
|
40
|
+
end
|
41
|
+
|
42
|
+
responses
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|