mingle_events 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/LICENSE.txt +202 -0
- data/README.textile +117 -0
- data/lib/mingle_events.rb +26 -0
- data/lib/mingle_events/feed.rb +5 -0
- data/lib/mingle_events/feed/author.rb +28 -0
- data/lib/mingle_events/feed/category.rb +75 -0
- data/lib/mingle_events/feed/element_support.rb +19 -0
- data/lib/mingle_events/feed/entry.rb +96 -0
- data/lib/mingle_events/feed/page.rb +61 -0
- data/lib/mingle_events/http_error.rb +26 -0
- data/lib/mingle_events/mingle_basic_auth_access.rb +71 -0
- data/lib/mingle_events/mingle_oauth_access.rb +40 -0
- data/lib/mingle_events/poller.rb +32 -0
- data/lib/mingle_events/processors.rb +10 -0
- data/lib/mingle_events/processors/author_filter.rb +62 -0
- data/lib/mingle_events/processors/card_data.rb +102 -0
- data/lib/mingle_events/processors/card_type_filter.rb +28 -0
- data/lib/mingle_events/processors/category_filter.rb +19 -0
- data/lib/mingle_events/processors/custom_property_filter.rb +30 -0
- data/lib/mingle_events/processors/http_post_publisher.rb +20 -0
- data/lib/mingle_events/processors/pipeline.rb +20 -0
- data/lib/mingle_events/processors/puts_publisher.rb +17 -0
- data/lib/mingle_events/project_custom_properties.rb +33 -0
- data/lib/mingle_events/project_event_fetcher.rb +96 -0
- data/test/mingle_events/feed/author_test.rb +39 -0
- data/test/mingle_events/feed/category_test.rb +20 -0
- data/test/mingle_events/feed/entry_test.rb +140 -0
- data/test/mingle_events/feed/page_test.rb +82 -0
- data/test/mingle_events/poller_test.rb +47 -0
- data/test/mingle_events/processors/author_filter_test.rb +80 -0
- data/test/mingle_events/processors/card_data_test.rb +210 -0
- data/test/mingle_events/processors/card_type_filter_test.rb +51 -0
- data/test/mingle_events/processors/category_filter_test.rb +27 -0
- data/test/mingle_events/processors/custom_property_filter_test.rb +51 -0
- data/test/mingle_events/processors/pipeline_test.rb +32 -0
- data/test/mingle_events/project_custom_properties_test.rb +39 -0
- data/test/mingle_events/project_event_fetcher_test.rb +122 -0
- data/test/test_helper.rb +163 -0
- data/test/web_hook_server/web_hook_server.rb +6 -0
- metadata +140 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
module MingleEvents
|
2
|
+
module Feed
|
3
|
+
|
4
|
+
# Provides some helpers for pulling values from Nokogiri Elems
|
5
|
+
module ElementSupport
|
6
|
+
|
7
|
+
def element_text(parent_element, element_name, optional = false)
|
8
|
+
element = parent_element.at(".//#{element_name}")
|
9
|
+
if optional && element.nil?
|
10
|
+
nil
|
11
|
+
else
|
12
|
+
element.inner_text
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module MingleEvents
|
2
|
+
module Feed
|
3
|
+
|
4
|
+
# A Ruby wrapper around an Atom entry, particularly an Atom entry
|
5
|
+
# representing an event in Mingle.
|
6
|
+
class Entry
|
7
|
+
|
8
|
+
# Construct with the wrapped Nokogiri Elem for the entry
|
9
|
+
def initialize(entry_element)
|
10
|
+
@entry_element = entry_element
|
11
|
+
end
|
12
|
+
|
13
|
+
# The raw entry XML from the Atom feed
|
14
|
+
def raw_xml
|
15
|
+
@raw_xml ||= @entry_element.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
# The Atom entry's id value. This is the one true identifier for the entry,
|
19
|
+
# and therefore the event.
|
20
|
+
def entry_id
|
21
|
+
@entry_id ||= @entry_element.at('id').inner_text
|
22
|
+
end
|
23
|
+
alias :event_id :entry_id
|
24
|
+
|
25
|
+
# The Atom entry's title
|
26
|
+
def title
|
27
|
+
@title ||= @entry_element.at('title').inner_text
|
28
|
+
end
|
29
|
+
|
30
|
+
# The time at which entry was created, i.e., the event was triggered
|
31
|
+
def updated
|
32
|
+
@updated ||= Time.parse(@entry_element.at('updated').inner_text)
|
33
|
+
end
|
34
|
+
|
35
|
+
# The user who created the entry (triggered the event), i.e., changed project data in Mingle
|
36
|
+
def author
|
37
|
+
@author ||= Author.new(@entry_element.at('author'))
|
38
|
+
end
|
39
|
+
|
40
|
+
# The set of Atom categoies describing the entry
|
41
|
+
def categories
|
42
|
+
@categories ||= @entry_element.search('category').map do |category_element|
|
43
|
+
Category.new(category_element.attribute('term').text, category_element.attribute('scheme').text)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Whether the entry/event was sourced by a Mingle card
|
48
|
+
def card?
|
49
|
+
categories.any?{|c| c == Category::CARD}
|
50
|
+
end
|
51
|
+
|
52
|
+
# The number of the card that sourced this entry/event. If the entry is not a card event
|
53
|
+
# an error will be thrown. The source of this data is perhaps not so robust and we'll need
|
54
|
+
# to revisit this in the next release of Mingle.
|
55
|
+
def card_number
|
56
|
+
raise "You cannot get the card number for an event that is not sourced by a card!" unless card?
|
57
|
+
@card_number ||= parse_card_number
|
58
|
+
end
|
59
|
+
|
60
|
+
# The version number of the card or page that was created by this event. (For now, only
|
61
|
+
# working with cards.)
|
62
|
+
def version
|
63
|
+
@version ||= CGI.parse(URI.parse(card_version_resource_uri).query)['version'].first.to_i
|
64
|
+
end
|
65
|
+
|
66
|
+
# The resource URI for the card version that was created by this event. Throws error if not card event.
|
67
|
+
def card_version_resource_uri
|
68
|
+
raise "You cannot get card version data for an event that is not sourced by a card!" unless card?
|
69
|
+
@card_version_resource_uri ||= parse_card_version_resource_uri
|
70
|
+
end
|
71
|
+
|
72
|
+
def to_s
|
73
|
+
"Entry[entry_id=#{entry_id}, updated=#{updated}]"
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def parse_card_number
|
79
|
+
card_number_element = @entry_element.at(
|
80
|
+
"link[@rel='http://www.thoughtworks-studios.com/ns/mingle#event-source'][@type='application/vnd.mingle+xml']"
|
81
|
+
)
|
82
|
+
# TODO: improve this bit of parsing :)
|
83
|
+
card_number_element.attribute('href').text.split('/').last.split('.')[0..-2].join.to_i
|
84
|
+
end
|
85
|
+
|
86
|
+
def parse_card_version_resource_uri
|
87
|
+
card_number_element = @entry_element.at(
|
88
|
+
"link[@rel='http://www.thoughtworks-studios.com/ns/mingle#version'][@type='application/vnd.mingle+xml']"
|
89
|
+
)
|
90
|
+
card_number_element.attribute('href').text
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module MingleEvents
|
2
|
+
module Feed
|
3
|
+
|
4
|
+
# A page of Atom events. I.e., *not* a Mingle page. Most users of this library
|
5
|
+
# will not use this class to access the Mingle Atom feed, but will use the
|
6
|
+
# ProjectFeed class which handles paging transparently.
|
7
|
+
class Page
|
8
|
+
|
9
|
+
attr_accessor :url
|
10
|
+
|
11
|
+
def initialize(url, mingle_access)
|
12
|
+
@url = url
|
13
|
+
@mingle_access = mingle_access
|
14
|
+
end
|
15
|
+
|
16
|
+
def entries
|
17
|
+
@entries ||= page_as_document.search('feed/entry').map do |entry_element|
|
18
|
+
Entry.new(entry_element)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def next
|
23
|
+
next_url_element = page_as_document.at("feed/link[@rel='next']")
|
24
|
+
if next_url_element.nil?
|
25
|
+
nil
|
26
|
+
else
|
27
|
+
Page.new(next_url_element.attribute('href').text, @mingle_access)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def previous
|
32
|
+
previous_url_element = page_as_document.at("feed/link[@rel='previous']")
|
33
|
+
if previous_url_element.nil?
|
34
|
+
nil
|
35
|
+
else
|
36
|
+
Page.new(previous_url_element.attribute('href').text, @mingle_access)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def archived?
|
41
|
+
URI.parse(url).query && url == page_as_document.at("feed/link[@rel='self']").attribute('href').text
|
42
|
+
end
|
43
|
+
|
44
|
+
def closest_archived_page
|
45
|
+
if archived?
|
46
|
+
self
|
47
|
+
else
|
48
|
+
self.next
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def page_as_document
|
55
|
+
@page_as_document ||= Nokogiri::XML(@mingle_access.fetch_page(@url))
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module MingleEvents
|
2
|
+
|
3
|
+
class HttpError < StandardError
|
4
|
+
|
5
|
+
attr_reader :response, :requested_location, :additional_context
|
6
|
+
|
7
|
+
def initialize(response, requested_location, additional_context = nil)
|
8
|
+
super(%{
|
9
|
+
Unable to retrieve 200 response from URI: <#{requested_location}>!
|
10
|
+
HTTP Code: #{response.code}
|
11
|
+
Body: #{response.body}
|
12
|
+
#{additional_context.nil? ? "" : additional_context}
|
13
|
+
})
|
14
|
+
@response = response
|
15
|
+
@requested_location = requested_location
|
16
|
+
@additional_context = additional_context
|
17
|
+
end
|
18
|
+
|
19
|
+
def not_found?
|
20
|
+
# has to be a better way to do this!!
|
21
|
+
response.class == Net::HTTPNotFound
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module MingleEvents
|
2
|
+
|
3
|
+
# Supports fetching of Mingle resources using HTTP basic auth.
|
4
|
+
# Please only use this class to access resources over HTTPS so
|
5
|
+
# as not to send credentials over plain-text connections.
|
6
|
+
class MingleBasicAuthAccess
|
7
|
+
|
8
|
+
attr_reader :base_url
|
9
|
+
|
10
|
+
BASIC_AUTH_HTTP_WARNING = %{
|
11
|
+
WARNING!!!
|
12
|
+
It looks like you are using basic authentication over a plain-text HTTP connection.
|
13
|
+
We HIGHLY recommend AGAINST this practice. You should only use basic authentication over
|
14
|
+
a secure HTTPS connection. Instructions for enabling HTTPS/SSL in Mingle can be found at
|
15
|
+
<http://www.thoughtworks-studios.com/mingle/3.3/help/advanced_mingle_configuration.html>
|
16
|
+
WARNING!!
|
17
|
+
}
|
18
|
+
|
19
|
+
def initialize(base_url, username, password)
|
20
|
+
@base_url = base_url
|
21
|
+
@username = username
|
22
|
+
@password = password
|
23
|
+
end
|
24
|
+
|
25
|
+
# Fetch the content at location via HTTP. Throws error if non-200 response.
|
26
|
+
def fetch_page(location)
|
27
|
+
MingleEvents.log.info("About to fetch #{location}...")
|
28
|
+
rsp = fetch_page_response(location)
|
29
|
+
case rsp
|
30
|
+
when Net::HTTPSuccess
|
31
|
+
rsp.body
|
32
|
+
when Net::HTTPUnauthorized
|
33
|
+
raise HttpError.new(rsp, location, %{
|
34
|
+
If you think you are passing correct credentials, please check
|
35
|
+
that you have enabled Mingle for basic authentication.
|
36
|
+
See <http://www.thoughtworks-studios.com/mingle/3.3/help/configuring_mingle_authentication.html>.})
|
37
|
+
else
|
38
|
+
raise HttpError.new(rsp, location)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def fetch_page_response(location)
|
45
|
+
location = @base_url + location if location[0..0] == '/'
|
46
|
+
|
47
|
+
uri = URI.parse(location)
|
48
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
49
|
+
|
50
|
+
if uri.scheme == 'https'
|
51
|
+
http.use_ssl = true
|
52
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
53
|
+
else
|
54
|
+
puts BASIC_AUTH_HTTP_WARNING
|
55
|
+
end
|
56
|
+
|
57
|
+
path = uri.path
|
58
|
+
path += "?#{uri.query}" if uri.query
|
59
|
+
puts "Fetching page at #{path}..."
|
60
|
+
|
61
|
+
start = Time.now
|
62
|
+
req = Net::HTTP::Get.new(path)
|
63
|
+
req.basic_auth(@username, @password)
|
64
|
+
rsp = http.request(req)
|
65
|
+
puts "...#{Time.now - start}"
|
66
|
+
|
67
|
+
rsp
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module MingleEvents
|
2
|
+
|
3
|
+
# Client for Mingle's experimental OAuth 2.0 support in 3.0
|
4
|
+
#--
|
5
|
+
# TODO: Update error handling and support of fetching response
|
6
|
+
# objects to that of MingleBasicAuthAccess
|
7
|
+
class MingleOauthAccess
|
8
|
+
|
9
|
+
def initialize(base_url, token)
|
10
|
+
@base_url = base_url
|
11
|
+
@token = token
|
12
|
+
end
|
13
|
+
|
14
|
+
def fetch_page(location)
|
15
|
+
location = @base_url + location if location[0..0] == '/'
|
16
|
+
|
17
|
+
uri = URI.parse(location)
|
18
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
19
|
+
http.use_ssl = true
|
20
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
21
|
+
headers = {
|
22
|
+
'Authorization' => %{Token token="#{@token}"}
|
23
|
+
}
|
24
|
+
|
25
|
+
path = uri.path
|
26
|
+
path += "?#{uri.query}" if uri.query
|
27
|
+
puts "Fetching page at #{path}..."
|
28
|
+
|
29
|
+
start = Time.now
|
30
|
+
response = http.get(path, headers)
|
31
|
+
puts "...#{Time.now - start}"
|
32
|
+
|
33
|
+
# todo: what's the right way to raise on non 200 ?
|
34
|
+
# raise StandardError.new(response.body) unless response == Net::HTTPSuccess
|
35
|
+
|
36
|
+
response.body
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module MingleEvents
|
2
|
+
class Poller
|
3
|
+
|
4
|
+
# Manages a full sweep of event processing across each processing pipeline
|
5
|
+
# configured for specified mingle projects. processors_by_project_identifier should
|
6
|
+
# be a hash where the keys are mingle project identifiers and the values are
|
7
|
+
# lists of event processors.
|
8
|
+
def initialize(mingle_access, processors_by_project_identifier)
|
9
|
+
@mingle_access = mingle_access
|
10
|
+
@processors_by_project_identifier = processors_by_project_identifier
|
11
|
+
end
|
12
|
+
|
13
|
+
# Run a single poll for each project configured with processor(s) and
|
14
|
+
# broadcast each event to each processor.
|
15
|
+
def run_once(options={})
|
16
|
+
MingleEvents.log.info("MingleEvents::Poller about to poll once...")
|
17
|
+
@processors_by_project_identifier.each do |project_identifier, processors|
|
18
|
+
fetcher = ProjectEventFetcher.new(project_identifier, @mingle_access)
|
19
|
+
fetcher.reset if options[:clean]
|
20
|
+
info_file_for_new_event = fetcher.fetch_latest
|
21
|
+
while info_file_for_new_event
|
22
|
+
entry_info = YAML.load(File.new(info_file_for_new_event))
|
23
|
+
entry = Feed::Entry.new(Nokogiri::XML(entry_info[:entry_xml]).at('/entry'))
|
24
|
+
MingleEvents.log.info("About to process event #{entry.entry_id}...")
|
25
|
+
processors.each{|p| p.process_events([entry])}
|
26
|
+
info_file_for_new_event = entry_info[:next_entry_file_path]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'processors', 'card_data'))
|
4
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'processors', 'category_filter'))
|
5
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'processors', 'card_type_filter'))
|
6
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'processors', 'http_post_publisher'))
|
7
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'processors', 'pipeline'))
|
8
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'processors', 'puts_publisher'))
|
9
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'processors', 'author_filter'))
|
10
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'processors', 'custom_property_filter'))
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module MingleEvents
|
2
|
+
module Processors
|
3
|
+
|
4
|
+
# Removes all events from stream not triggered by the specified author
|
5
|
+
class AuthorFilter
|
6
|
+
|
7
|
+
def initialize(spec, mingle_access, project_identifier)
|
8
|
+
unless spec.size == 1
|
9
|
+
raise "Author spec must contain 1 and only 1 piece of criteria (the only legal criteria are each unique identifiers in and of themselves so multiple criteria is not needed.)"
|
10
|
+
end
|
11
|
+
|
12
|
+
@author_spec = AuthorSpec.new(spec, mingle_access, project_identifier)
|
13
|
+
end
|
14
|
+
|
15
|
+
def process_events(events)
|
16
|
+
events.select{|event| @author_spec.event_triggered_by?(event)}
|
17
|
+
end
|
18
|
+
|
19
|
+
class AuthorSpec
|
20
|
+
|
21
|
+
def initialize(spec, mingle_access, project_identifier)
|
22
|
+
@spec = spec
|
23
|
+
@mingle_access = mingle_access
|
24
|
+
@project_identifier = project_identifier
|
25
|
+
end
|
26
|
+
|
27
|
+
def event_triggered_by?(event)
|
28
|
+
event.author.uri == author_uri
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def author_uri
|
34
|
+
lookup_author_uri
|
35
|
+
end
|
36
|
+
|
37
|
+
def lookup_author_uri
|
38
|
+
team_resource = "/api/v2/projects/#{@project_identifier}/team.xml"
|
39
|
+
@raw_xml ||= @mingle_access.fetch_page(URI.escape(team_resource))
|
40
|
+
@doc ||= Nokogiri::XML(@raw_xml)
|
41
|
+
|
42
|
+
users = @doc.search('/projects_members/projects_member/user').map do |user|
|
43
|
+
{
|
44
|
+
:url => user.attribute('url').inner_text,
|
45
|
+
:login => user.at('login').inner_text,
|
46
|
+
:email => user.at('email').inner_text
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
spec_user = users.find do |user|
|
51
|
+
# is this too hacky?
|
52
|
+
user.merge(@spec) == user
|
53
|
+
end
|
54
|
+
|
55
|
+
spec_user.nil? ? nil : spec_user[:url]
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
module MingleEvents
|
2
|
+
module Processors
|
3
|
+
|
4
|
+
# Provides ability to lookup card data, e.g., card's type name, for any
|
5
|
+
# card serving as a source for the stream of events. Implements two interfaces:
|
6
|
+
# the standard event processing interface, handle_events, and also, for_card_event,
|
7
|
+
# which returns of has of card data for the given event. See the project README
|
8
|
+
# for additional information on using this class in a processing pipeline.
|
9
|
+
class CardData
|
10
|
+
|
11
|
+
def initialize(mingle_access, project_identifier, custom_properties = ProjectCustomProperties.new(mingle_access, project_identifier))
|
12
|
+
@mingle_access = mingle_access
|
13
|
+
@project_identifier = project_identifier
|
14
|
+
@custom_properties = custom_properties
|
15
|
+
@card_data_by_number_and_version = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
# Capture which events are card events and might require data lookup. The
|
19
|
+
# actual data retrieval is lazy and will only occur as needed.
|
20
|
+
def process_events(events)
|
21
|
+
@card_events = events.select(&:card?)
|
22
|
+
events
|
23
|
+
end
|
24
|
+
|
25
|
+
# Return a hash of data for the card that sourced the passed event. The data
|
26
|
+
# will be for the version of the card that was created by the event and not the
|
27
|
+
# current state of the card. Currently supported data keys are: :number, :version,
|
28
|
+
# :card_type_name
|
29
|
+
def for_card_event(card_event)
|
30
|
+
if @card_data_by_number_and_version.nil?
|
31
|
+
load_bulk_card_data
|
32
|
+
end
|
33
|
+
key = data_key(card_event.card_number, card_event.version)
|
34
|
+
@card_data_by_number_and_version[key] ||= load_card_data_for_event(card_event)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def data_key(number, version)
|
40
|
+
"#{number}:#{version}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def load_bulk_card_data
|
44
|
+
@card_data_by_number_and_version = {}
|
45
|
+
|
46
|
+
# TODO: figure out max number of card numbers before we have to chunk this up. or does mingle
|
47
|
+
# figure out how to handle a too-large IN clause?
|
48
|
+
card_numbers = @card_events.map(&:card_number).uniq
|
49
|
+
path = "/api/v2/projects/#{@project_identifier}/cards/execute_mql.xml?mql=WHERE number IN (#{card_numbers.join(',')})"
|
50
|
+
|
51
|
+
raw_xml = @mingle_access.fetch_page(URI.escape(path))
|
52
|
+
doc = Nokogiri::XML(raw_xml)
|
53
|
+
|
54
|
+
doc.search('/results/result').map do |card_result|
|
55
|
+
card_number = card_result.at('number').inner_text.to_i
|
56
|
+
card_version = card_result.at('version').inner_text.to_i
|
57
|
+
custom_properties = {}
|
58
|
+
@card_data_by_number_and_version[data_key(card_number, card_version)] = {
|
59
|
+
:number => card_number,
|
60
|
+
:version => card_version,
|
61
|
+
:card_type_name => card_result.at('card_type_name').inner_text,
|
62
|
+
:custom_properties => custom_properties
|
63
|
+
}
|
64
|
+
card_result.children.each do |child|
|
65
|
+
if child.name.index("cp_") == 0
|
66
|
+
custom_properties[@custom_properties.property_name_for_column(child.name)] =
|
67
|
+
nullable_value_from_element(child)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def load_card_data_for_event(card_event)
|
74
|
+
begin
|
75
|
+
page_xml = @mingle_access.fetch_page(card_event.card_version_resource_uri)
|
76
|
+
doc = Nokogiri::XML(page_xml)
|
77
|
+
custom_properties = {}
|
78
|
+
result = {
|
79
|
+
:number => card_event.card_number,
|
80
|
+
:version => card_event.version,
|
81
|
+
:card_type_name => doc.at('/card/card_type/name').inner_text,
|
82
|
+
:custom_properties => custom_properties
|
83
|
+
}
|
84
|
+
doc.search('/card/properties/property').each do |property|
|
85
|
+
custom_properties[property.at('name').inner_text] =
|
86
|
+
nullable_value_from_element(property.at('value'))
|
87
|
+
end
|
88
|
+
|
89
|
+
result
|
90
|
+
rescue HttpError => httpError
|
91
|
+
raise httpError unless httpError.not_found?
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def nullable_value_from_element(element)
|
97
|
+
element['nil'] == 'true' ? nil : element.inner_text
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|