dynamics_crm 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 511b29b2b863fee746767bb6bf4781d043f13969
4
- data.tar.gz: 9bffa8007f6f12332be94fb81e447b05c43c88e8
3
+ metadata.gz: 98498eb29f5847c9e344859c2a728367d4e5227a
4
+ data.tar.gz: a6621ceb97f27de1298c7afba4b99cfd6f5fa44b
5
5
  SHA512:
6
- metadata.gz: d093920089243f8f13e586cb5815c5b4ff2ddaffa504bd5e41e9de1f9c4e15a3356126e47887373bac5fe29d2b837432c299d97350c3afdd2b938fb3edf28499
7
- data.tar.gz: 45c230a953e78736440accdea82c61d77c440b1c5efdd7779a40131d11e9ea91e025ea2a0092889de1dcf8f2b49ab24dbeea483a5fce455079177d075b35e156
6
+ metadata.gz: 1773ffbe9669459911e9c08bc44f241fa825ca14858f738084e7179cb5b46b965b8c01222c3c6c0b98651da0da0dbdd3f59d52c4cfe304197950e9473379b666
7
+ data.tar.gz: 261c9ae185cfdd0151ff118a56f78bb873ca69ef866dfbee46b9bc4e85265292c1cd1d5dad7ef1b18c143668fb9798938bf90089d92cb4dae3bb37d7a071e4a9
data/.gitignore CHANGED
@@ -3,6 +3,7 @@
3
3
  .bundle
4
4
  .config
5
5
  .yardoc
6
+ .idea
6
7
  Gemfile.lock
7
8
  InstalledFiles
8
9
  _yardoc
@@ -16,3 +17,4 @@ test/tmp
16
17
  test/version_tmp
17
18
  tmp
18
19
  vendor
20
+
data/.travis.yml CHANGED
@@ -1,7 +1,8 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.0.0
4
3
  - 2.1.0
4
+ - 2.2.0
5
+ - 2.3.0
5
6
  notifications:
6
7
  email:
7
8
  recipients:
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 0.8.0 (January 4, 2017)
2
+ * Support ArrayOfEntity so one can create bound entities like ActivityParties on create #41
3
+ * Add special case for nil value #43
4
+ * Add support for double type #44
5
+ * Support Or queries in Criteria #53
6
+ * Removed dependency on Curb #54
7
+ * Adds PageInfo support to QueryExpression. #56
8
+
1
9
  ## 0.7.0 (March 4, 2016)
2
10
  * Add EntityCollection to xml attributes #24
3
11
  * Fix illegal XML characters #25
data/README.md CHANGED
@@ -41,8 +41,33 @@ client.retrieve('account', '53291AAB-4A9A-E311-B097-6C3BE5A8DD60')
41
41
  client.retrieve_multiple('account', [["name", "Equal", "Test Account"]])
42
42
  # => [#<DynamicsCRM::XML::Entity ... >]
43
43
 
