taskmapper-pivotal 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ gem "taskmapper", "~> 0.8"
7
+ # Add dependencies to develop your gem here.
8
+ # Include everything needed to run rake, tests, features, etc.
9
+ group :development do
10
+ gem "rspec", "~> 2.8"
11
+ gem "jeweler", "~> 1.6"
12
+ gem "simplecov", "~> 0.5", :platforms => :ruby_19
13
+ gem "rcov", "~> 1.0", :platforms => :ruby_18
14
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,50 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activemodel (3.2.3)
5
+ activesupport (= 3.2.3)
6
+ builder (~> 3.0.0)
7
+ activeresource (3.2.3)
8
+ activemodel (= 3.2.3)
9
+ activesupport (= 3.2.3)
10
+ activesupport (3.2.3)
11
+ i18n (~> 0.6)
12
+ multi_json (~> 1.0)
13
+ builder (3.0.0)
14
+ diff-lcs (1.1.3)
15
+ git (1.2.5)
16
+ hashie (1.2.0)
17
+ i18n (0.6.0)
18
+ jeweler (1.6.4)
19
+ bundler (~> 1.0)
20
+ git (>= 1.2.5)
21
+ rake
22
+ multi_json (1.0.4)
23
+ rake (0.9.2.2)
24
+ rcov (1.0.0)
25
+ rspec (2.8.0)
26
+ rspec-core (~> 2.8.0)
27
+ rspec-expectations (~> 2.8.0)
28
+ rspec-mocks (~> 2.8.0)
29
+ rspec-core (2.8.0)
30
+ rspec-expectations (2.8.0)
31
+ diff-lcs (~> 1.1.2)
32
+ rspec-mocks (2.8.0)
33
+ simplecov (0.5.4)
34
+ multi_json (~> 1.0.3)
35
+ simplecov-html (~> 0.5.3)
36
+ simplecov-html (0.5.3)
37
+ taskmapper (0.8.0)
38
+ activeresource (~> 3.0)
39
+ activesupport (~> 3.0)
40
+ hashie (~> 1.2)
41
+
42
+ PLATFORMS
43
+ ruby
44
+
45
+ DEPENDENCIES
46
+ jeweler (~> 1.6)
47
+ rcov (~> 1.0)
48
+ rspec (~> 2.8)
49
+ simplecov (~> 0.5)
50
+ taskmapper (~> 0.8)
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2009-2010 The Hybrid Group
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ NONE
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,17 @@
1
+ # taskmapper-pivotal
2
+
3
+ Description goes here.
4
+
5
+ ## Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ ## Copyright
16
+
17
+ Copyright (c) 2010 The Hybrid Group. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,41 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "taskmapper-pivotal"
8
+ gem.summary = %Q{This is a taskmapper provider for interacting with Pivotal Tracker}
9
+ gem.description = %Q{This is a taskmapper provider for interacting with Pivotal Tracker .}
10
+ gem.email = "hong.quach@abigfisch.com"
11
+ gem.homepage = "http://ticket.rb"
12
+ gem.authors = ["HybridGroup"]
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
18
+ end
19
+
20
+ require 'rspec/core'
21
+ require 'rspec/core/rake_task'
22
+ RSpec::Core::RakeTask.new(:spec) do |spec|
23
+ spec.pattern = FileList['spec/**/*_spec.rb']
24
+ end
25
+
26
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
27
+ spec.pattern = 'spec/**/*_spec.rb'
28
+ spec.rcov = true
29
+ end
30
+
31
+ task :default => :spec
32
+
33
+ require 'rake/rdoctask'
34
+ Rake::RDocTask.new do |rdoc|
35
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
36
+
37
+ rdoc.rdoc_dir = 'rdoc'
38
+ rdoc.title = "taskmapper-kanbanpad#{version}"
39
+ rdoc.rdoc_files.include('README*')
40
+ rdoc.rdoc_files.include('lib/**/*.rb')
41
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.8.0
@@ -0,0 +1,74 @@
1
+ require 'rubygems'
2
+ require 'active_support'
3
+ require 'active_resource'
4
+
5
+ module PivotalAPI
6
+ class Error < StandardError; end
7
+ class << self
8
+ # Sets up basic authentication credentials for all the resources.
9
+ def authenticate(user, password)
10
+ Token.user = user
11
+ Token.password = password
12
+ self.token = Token.get(:active)['guid']
13
+ Token.user = nil
14
+ Token.password = nil
15
+ end
16
+
17
+ # Sets the API token for all the resources.
18
+ def token=(value)
19
+ resources.each do |klass|
20
+ klass.headers['X-TrackerToken'] = value
21
+ end
22
+ @token = value
23
+ end
24
+
25
+ def resources
26
+ @resources ||= []
27
+ end
28
+ end
29
+
30
+ class Base < ActiveResource::Base
31
+ self.site = 'https://www.pivotaltracker.com/services/v3/'
32
+ self.format = ActiveResource::Formats::XmlFormat
33
+ def self.inherited(base)
34
+ PivotalAPI.resources << base
35
+ super
36
+ end
37
+ end
38
+
39
+ class Project < Base
40
+ def stories(options = {})
41
+ Story.find(:all, :params => options.merge!(:project_id => self.id))
42
+ end
43
+ end
44
+
45
+ class Token < Base
46
+ end
47
+
48
+ class Activity < Base
49
+ self.site += 'projects/:project_id/'
50
+ end
51
+
52
+ class Membership < Base
53
+ self.site += 'projects/:project_id/'
54
+ end
55
+
56
+ class Iteration < Base
57
+ self.site += 'projects/:project_id/'
58
+ end
59
+
60
+ class Story < Base
61
+ self.site += 'projects/:project_id/'
62
+ end
63
+
64
+ class Note < Base
65
+ self.site += 'projects/:project_id/stories/:story_id/'
66
+ end
67
+
68
+ class Task < Base
69
+ self.site += 'projects/:project_id/stories/:story_id/'
70
+ end
71
+
72
+ class AllActivity < Base
73
+ end
74
+ end
@@ -0,0 +1,70 @@
1
+ module TaskMapper::Provider
2
+ module Pivotal
3
+ # The comment class for taskmapper-pivotal
4
+ # * author
5
+ # * body => text
6
+ # * id => position in the versions array (set by the initializer)
7
+ # * created_at => noted_at
8
+ # * updated_at => noted_at
9
+ # * ticket_id (actually the story id)
10
+ # * project_id
11
+ class Comment < TaskMapper::Provider::Base::Comment
12
+ API = PivotalAPI::Note
13
+
14
+ # A custom find_by_id
15
+ # The "comment" id is it's index in the versions array. An id of 0 therefore exists and
16
+ # should be the first ticket (original)
17
+ def self.find_by_id(project_id, ticket_id, id)
18
+ self.new(project_id, ticket_id, PivotalAPI::Note.find(id, :params => {:project_id => project_id, :story_id => ticket_id}))
19
+ end
20
+
21
+ # A custom find_by_attributes
22
+ #
23
+ def self.find_by_attributes(project_id, ticket_id, attributes = {})
24
+ self.search(project_id, ticket_id, attributes).collect { |comment| self.new(project_id, ticket_id, comment) }
25
+ end
26
+
27
+ # A custom searcher
28
+ #
29
+ # It returns a custom result because we need the original story to make a comment.
30
+ def self.search(project_id, ticket_id, options = {}, limit = 1000)
31
+ comments = PivotalAPI::Note.find(:all, :params => {:project_id => project_id, :story_id => ticket_id})
32
+ search_by_attribute(comments, options, limit)
33
+ end
34
+
35
+ # A custom creator
36
+ # We didn't really need to do much other than change the :ticket_id attribute to :story_id
37
+ def self.create(project_id, ticket_id, *options)
38
+ first = options.first
39
+ first[:story_id] ||= ticket_id
40
+ first[:project_id] ||= project_id
41
+ first[:text] ||= first.delete(:body) || first.delete('body')
42
+ note = PivotalAPI::Note.new(first)
43
+ note.save
44
+ self.new(project_id, ticket_id, note)
45
+ end
46
+
47
+ def initialize(project_id, ticket_id, *object)
48
+ if object.first
49
+ object = object.first
50
+ unless object.is_a? Hash
51
+ hash = {:id => object.id,
52
+ :body => object.text,
53
+ :update_at => object.noted_at,
54
+ :created_at => object.noted_at,
55
+ :project_id => project_id,
56
+ :ticket_id => ticket_id
57
+ }
58
+ else
59
+ hash = object
60
+ end
61
+ super hash
62
+ end
63
+ end
64
+
65
+ def body=(bod)
66
+ self.text = bod
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,37 @@
1
+ module TaskMapper::Provider
2
+ # This is the Pivotal Tracker Provider for taskmapper
3
+ module Pivotal
4
+ include TaskMapper::Provider::Base
5
+ TICKET_API = PivotalAPI::Story
6
+ PROJECT_API = PivotalAPI::Project
7
+
8
+ # This is for cases when you want to instantiate using TaskMapper::Provider::Lighthouse.new(auth)
9
+ def self.new(auth = {})
10
+ TaskMapper.new(:pivotal, auth)
11
+ end
12
+
13
+ # The authorize and initializer for this provider
14
+ def authorize(auth = {})
15
+ @authentication ||= TaskMapper::Authenticator.new(auth)
16
+ auth = @authentication
17
+ if auth.token.empty?
18
+ raise "You should pass a token for authentication"
19
+ end
20
+ if auth.token
21
+ PivotalAPI.token = auth.token
22
+ elsif auth.username && auth.password
23
+ PivotalAPI.authenticate(auth.username, auth.password)
24
+ end
25
+ end
26
+
27
+ def valid?
28
+ begin
29
+ PROJECT_API.find(:first)
30
+ true
31
+ rescue
32
+ false
33
+ end
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,55 @@
1
+ module TaskMapper::Provider
2
+ module Pivotal
3
+ # Project class for taskmapper-pivotal
4
+ #
5
+ #
6
+ class Project < TaskMapper::Provider::Base::Project
7
+ API = PivotalAPI::Project
8
+ # The finder method
9
+ #
10
+ # It accepts all the find functionalities defined by taskmapper
11
+ #
12
+ # + find() and find(:all) - Returns all projects on the account
13
+ # + find(<project_id>) - Returns the project based on the id
14
+ # + find(:first, :name => <project_name>) - Returns the first project based on the attribute
15
+ # + find(:name => <project name>) - Returns all projects based on the attribute
16
+ attr_accessor :prefix_options
17
+ alias_method :stories, :tickets
18
+ alias_method :story, :ticket
19
+
20
+ # Save this project
21
+ def save
22
+ warn 'Warning: Pivotal does not allow editing of project attributes. This method does nothing.'
23
+ true
24
+ end
25
+
26
+ def initialize(*options)
27
+ super(*options)
28
+ self.id = self.id.to_i
29
+ end
30
+
31
+ # Delete this project
32
+ def destroy
33
+ result = self.system_data[:client].destroy
34
+ result.is_a?(Net::HTTPOK)
35
+ end
36
+
37
+ def ticket!(*options)
38
+ options.first.merge!(:project_id => self.id)
39
+ Ticket.create(options.first)
40
+ end
41
+
42
+ # copy from
43
+ def copy(project)
44
+ project.tickets.each do |ticket|
45
+ copy_ticket = self.ticket!(:name => ticket.title, :description => ticket.description)
46
+ ticket.comments.each do |comment|
47
+ copy_ticket.comment!(:text => comment.body)
48
+ sleep 1
49
+ end
50
+ end
51
+ end
52
+
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,124 @@
1
+ module TaskMapper::Provider
2
+ module Pivotal
3
+ # Ticket class for taskmapper-pivotal
4
+ # * id
5
+ # * status
6
+ # * priority
7
+ # * title => name
8
+ # * resolution
9
+ # * created_at
10
+ # * updated_at
11
+ # * description => text
12
+ # * assignee
13
+ # * requestor
14
+ # * project_id (prefix_options[:project_id])
15
+ class Ticket < TaskMapper::Provider::Base::Ticket
16
+ @@allowed_states = ['new', 'open', 'resolved', 'hold', 'invalid']
17
+
18
+ attr_accessor :prefix_options
19
+ API = PivotalAPI::Story
20
+
21
+
22
+ # The saver
23
+ def save(*options)
24
+ pt_ticket = @system_data[:client]
25
+ self.keys.each do |key|
26
+ pt_ticket.send(key + '=', self.send(key)) if self.send(key) != pt_ticket.send(key)
27
+ end
28
+ pt_ticket.save
29
+ end
30
+
31
+ def destroy(*options)
32
+ @system_data[:client].destroy.is_a?(Net::HTTPOK)
33
+ end
34
+
35
+ def project_id
36
+ self.prefix_options[:project_id]
37
+ end
38
+
39
+ def requestor
40
+ self.requested_by
41
+ end
42
+
43
+ def title
44
+ self.name
45
+ end
46
+
47
+ def title=(title)
48
+ self.name=title
49
+ end
50
+
51
+ def status
52
+ self.current_state
53
+ end
54
+
55
+ def priority
56
+ self.estimate
57
+ end
58
+
59
+ def resolution
60
+ self.current_state
61
+ end
62
+
63
+ def assignee
64
+ self.owned_by
65
+ end
66
+
67
+ def comment!(*options)
68
+ Comment.create(self.project_id, self.id, options.first)
69
+ end
70
+ # The closer
71
+ def close(resolution = 'resolved')
72
+ resolution = 'resolved' unless @@allowed_states.include?(resolution)
73
+ ticket = PivotalAPI::Ticket.find(self.id, :params => {:project_id => self.prefix_options[:project_id]})
74
+ ticket.state = resolution
75
+ ticket.save
76
+ end
77
+
78
+ class << self
79
+
80
+ def find_by_attributes(project_id, attributes = {})
81
+ date_to_search = attributes[:updated_at] || attributes[:created_at]
82
+ tickets = []
83
+ unless date_to_search.nil?
84
+ tickets = search_by_datefields(project_id, date_to_search)
85
+ else
86
+ tickets += API.find(:all, :params => {:project_id => project_id, :filter => filter(attributes)}).map { |xticket| self.new xticket }
87
+ end
88
+ tickets.flatten
89
+ end
90
+
91
+ def filter(attributes = {})
92
+ filter = ""
93
+ attributes.each_pair do |key, value|
94
+ filter << "#{key}:#{value} "
95
+ end
96
+ filter.strip!
97
+ end
98
+
99
+ def create(options)
100
+ super translate options, {:title => :name,
101
+ :requestor => :requested_by,
102
+ :status => :current_state,
103
+ :estimate => :priority,
104
+ :assignee => :owned_by}
105
+ end
106
+
107
+ private
108
+ def search_by_datefields(project_id, date_to_search)
109
+ date_to_search = date_to_search.strftime("%Y/%m/%d")
110
+ tickets = []
111
+ PivotalAPI::Activity.find(:all, :params => {:project_id => project_id, :occurred_since_date => date_to_search}).each do |activity|
112
+ tickets = activity.stories.map { |xstory| self.new xstory }
113
+ end
114
+ tickets
115
+ end
116
+
117
+ def translate(hash, mapping)
118
+ Hash[hash.map { |k, v| [mapping[k] ||= k, v]}]
119
+ end
120
+ end
121
+ end
122
+
123
+ end
124
+ end
@@ -0,0 +1,6 @@
1
+ require File.dirname(__FILE__) + '/pivotal/pivotal-api'
2
+
3
+ %w{ pivotal ticket project comment }.each do |f|
4
+ require File.dirname(__FILE__) + '/provider/' + f + '.rb';
5
+ end
6
+
@@ -0,0 +1,67 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "TaskMapper::Provider::Pivotal::Comment" do
4
+ before(:all) do
5
+ headers = {'X-TrackerToken' => '000000'}
6
+ wheaders = headers.merge('Content-Type' => 'application/xml')
7
+ ActiveResource::HttpMock.respond_to do |mock|
8
+ mock.get '/services/v3/projects/93790.xml', headers, fixture_for('projects/93790'), 200
9
+ mock.get '/services/v3/projects/93790/stories.xml', headers, fixture_for('stories'), 200
10
+ mock.get '/services/v3/projects/93790/stories.xml?filter=', headers, fixture_for('stories'), 200
11
+ mock.get '/services/v3/projects/93790/stories/4056827.xml', headers, fixture_for('stories/4056827'), 200
12
+ mock.get '/services/v3/projects/93790/stories/4056827/notes.xml', headers, fixture_for('notes'), 200
13
+ mock.get '/services/v3/projects/93790/stories/4056827/notes/1946635.xml', headers, fixture_for('notes/1946635'), 200
14
+ mock.post '/services/v3/projects/93790/stories/4056827/notes.xml', wheaders, fixture_for('notes/1946635'), 200
15
+ mock.put '/services/v3/projects/93790/stories/4056827.xml', wheaders, '', 200
16
+ end
17
+ @project_id = 93790
18
+ @ticket_id = 4056827
19
+ @comment_id = 1946635
20
+ end
21
+
22
+ before(:each) do
23
+ @taskmapper = TaskMapper.new(:pivotal, :token => '000000')
24
+ @project = @taskmapper.project(@project_id)
25
+ @ticket = @project.ticket(4056827)
26
+ @klass = TaskMapper::Provider::Pivotal::Comment
27
+ end
28
+
29
+ it "should be able to load all comments" do
30
+ @comments = @ticket.comments
31
+ @comments.should be_an_instance_of(Array)
32
+ @comments.first.should be_an_instance_of(@klass)
33
+ end
34
+
35
+ it "should be able to load all comments based on 'id's" do
36
+ @comments = @ticket.comments([@comment_id])
37
+ @comments.should be_an_instance_of(Array)
38
+ @comments.first.should be_an_instance_of(@klass)
39
+ @comments.first.id.should == @comment_id
40
+ end
41
+
42
+ it "should be able to load all comments based on attributes" do
43
+ @comments = @ticket.comments(:id => @comment_id)
44
+ @comments.should be_an_instance_of(Array)
45
+ @comments.first.should be_an_instance_of(@klass)
46
+ end
47
+
48
+ it "should be able to load a comment based on id" do
49
+ @comment = @ticket.comment(@comment_id)
50
+ @comment.should be_an_instance_of(@klass)
51
+ @comment.id.should == @comment_id
52
+ end
53
+
54
+ it "should be able to load a comment based on attributes" do
55
+ @comment = @ticket.comment(:id => @comment_id)
56
+ @comment.should be_an_instance_of(@klass)
57
+ end
58
+
59
+ it "should return the class" do
60
+ @ticket.comment.should == @klass
61
+ end
62
+
63
+ it "should be able to create a comment" do # which as mentioned before is technically a ticket update
64
+ @comment = @ticket.comment!(:body => 'hello there boys and girls')
65
+ @comment.should be_an_instance_of(@klass)
66
+ end
67
+ end
@@ -0,0 +1,39 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <activities type="array">
3
+ <activity>
4
+ <id type="integer">86811411</id>
5
+ <version type="integer">3</version>
6
+ <event_type>story_create</event_type>
7
+ <occurred_at type="datetime">2011/06/09 23:29:12 UTC</occurred_at>
8
+ <author>Clutch Test</author>
9
+ <project_id type="integer">136096</project_id>
10
+ <description>Clutch Test added &quot;Let's see with another story&quot;</description>
11
+ <stories type="array">
12
+ <story>
13
+ <id type="integer">14398445</id>
14
+ <url>http://www.pivotaltracker.com/services/v3/projects/136096/stories/14398445</url>
15
+ <name>Let's see with another story</name>
16
+ <story_type>feature</story_type>
17
+ <current_state>unscheduled</current_state>
18
+ </story>
19
+ </stories>
20
+ </activity>
21
+ <activity>
22
+ <id type="integer">86747595</id>
23
+ <version type="integer">2</version>
24
+ <event_type>story_create</event_type>
25
+ <occurred_at type="datetime">2011/06/09 20:06:46 UTC</occurred_at>
26
+ <author>Clutch Test</author>
27
+ <project_id type="integer">136096</project_id>
28
+ <description>Clutch Test added &quot;Hello, This is clutch&quot;</description>
29
+ <stories type="array">
30
+ <story>
31
+ <id type="integer">14389955</id>
32
+ <url>http://www.pivotaltracker.com/services/v3/projects/136096/stories/14389955</url>
33
+ <name>Hello, This is clutch</name>
34
+ <story_type>feature</story_type>
35
+ <current_state>unscheduled</current_state>
36
+ </story>
37
+ </stories>
38
+ </activity>
39
+ </activities>
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <note>
3
+ <id type="integer">1946635</id>
4
+ <text>note</text>
5
+ <author>Hong Quach</author>
6
+ <noted_at type="datetime">2010/07/03 08:09:38 UTC</noted_at>
7
+ </note>
@@ -0,0 +1,15 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <notes type="array">
3
+ <note>
4
+ <id type="integer">1946635</id>
5
+ <text>note</text>
6
+ <author>Hong Quach</author>
7
+ <noted_at type="datetime">2010/07/03 08:09:38 UTC</noted_at>
8
+ </note>
9
+ <note>
10
+ <id type="integer">1946719</id>
11
+ <text>etuhanoeth naou</text>
12
+ <author>Hong Quach</author>
13
+ <noted_at type="datetime">2010/07/03 09:57:03 UTC</noted_at>
14
+ </note>
15
+ </notes>