mingle_events 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|