44
- client.retrieve_multiple('account', [["name", "Equal", "Test Account"], ["Name, "CreatedBy"]])
44
+ client.retrieve_multiple('account', [["name", "Equal", "Test Account"], ['salesstage', 'In', [0, 1, 2]]])
45
45
  # => [#<DynamicsCRM::XML::Entity ... >]
46
+
47
+ client.retrieve_multiple('account', [["telephone1", "EndsWith", "5558675309"], ["mobilephone", "EndsWith", "5558675309"]], [], "Or")
48
+ # => [#<DynamicsCRM::XML::Entity ... >]
49
+ ```
50
+
51
+ ### retrieve_multiple using QueryExpression
52
+
53
+ ```ruby
54
+ # Build QueryExpression
55
+ query = DynamicsCRM::XML::QueryExpression.new('account')
56
+ query.columns = %w(accountid name)
57
+ query.criteria.add_condition('name', 'NotEqual', 'Test Account')
58
+ # Optional PageInfo
59
+ query.page_info = DynamicsCRM::XML::PageInfo.new(count: 5, page_number: 1, return_total_record_count: true)
60
+
61
+ # Get first page
62
+ result = client.retrieve_multiple(query)
63
+
64
+ while result.MoreRecords
65
+ # Next page
66
+ query.page_info.page_number += 1
67
+ query.page_info.paging_cookie = result.PagingCookie
68
+
69
+ result = client.retrieve_multiple(query)
70
+ end
46
71
  ```
47
72
 
48
73
  ### fetch (FetchXml)
data/dynamics_crm.gemspec CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
8
8
  spec.version = DynamicsCRM::VERSION
9
9
  spec.authors = ["Joe Heth"]
10
10
  spec.email = ["joeheth@gmail.com"]
11
- spec.description = %q{Ruby API for integrating with MS Dynamics 2011/2013 SOAP API}
12
- spec.summary = %q{Ruby gem for integrating with MS Dynamics 2011/2013 SOAP API}
11
+ spec.description = %q{Ruby API for integrating with MS Dynamics SOAP API}
12
+ spec.summary = %q{Ruby gem for integrating with MS Dynamics SOAP API}
13
13
  spec.homepage = "https://github.com/TinderBox/dynamics_crm"
14
14
  spec.license = "MIT"
15
15
 
@@ -18,7 +18,6 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
- spec.add_runtime_dependency 'curb', '>= 0.8', '< 1.0.0'
22
21
  spec.add_runtime_dependency 'mimemagic', '>= 0.2', '< 4.0.0'
23
22
  spec.add_runtime_dependency 'builder', '>= 3.0.0', '< 4.0.0'
24
23
 
data/lib/dynamics_crm.rb CHANGED
@@ -4,14 +4,16 @@ require "dynamics_crm/xml/message_builder"
4
4
  require 'dynamics_crm/xml/message_parser'
5
5
  require "dynamics_crm/xml/fault"
6
6
  require "dynamics_crm/xml/attributes"
7
+ require "dynamics_crm/xml/condition_expression"
7
8
  require "dynamics_crm/xml/column_set"
8
9
  require "dynamics_crm/xml/criteria"
9
- require "dynamics_crm/xml/query"
10
+ require "dynamics_crm/xml/query_expression"
10
11
  require "dynamics_crm/xml/fetch_expression"
11
12
  require "dynamics_crm/xml/entity"
12
13
  require "dynamics_crm/xml/entity_reference"
13
14
  require "dynamics_crm/xml/entity_collection"
14
15
  require "dynamics_crm/xml/money"
16
+ require "dynamics_crm/xml/page_info"
15
17
  require "dynamics_crm/response/result"
16
18
  require "dynamics_crm/response/retrieve_result"
17
19
  require "dynamics_crm/response/retrieve_multiple_result"
@@ -31,6 +33,7 @@ require "dynamics_crm/metadata/retrieve_all_entities_response"
31
33
  require "dynamics_crm/metadata/retrieve_entity_response"
32
34
  require "dynamics_crm/metadata/retrieve_attribute_response"
33
35
  require "dynamics_crm/metadata/retrieve_metadata_changes_response"
36
+ require "dynamics_crm/metadata/double"
34
37
  # Model
35
38
  require "dynamics_crm/model/entity"
36
39
  require "dynamics_crm/model/opportunity"
@@ -44,8 +47,8 @@ require "dynamics_crm/client"
44
47
  require 'bigdecimal'
45
48
  require 'base64'
46
49
  require "rexml/document"
50
+ require 'net/https'
47
51
  require 'mimemagic'
48
- require 'curl'
49
52
  require 'securerandom'
50
53
  require 'date'
51
54
  require 'cgi'
@@ -141,15 +141,19 @@ module DynamicsCRM
141
141
  })
142
142
  end
143
143
 
144
- def retrieve_multiple(entity_name, criteria=[], columns=[])
145
-
146
- query = XML::Query.new(entity_name)
147
- query.columns = columns
148
- query.criteria = XML::Criteria.new(criteria)
144
+ # Suports parameter list or QueryExpression object.
145
+ def retrieve_multiple(entity_name, criteria = [], columns = [], operator = nil)
146
+ if entity_name.is_a?(XML::QueryExpression)
147
+ query = entity_name
148
+ else
149
+ query = XML::QueryExpression.new(entity_name)
150
+ query.columns = columns
151
+ query.criteria = XML::Criteria.new(criteria, filter_operator: operator)
152
+ end
149
153
 
150
154
  request = retrieve_multiple_request(query)
151
155
  xml_response = post(organization_endpoint, request)
152
- return Response::RetrieveMultipleResult.new(xml_response)
156
+ Response::RetrieveMultipleResult.new(xml_response)
153
157
  end
154
158
 
155
159
  def fetch(fetchxml)
@@ -321,30 +325,29 @@ module DynamicsCRM
321
325
 
322
326
  def post(url, request)
323
327
  log_xml("REQUEST", request)
328
+ uri = URI.parse(url)
324
329
 
325
- c = Curl::Easy.new(url) do |http|
326
- # Set up headers.
327
- http.headers["Connection"] = "Keep-Alive"
328
- http.headers["Content-type"] = "application/soap+xml; charset=UTF-8"
329
- http.headers["Content-length"] = request.bytesize
330
-
331
- http.ssl_verify_peer = false
332
- http.timeout = timeout
333
- http.follow_location = true
334
- http.ssl_version = 1
335
- # http.verbose = 1
336
- end
330
+ http = Net::HTTP.new uri.host, uri.port
331
+ http.use_ssl = true
332
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
333
+
334
+ req = Net::HTTP::Post.new(uri.request_uri, {
335
+ "Connection" => "Keep-Alive",
336
+ "Content-type" => "application/soap+xml; charset=UTF-8",
337
+ "Content-length" => request.bytesize.to_s
338
+ })
339
+ req.body = request
340
+ response = http.request(req)
337
341
 
338
- if c.http_post(request)
339
- response = c.body_str
342
+ if response.code.to_i == 200
343
+ response_body = response.body
340
344
  else
341
345
  # Do something here on error.
342
346
  end
343
- c.close
344
347
 
345
- log_xml("RESPONSE", response)
348
+ log_xml("RESPONSE", response_body)
346
349
 
347
- response
350
+ response_body
348
351
  end
349
352
 
350
353
  def log_xml(title, xml)
@@ -0,0 +1,5 @@
1
+ module DynamicsCRM
2
+ module Metadata
3
+ Double = Struct.new(:value)
4
+ end
5
+ end
@@ -1,3 +1,3 @@
1
1
  module DynamicsCRM
2
- VERSION = "0.7.1"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -11,6 +11,8 @@ module DynamicsCRM
11
11
  def get_type(key, value)
12
12
  type = "string"
13
13
  case value
14
+ when ::Array
15
+ type = "ArrayOfEntity"
14
16
  when ::Fixnum
15
17
  type = "int"
16
18
  when ::BigDecimal, ::Float
@@ -31,6 +33,8 @@ module DynamicsCRM
31
33
  type = "FetchExpression"
32
34
  when Money
33
35
  type = "Money"
36
+ when DynamicsCRM::Metadata::Double
37
+ type = "double"
34
38
  when DynamicsCRM::Metadata::FilterExpression
35
39
  type = "FilterExpression"
36
40
  when DynamicsCRM::Metadata::PropertiesExpression
@@ -111,6 +115,13 @@ module DynamicsCRM
111
115
  def render_value_xml(type, value)
112
116
  xml = ""
113
117
  case type
118
+ when "ArrayOfEntity"
119
+ raise "We can only serialize Entities inside of ArrayOfEntity" unless value.all?{|a| a.is_a?(DynamicsCRM::XML::Entity)}
120
+ xml << %Q{
121
+ <c:value i:type="a:ArrayOfEntity">
122
+ #{value.map(&->(_) { _.to_xml({in_array: true}) }).join}
123
+ </c:value>
124
+ }
114
125
  when "EntityReference"
115
126
  xml << %Q{
116
127
  <c:value i:type="a:EntityReference">
@@ -131,7 +142,11 @@ module DynamicsCRM
131
142
  s_namespace = "http://schemas.microsoft.com/xrm/2011/Metadata"
132
143
  end
133
144
 
134
- if type == "guid"
145
+ if value.nil?
146
+ xml << %Q{
147
+ <c:value i:nil="true"></c:value>
148
+ }
149
+ elsif type == "guid"
135
150
  xml << %Q{
136
151
  <c:value xmlns:d="http://schemas.microsoft.com/2003/10/Serialization/" i:type="d:guid">#{value}</c:value>
137
152
  }
@@ -143,6 +158,10 @@ module DynamicsCRM
143
158
  xml << %Q{
144
159
  <c:value i:type="s:#{type}" xmlns:s="http://www.w3.org/2001/XMLSchema">#{value.utc.strftime('%Y-%m-%dT%H:%M:%SZ')}</c:value>
145
160
  }
161
+ elsif type == "double"
162
+ xml << %Q{
163
+ <c:value i:type="s:#{type}" xmlns:s="#{s_namespace}">#{value.value}</c:value>
164
+ }
146
165
  else
147
166
  xml << %Q{
148
167
  <c:value i:type="s:#{type}" xmlns:s="#{s_namespace}">#{value}</c:value>
@@ -154,14 +173,14 @@ module DynamicsCRM
154
173
  end
155
174
 
156
175
  def render_object_xml(type, value)
157
- case type
158
- when "EntityQueryExpression"
159
- xml = %Q{<c:value i:type="d:#{type}" xmlns:d="http://schemas.microsoft.com/xrm/2011/Metadata/Query">} << value.to_xml({namespace: 'd'}) << "</c:value>"
160
- else
161
- xml = %Q{<c:value i:type="a:#{type}">} << value.to_xml({exclude_root: true, namespace: 'a'}) << "</c:value>"
162
- end
176
+ case type
177
+ when "EntityQueryExpression"
178
+ xml = %Q{<c:value i:type="d:#{type}" xmlns:d="http://schemas.microsoft.com/xrm/2011/Metadata/Query">} << value.to_xml({namespace: 'd'}) << "</c:value>"
179
+ else
180
+ xml = %Q{<c:value i:type="a:#{type}">} << value.to_xml({exclude_root: true, namespace: 'a'}) << "</c:value>"
181
+ end
163
182
 
164
- return xml
183
+ xml
165
184
  end
166
185
 
167
186
  def class_name
@@ -0,0 +1,48 @@
1
+ module DynamicsCRM
2
+ module XML
3
+ # Loosely based on https://msdn.microsoft.com/en-us/library/gg334419.aspx
4
+ # Creates a ConditionExpression element to be used in retrieve calls.
5
+ class ConditionExpression
6
+ attr_accessor :attr_name, :operator, :value, :type
7
+ def initialize(attr_name, operator, value, type: nil)
8
+ @attr_name = attr_name
9
+ @operator = operator
10
+ @value = value
11
+ @values = Array(value)
12
+ @type = type
13
+ end
14
+
15
+ def value_type
16
+ return type unless type.nil?
17
+
18
+ type = @values.first.class.to_s.downcase
19
+ if type == 'fixnum'
20
+ type = 'int'
21
+ elsif %w(trueclass falseclass).include?(type)
22
+ type = 'boolean'
23
+ end
24
+
25
+ type
26
+ end
27
+
28
+ def to_xml(options = {})
29
+ ns = options[:namespace] ? options[:namespace] : 'a'
30
+
31
+ expression = %(<#{ns}:ConditionExpression>
32
+ <#{ns}:AttributeName>#{attr_name}</#{ns}:AttributeName>
33
+ <#{ns}:Operator>#{operator}</#{ns}:Operator>
34
+ <#{ns}:Values xmlns:d="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
35
+ )
36
+ @values.each do |v|
37
+ expression << %(<d:anyType i:type="s:#{value_type}" xmlns:s="http://www.w3.org/2001/XMLSchema">#{v}</d:anyType>)
38
+ end
39
+
40
+ expression << %(
41
+ </#{ns}:Values>
42
+ </#{ns}:ConditionExpression>)
43
+
44
+ expression
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,52 +1,44 @@
1
1
  module DynamicsCRM
2
2
  module XML
3
-
4
3
  class Criteria < Array
4
+ SUPPORTED_OPERATORS = %w(And Or)
5
+
6
+ def initialize(tuples = [], filter_operator: nil)
7
+ filter_operator ||= 'And'
8
+ raise "Supported operators: #{SUPPORTED_OPERATORS.join(',')}" if !SUPPORTED_OPERATORS.include?(filter_operator)
9
+
10
+ super(tuples)
11
+ @filter_operator = filter_operator
12
+
13
+ # Convert to ConditionExpression
14
+ @expressions = self.map do |tuple|
15
+ attr_name, operator, value, data_type = *tuple
16
+ ConditionExpression.new(attr_name, operator, value, type: data_type)
17
+ end
18
+ end
5
19
 
6
- attr_accessor :filter_operator
7
- def initialize(tuples=[])
8
- super
9
- @filter_operator = 'And'
20
+ def add_condition(attr_name, operator, value, type: nil)
21
+ @expressions << ConditionExpression.new(attr_name, operator, value, type: type)
10
22
  end
11
23
 
12
24
  # ConditionExpression can be repeated multiple times
13
- # Operator: can be lots of values such as: eq (Equals), neq (Not Equals), gt (Greater Than)
25
+ # Operator: can be lots of values such as: eq (Equals), neq (Not Equals), gt (Greater Than)
14
26
  # get the values from a fetch xml query
15
27
  # Values -> Value can be repeated multiple times
16
28
  # FilterOperator: and OR or depending on the filter requirements
17
- def to_xml(options={})
18
- ns = options[:namespace] ? options[:namespace] : "a"
29
+ def to_xml(options = {})
30
+ ns = options[:namespace] ? options[:namespace] : 'a'
19
31
 
20
- expressions = ""
21
- self.each do |tuple|
22
- attr_name = tuple[0]
23
- operator = tuple[1]
24
- values = tuple[2].is_a?(Array) ? tuple[2] : [tuple[2]]
25
- # TODO: Improve type detection
26
- type = (tuple[3] || values.first.class).to_s.downcase
27
- type = "int" if type == "fixnum"
28
- type = "boolean" if ["trueclass", "falseclass"].include?(type)
29
-
30
- expressions << %Q{<#{ns}:ConditionExpression>
31
- <#{ns}:AttributeName>#{attr_name}</#{ns}:AttributeName>
32
- <#{ns}:Operator>#{operator}</#{ns}:Operator>
33
- <#{ns}:Values xmlns:d="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
34
- }
35
- values.each do |v|
36
- expressions << %Q{<d:anyType i:type="s:#{type}" xmlns:s="http://www.w3.org/2001/XMLSchema">#{v}</d:anyType>}
37
- end
38
-
39
- expressions << %Q{
40
- </#{ns}:Values>
41
- </#{ns}:ConditionExpression>}
42
- end
32
+ xml_expression = @expressions.map do |conditional|
33
+ conditional.to_xml(options)
34
+ end.join('')
43
35
 
44
- %Q{<#{ns}:Criteria>
36
+ %(<#{ns}:Criteria>
45
37
  <#{ns}:Conditions>
46
- #{expressions}
38
+ #{xml_expression}
47
39
  </#{ns}:Conditions>
48
40
  <#{ns}:FilterOperator>#{@filter_operator}</#{ns}:FilterOperator>
49
- </#{ns}:Criteria>}
41
+ </#{ns}:Criteria>)
50
42
  end
51
43
  end
52
44
  # Criteria
@@ -24,11 +24,19 @@ module DynamicsCRM
24
24
 
25
25
  return inner_xml if options[:exclude_root]
26
26
 
27
- %Q{
28
- <entity xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts">
29
- #{inner_xml}
30
- </entity>
31
- }
27
+ if options[:in_array]
28
+ %Q{
29
+ <a:Entity>
30
+ #{inner_xml}
31
+ </a:Entity>
32
+ }
33
+ else
34
+ %Q{
35
+ <entity xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts">
36
+ #{inner_xml}
37
+ </entity>
38
+ }
39
+ end
32
40
  end
33
41
 
34
42
  def to_hash
@@ -1,26 +1,31 @@
1
1
  module DynamicsCRM
2
2
  module XML
3
-
4
3
  class PageInfo
5
-
6
4
  attr_accessor :count, :page_number, :paging_cookie, :return_total_record_count
7
- def initialize
8
- @count = 20
9
- @page_number = 1
10
- @paging_cookie = nil
11
- @return_total_record_count = false
5
+
6
+ def initialize(count: 20, page_number: 1, paging_cookie: nil, return_total_record_count: false)
7
+ @count = count
8
+ @page_number = page_number
9
+ @paging_cookie = paging_cookie
10
+ @return_total_record_count = return_total_record_count
12
11
  end
13
12
 
14
13
  # Using Entity vs entity causes the error: Value cannot be null.
15
14
  def to_xml
16
- %Q{
15
+ cookie = if paging_cookie.nil?
16
+ '<b:PagingCookie i:nil="true" />'
17
+ else
18
+ %(<b:PagingCookie>#{CGI.escapeHTML(paging_cookie)}</b:PagingCookie>)
19
+ end
20
+
21
+ %(
17
22
  <b:PageInfo>
18
23
  <b:Count>#{count}</b:Count>
19
24
  <b:PageNumber>#{page_number}</b:PageNumber>
20
- <b:PagingCookie i:nil="true" />
21
- <b:ReturnTotalRecordCount>false</b:ReturnTotalRecordCount>
25
+ #{cookie}
26
+ <b:ReturnTotalRecordCount>#{return_total_record_count}</b:ReturnTotalRecordCount>
22
27
  </b:PageInfo>
23
- }
28
+ )
24
29
  end
25
30
 
26
31
  def to_hash
@@ -31,8 +36,7 @@ module DynamicsCRM
31
36
  :return_total_record_count => return_total_record_count
32
37
  }
33
38
  end
34
-
35
39
  end
36
40
  # PageInfo
37
41
  end
38
- end
42
+ end
@@ -0,0 +1,43 @@
1
+ module DynamicsCRM
2
+ module XML
3
+ # Represents QueryExpression XML fragment.
4
+ class QueryExpression
5
+ attr_accessor :columns, :criteria, :entity_name, :page_info
6
+
7
+ def initialize(entity_name)
8
+ @entity_name = entity_name
9
+ @criteria = Criteria.new
10
+ end
11
+
12
+ def to_xml(options = {})
13
+ namespace = options[:namespace] ? options[:namespace] : 'b'
14
+
15
+ column_set = columns.is_a?(ColumnSet) ? columns : ColumnSet.new(columns)
16
+
17
+ xml = %(
18
+ #{column_set.to_xml(namespace: namespace, camel_case: true)}
19
+ #{criteria.to_xml(namespace: namespace)}
20
+ <#{namespace}:Distinct>false</#{namespace}:Distinct>
21
+ <#{namespace}:EntityName>#{entity_name}</#{namespace}:EntityName>
22
+ <#{namespace}:LinkEntities />
23
+ <#{namespace}:Orders />
24
+ )
25
+
26
+ xml << page_info.to_xml if page_info
27
+
28
+ if options[:exclude_root].nil?
29
+ xml = %(<query i:type="b:QueryExpression" xmlns:b="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
30
+ #{xml}
31
+ </query>)
32
+ end
33
+
34
+ xml
35
+ end
36
+ end
37
+ # QueryExpression
38
+
39
+ # Backward compatible class
40
+ class Query < QueryExpression
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,37 @@
1
+ <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
2
+ <s:Body>
3
+ <Create xmlns="http://schemas.microsoft.com/xrm/2011/Contracts/Services" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
4
+ <entity xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts">
5
+ <a:Attributes xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
6
+ <a:KeyValuePairOfstringanyType>
7
+ <b:key>from</b:key>
8
+ <b:value i:type="a:ArrayOfEntity">
9
+ <a:Entity>
10
+ <a:Attributes>
11
+ <a:KeyValuePairOfstringanyType>
12
+ <b:key>partyid</b:key>
13
+ <b:value i:type="a:EntityReference">
14
+ <a:Id>f36aa96c-e7a5-4c70-8254-47c8ba947561</a:Id>
15
+ <a:LogicalName>systemuser</a:LogicalName>
16
+ <a:Name i:nil="true" />
17
+ </b:value>
18
+ </a:KeyValuePairOfstringanyType>
19
+ </a:Attributes>
20
+ <a:EntityState i:nil="true" />
21
+ <a:FormattedValues />
22
+ <a:Id>00000000-0000-0000-0000-000000000000</a:Id>
23
+ <a:LogicalName>activityparty</a:LogicalName>
24
+ <a:RelatedEntities />
25
+ </a:Entity>
26
+ </b:value>
27
+ </a:KeyValuePairOfstringanyType>
28
+ </a:Attributes>
29
+ <a:EntityState i:nil="true" />
30
+ <a:FormattedValues xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
31
+ <a:Id>00000000-0000-0000-0000-000000000000</a:Id>
32
+ <a:LogicalName>phonecall</a:LogicalName>
33
+ <a:RelatedEntities xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
34
+ </entity>
35
+ </Create>
36
+ </s:Body>
37
+ </s:Envelope>
@@ -36,7 +36,7 @@ describe DynamicsCRM::Client do
36
36
  end
37
37
 
38
38
  context "On-Premise" do
39
- let(:subject) { DynamicsCRM::Client.new(organization_name: "psavtest", hostname: "psavtest.crm.powerobjects.net")}
39
+ let(:subject) { DynamicsCRM::Client.new(hostname: "customers.crm.psav.com")}
40
40
 
41
41
  it "authenticates with username and password" do
42
42
 
@@ -110,6 +110,30 @@ describe DynamicsCRM::Client do
110
110
  expect(entities[1].attributes["accountid"]).to eq("dbe9d7c9-2c98-e311-9752-6c3be5a87df0")
111
111
  expect(entities[2].attributes["accountid"]).to eq("8ff0325c-a592-e311-b7f3-6c3be5a8a0c8")
112
112
  end
113
+
114
+ it "retrieves multiple entities by criteria using OR" do
115
+
116
+ allow(subject).to receive(:post).and_return(fixture("retrieve_multiple_result"))
117
+
118
+ result = subject.retrieve_multiple("account", ["name", "Equal", "Test Account"], columns=[], 'Or')
119
+
120
+ expect(result).to be_a(DynamicsCRM::Response::RetrieveMultipleResult)
121
+ end
122
+
123
+ it "retrieves multiple entities by QueryExpression" do
124
+ allow(subject).to receive(:post).and_return(fixture("retrieve_multiple_result"))
125
+
126
+ query = DynamicsCRM::XML::QueryExpression.new('account')
127
+ query.columns = %w(accountid name)
128
+ query.criteria.add_condition('name', 'Equal', 'Test Account')
129
+
130
+ result = subject.retrieve_multiple(query)
131
+
132
+ expect(result).to be_a(DynamicsCRM::Response::RetrieveMultipleResult)
133
+
134
+ expect(result['EntityName']).to eq('account')
135
+ expect(result.entities.size).to eq(3)
136
+ end
113
137
  end
114
138
 
115
139
  describe "#retrieve_attachments" do
@@ -9,7 +9,9 @@ describe DynamicsCRM::XML::Attributes do
9
9
  "modifiedon" => Time.now,
10
10
  "donotemail" => true,
11
11
  "id" => "1bfa3886-df7e-468c-8435-b5adfb0441ed",
12
- "reference" => {"Id" => "someid", "Name" => "entityname", "LogicalName" => "opportunity"}
12
+ "reference" => {"Id" => "someid", "Name" => "entityname", "LogicalName" => "opportunity"},
13
+ "expireson" => nil,
14
+ "address1_latitude" => DynamicsCRM::Metadata::Double.new(5.22123)
13
15
  }
14
16
  }
15
17
  subject {
@@ -32,6 +34,11 @@ describe DynamicsCRM::XML::Attributes do
32
34
  it { expect(subject.to_xml).to include("<c:key>telephone1</c:key>") }
33
35
  it { expect(subject.to_xml).to include("<c:key>donotemail</c:key>") }
34
36
  it { expect(subject.to_xml).to include("<c:key>modifiedon</c:key>") }
37
+ it { expect(subject.to_xml).to include('<c:value i:nil="true"></c:value>') }
38
+ it do
39
+ expect(subject.to_xml)
40
+ .to include('<c:value i:type="s:double" xmlns:s="http://www.w3.org/2001/XMLSchema">5.22123</c:value>')
41
+ end
35
42
  end
36
43
 
37
44
  end
@@ -0,0 +1,83 @@
1
+ require 'spec_helper'
2
+
3
+ describe DynamicsCRM::XML::Criteria do
4
+ describe 'initialization' do
5
+ subject do
6
+ DynamicsCRM::XML::Criteria.new
7
+ end
8
+
9
+ it 'generates empty Criteria fragment' do
10
+ expected = %(<a:Criteria>
11
+ <a:Conditions>
12
+ </a:Conditions>
13
+ <a:FilterOperator>And</a:FilterOperator>
14
+ </a:Criteria>)
15
+
16
+ expect(subject.to_xml).to match_xml(expected)
17
+ end
18
+ end
19
+
20
+ describe 'single criteria' do
21
+ subject do
22
+ DynamicsCRM::XML::Criteria.new([['name', 'Equal', 'Test Opp']])
23
+ end
24
+ let(:expected) {
25
+ %(<a:Criteria>
26
+ <a:Conditions>
27
+ <a:ConditionExpression>
28
+ <a:AttributeName>name</a:AttributeName>
29
+ <a:Operator>Equal</a:Operator>
30
+ <a:Values xmlns:d="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
31
+ <d:anyType i:type="s:string" xmlns:s="http://www.w3.org/2001/XMLSchema">Test Opp</d:anyType>
32
+ </a:Values>
33
+ </a:ConditionExpression>
34
+ </a:Conditions>
35
+ <a:FilterOperator>And</a:FilterOperator>
36
+ </a:Criteria>)
37
+ }
38
+
39
+ it 'generates Criteria fragment with single ConditionExpression' do
40
+ expect(subject.to_xml).to match_xml expected
41
+ end
42
+
43
+ it 'set data type explicitly' do
44
+ # Supports optional fourth value for data type
45
+ subject = DynamicsCRM::XML::Criteria.new([['name', 'Equal', 'Test Opp', 'customstring']])
46
+
47
+ expect(subject.to_xml).to match_xml expected.gsub('s:string', 's:customstring')
48
+ end
49
+ end
50
+
51
+ describe 'multiple criteria' do
52
+ subject do
53
+ DynamicsCRM::XML::Criteria.new([
54
+ ['name', 'Equal', 'Test Opp'],
55
+ ['salesstage', 'In', [0, 1, 2]],
56
+ ])
57
+ end
58
+
59
+ it 'generates Criteria with multiple ConditionExpression(s)' do
60
+ expect(subject.to_xml).to match_xml %(<a:Criteria>
61
+ <a:Conditions>
62
+ <a:ConditionExpression>
63
+ <a:AttributeName>name</a:AttributeName>
64
+ <a:Operator>Equal</a:Operator>
65
+ <a:Values xmlns:d="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
66
+ <d:anyType i:type="s:string" xmlns:s="http://www.w3.org/2001/XMLSchema">Test Opp</d:anyType>
67
+ </a:Values>
68
+ </a:ConditionExpression>
69
+ <a:ConditionExpression>
70
+ <a:AttributeName>salesstage</a:AttributeName>
71
+ <a:Operator>In</a:Operator>
72
+ <a:Values xmlns:d="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
73
+ <d:anyType i:type="s:int" xmlns:s="http://www.w3.org/2001/XMLSchema">0</d:anyType>
74
+ <d:anyType i:type="s:int" xmlns:s="http://www.w3.org/2001/XMLSchema">1</d:anyType>
75
+ <d:anyType i:type="s:int" xmlns:s="http://www.w3.org/2001/XMLSchema">2</d:anyType>
76
+ </a:Values>
77
+ </a:ConditionExpression>
78
+ </a:Conditions>
79
+ <a:FilterOperator>And</a:FilterOperator>
80
+ </a:Criteria>)
81
+ end
82
+ end
83
+ end
@@ -60,4 +60,18 @@ describe DynamicsCRM::XML::Entity do
60
60
  end
61
61
  end
62
62
 
63
+ describe "entity with array" do
64
+ subject {
65
+ entity = DynamicsCRM::XML::Entity.new("activityparty")
66
+ entity.attributes = DynamicsCRM::XML::Attributes.new(
67
+ partyid: DynamicsCRM::XML::EntityReference.new("systemuser", "f36aa96c-e7a5-4c70-8254-47c8ba947561")
68
+ )
69
+ entity
70
+ }
71
+
72
+ context "#to_xml" do
73
+ it { expect(DynamicsCRM::XML::Attributes.new({to: [subject]}).to_xml).to include('<c:value i:type="a:ArrayOfEntity">') }
74
+ end
75
+ end
76
+
63
77
  end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ describe DynamicsCRM::XML::QueryExpression do
4
+
5
+ describe 'initialization' do
6
+ subject {
7
+ DynamicsCRM::XML::QueryExpression.new('opportunity')
8
+ }
9
+
10
+ context "generate empty QueryExpression fragment" do
11
+ it { expect(subject.to_xml).to include("<b:ColumnSet ") }
12
+ it { expect(subject.to_xml).to match(/<b:Conditions>\s+<\/b:Conditions>/) }
13
+ it { expect(subject.to_xml).to include("<b:AllColumns>true</b:AllColumns>") }
14
+ it { expect(subject.to_xml).to include("<b:Distinct>false</b:Distinct>") }
15
+ it { expect(subject.to_xml).to include("<b:EntityName>opportunity</b:EntityName>") }
16
+ it { expect(subject.to_xml).to include("<b:FilterOperator>And</b:FilterOperator>").or(include("<b:FilterOperator>Or</b:FilterOperator>")) }
17
+ end
18
+ end
19
+
20
+ describe 'Criteria' do
21
+ subject {
22
+ query = DynamicsCRM::XML::QueryExpression.new('opportunity')
23
+ query.criteria = DynamicsCRM::XML::Criteria.new([["name", "Equal", "Test Opp"]])
24
+ query
25
+ }
26
+
27
+ context "generate QueryExpression fragment" do
28
+ it { expect(subject.to_xml).to include("<b:ColumnSet ") }
29
+ it { expect(subject.to_xml).to include("<b:ConditionExpression") }
30
+ it { expect(subject.to_xml).to include("AttributeName>name</") }
31
+ it { expect(subject.to_xml).to include("Operator>Equal</") }
32
+ it { expect(subject.to_xml).to include('<d:anyType i:type="s:string" xmlns:s="http://www.w3.org/2001/XMLSchema">Test Opp</d:anyType>') }
33
+ it { expect(subject.to_xml).to include("<b:AllColumns>true</b:AllColumns>") }
34
+ it { expect(subject.to_xml).to include("<b:Distinct>false</b:Distinct>") }
35
+ it { expect(subject.to_xml).to include("<b:EntityName>opportunity</b:EntityName>") }
36
+ it { expect(subject.to_xml).to include("<b:FilterOperator>And</b:FilterOperator>").or(include("<b:FilterOperator>Or</b:FilterOperator>")) }
37
+ end
38
+ end
39
+
40
+ describe 'PageInfo' do
41
+ subject {
42
+ query = DynamicsCRM::XML::QueryExpression.new('account')
43
+ query.columns = %w(accountid name)
44
+ query.criteria.add_condition('name', 'NotEqual', 'Test Account')
45
+ query.page_info = DynamicsCRM::XML::PageInfo.new(count: 5, page_number: 2, return_total_record_count: true)
46
+ query
47
+ }
48
+
49
+ context "generate empty QueryExpression fragment" do
50
+ it { expect(subject.to_xml).to include('<b:ColumnSet ') }
51
+ it { expect(subject.to_xml).to include('<b:ConditionExpression') }
52
+ it { expect(subject.to_xml).to include('AttributeName>name</') }
53
+ it { expect(subject.to_xml).to include('Operator>NotEqual</') }
54
+ it { expect(subject.to_xml).to include('<d:anyType i:type="s:string" xmlns:s="http://www.w3.org/2001/XMLSchema">Test Account</d:anyType>') }
55
+ it { expect(subject.to_xml).to include('<b:AllColumns>false</b:AllColumns>') }
56
+ it { expect(subject.to_xml).to include('<b:Columns') }
57
+ it { expect(subject.to_xml).to include('<b:EntityName>account</b:EntityName>') }
58
+ it { expect(subject.to_xml).to include('<b:PageInfo>') }
59
+ it { expect(subject.to_xml).to include('<b:Count>5</b:Count>') }
60
+ it { expect(subject.to_xml).to include('<b:PageNumber>2</b:PageNumber>') }
61
+ it { expect(subject.to_xml).to include('<b:ReturnTotalRecordCount>true</b:ReturnTotalRecordCount>') }
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,9 @@
1
+ RSpec::Matchers.define :match_xml do |expected|
2
+ match do |actual|
3
+ clean_xml(expected) == clean_xml(actual)
4
+ end
5
+
6
+ def clean_xml(str)
7
+ str.split("\n").map(&:strip).join('')
8
+ end
9
+ end
metadata CHANGED
@@ -1,35 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynamics_crm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe Heth
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-03-08 00:00:00.000000000 Z
11
+ date: 2017-01-22 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: curb
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0.8'
20
- - - "<"
21
- - !ruby/object:Gem::Version
22
- version: 1.0.0
23
- type: :runtime
24
- prerelease: false
25
- version_requirements: !ruby/object:Gem::Requirement
26
- requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- version: '0.8'
30
- - - "<"
31
- - !ruby/object:Gem::Version
32
- version: 1.0.0
33
13
  - !ruby/object:Gem::Dependency
34
14
  name: mimemagic
35
15
  requirement: !ruby/object:Gem::Requirement
@@ -158,7 +138,7 @@ dependencies:
158
138
  - - "~>"
159
139
  - !ruby/object:Gem::Version
160
140
  version: 0.10.3
161
- description: Ruby API for integrating with MS Dynamics 2011/2013 SOAP API
141
+ description: Ruby API for integrating with MS Dynamics SOAP API
162
142
  email:
163
143
  - joeheth@gmail.com
164
144
  executables: []
@@ -181,6 +161,7 @@ files:
181
161
  - lib/dynamics_crm/fetch_xml/link_entity.rb
182
162
  - lib/dynamics_crm/metadata/attribute_metadata.rb
183
163
  - lib/dynamics_crm/metadata/attribute_query_expression.rb
164
+ - lib/dynamics_crm/metadata/double.rb
184
165
  - lib/dynamics_crm/metadata/entity_metadata.rb
185
166
  - lib/dynamics_crm/metadata/entity_query_expression.rb
186
167
  - lib/dynamics_crm/metadata/filter_expression.rb
@@ -202,6 +183,7 @@ files:
202
183
  - lib/dynamics_crm/version.rb
203
184
  - lib/dynamics_crm/xml/attributes.rb
204
185
  - lib/dynamics_crm/xml/column_set.rb
186
+ - lib/dynamics_crm/xml/condition_expression.rb
205
187
  - lib/dynamics_crm/xml/criteria.rb
206
188
  - lib/dynamics_crm/xml/entity.rb
207
189
  - lib/dynamics_crm/xml/entity_collection.rb
@@ -213,11 +195,12 @@ files:
213
195
  - lib/dynamics_crm/xml/money.rb
214
196
  - lib/dynamics_crm/xml/orders.rb
215
197
  - lib/dynamics_crm/xml/page_info.rb
216
- - lib/dynamics_crm/xml/query.rb
198
+ - lib/dynamics_crm/xml/query_expression.rb
217
199
  - spec/fixtures/associate_response.xml
218
200
  - spec/fixtures/create_response.xml
219
201
  - spec/fixtures/delete_response.xml
220
202
  - spec/fixtures/disassociate_response.xml
203
+ - spec/fixtures/entity_array_response.xml
221
204
  - spec/fixtures/fetch_xml_response.xml
222
205
  - spec/fixtures/lose_opportunity_response.xml
223
206
  - spec/fixtures/receiver_fault.xml
@@ -248,18 +231,20 @@ files:
248
231
  - spec/lib/metadata/retrieve_metadata_changes_response_spec.rb
249
232
  - spec/lib/model/opportunity_spec.rb
250
233
  - spec/lib/response/execute_result_spec.rb
251
- - spec/lib/response/retrieve_multiple_spec.rb
234
+ - spec/lib/response/retrieve_multiple_result_spec.rb
252
235
  - spec/lib/response/retrieve_result_spec.rb
253
236
  - spec/lib/xml/attributes_spec.rb
254
237
  - spec/lib/xml/column_set_spec.rb
238
+ - spec/lib/xml/criteria_spec.rb
255
239
  - spec/lib/xml/entity_reference_spec.rb
256
240
  - spec/lib/xml/entity_spec.rb
257
241
  - spec/lib/xml/fault_spec.rb
258
242
  - spec/lib/xml/message_builder_spec.rb
259
243
  - spec/lib/xml/money_spec.rb
260
- - spec/lib/xml/query_spec.rb
244
+ - spec/lib/xml/query_expression_spec.rb
261
245
  - spec/spec_helper.rb
262
246
  - spec/support/fixture_helpers.rb
247
+ - spec/support/matchers/match_xml.rb
263
248
  homepage: https://github.com/TinderBox/dynamics_crm
264
249
  licenses:
265
250
  - MIT
@@ -280,15 +265,16 @@ required_rubygems_version: !ruby/object:Gem::Requirement
280
265
  version: '0'
281
266
  requirements: []
282
267
  rubyforge_project:
283
- rubygems_version: 2.4.8
268
+ rubygems_version: 2.5.1
284
269
  signing_key:
285
270
  specification_version: 4
286
- summary: Ruby gem for integrating with MS Dynamics 2011/2013 SOAP API
271
+ summary: Ruby gem for integrating with MS Dynamics SOAP API
287
272
  test_files:
288
273
  - spec/fixtures/associate_response.xml
289
274
  - spec/fixtures/create_response.xml
290
275
  - spec/fixtures/delete_response.xml
291
276
  - spec/fixtures/disassociate_response.xml
277
+ - spec/fixtures/entity_array_response.xml
292
278
  - spec/fixtures/fetch_xml_response.xml
293
279
  - spec/fixtures/lose_opportunity_response.xml
294
280
  - spec/fixtures/receiver_fault.xml
@@ -319,15 +305,17 @@ test_files:
319
305
  - spec/lib/metadata/retrieve_metadata_changes_response_spec.rb
320
306
  - spec/lib/model/opportunity_spec.rb
321
307
  - spec/lib/response/execute_result_spec.rb
322
- - spec/lib/response/retrieve_multiple_spec.rb
308
+ - spec/lib/response/retrieve_multiple_result_spec.rb
323
309
  - spec/lib/response/retrieve_result_spec.rb
324
310
  - spec/lib/xml/attributes_spec.rb
325
311
  - spec/lib/xml/column_set_spec.rb
312
+ - spec/lib/xml/criteria_spec.rb
326
313
  - spec/lib/xml/entity_reference_spec.rb
327
314
  - spec/lib/xml/entity_spec.rb
328
315
  - spec/lib/xml/fault_spec.rb
329
316
  - spec/lib/xml/message_builder_spec.rb
330
317
  - spec/lib/xml/money_spec.rb
331
- - spec/lib/xml/query_spec.rb
318
+ - spec/lib/xml/query_expression_spec.rb
332
319
  - spec/spec_helper.rb
333
320
  - spec/support/fixture_helpers.rb
321
+ - spec/support/matchers/match_xml.rb
@@ -1,38 +0,0 @@
1
- module DynamicsCRM
2
- module XML
3
- # Represents Query XML fragment.
4
- class Query
5
-
6
- attr_accessor :columns, :criteria, :entity_name
7
-
8
- def initialize(entity_name)
9
- @entity_name = entity_name
10
- end
11
-
12
- def to_xml(options={})
13
- namespace = options[:namespace] ? options[:namespace] : "b"
14
-
15
- column_set = ColumnSet.new(columns)
16
- @criteria ||= Criteria.new
17
-
18
- xml = %Q{
19
- #{column_set.to_xml(namespace: namespace, camel_case: true)}
20
- #{@criteria.to_xml(namespace: namespace)}
21
- <#{namespace}:Distinct>false</#{namespace}:Distinct>
22
- <#{namespace}:EntityName>#{entity_name}</#{namespace}:EntityName>
23
- <#{namespace}:LinkEntities />
24
- <#{namespace}:Orders />
25
- }
26
-
27
- if options[:exclude_root].nil?
28
- xml = %Q{<query i:type="b:QueryExpression" xmlns:b="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
29
- #{xml}
30
- </query>}
31
- end
32
- return xml
33
- end
34
-
35
- end
36
- # Query
37
- end
38
- end
@@ -1,43 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe DynamicsCRM::XML::Query do
4
-
5
- describe 'initialization' do
6
- subject {
7
- DynamicsCRM::XML::Query.new('opportunity')
8
- }
9
-
10
- context "generate empty Query fragment" do
11
- it { expect(subject.to_xml).to include("<b:ColumnSet ") }
12
- it { expect(subject.to_xml).to match(/<b:Conditions>\s+<\/b:Conditions>/) }
13
- it { expect(subject.to_xml).to include("<b:AllColumns>true</b:AllColumns>") }
14
- it { expect(subject.to_xml).to include("<b:Distinct>false</b:Distinct>") }
15
- it { expect(subject.to_xml).to include("<b:EntityName>opportunity</b:EntityName>") }
16
- it { expect(subject.to_xml).to include("<b:FilterOperator>And</b:FilterOperator>") }
17
- end
18
-
19
- end
20
-
21
-
22
- describe 'criteria' do
23
- subject {
24
- query = DynamicsCRM::XML::Query.new('opportunity')
25
- query.criteria = DynamicsCRM::XML::Criteria.new([["name", "Equal", "Test Opp"]])
26
- query
27
- }
28
-
29
- context "generate empty Query fragment" do
30
- it { expect(subject.to_xml).to include("<b:ColumnSet ") }
31
- it { expect(subject.to_xml).to include("<b:ConditionExpression") }
32
- it { expect(subject.to_xml).to include("AttributeName>name</") }
33
- it { expect(subject.to_xml).to include("Operator>Equal</") }
34
- it { expect(subject.to_xml).to include('<d:anyType i:type="s:string" xmlns:s="http://www.w3.org/2001/XMLSchema">Test Opp</d:anyType>') }
35
- it { expect(subject.to_xml).to include("<b:AllColumns>true</b:AllColumns>") }
36
- it { expect(subject.to_xml).to include("<b:Distinct>false</b:Distinct>") }
37
- it { expect(subject.to_xml).to include("<b:EntityName>opportunity</b:EntityName>") }
38
- it { expect(subject.to_xml).to include("<b:FilterOperator>And</b:FilterOperator>") }
39
- end
40
-
41
- end
42
-
43
- end