lrd-pivotal-tracker 0.5.14
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.
- checksums.yaml +7 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +94 -0
- data/LICENSE +21 -0
- data/README.rdoc +82 -0
- data/Rakefile +42 -0
- data/VERSION +1 -0
- data/lib/pivotal-tracker.rb +49 -0
- data/lib/pivotal-tracker/activity.rb +68 -0
- data/lib/pivotal-tracker/attachment.rb +16 -0
- data/lib/pivotal-tracker/author.rb +16 -0
- data/lib/pivotal-tracker/client.rb +84 -0
- data/lib/pivotal-tracker/extensions.rb +11 -0
- data/lib/pivotal-tracker/iteration.rb +40 -0
- data/lib/pivotal-tracker/membership.rb +20 -0
- data/lib/pivotal-tracker/note.rb +58 -0
- data/lib/pivotal-tracker/project.rb +98 -0
- data/lib/pivotal-tracker/proxy.rb +64 -0
- data/lib/pivotal-tracker/story.rb +153 -0
- data/lib/pivotal-tracker/task.rb +71 -0
- data/lib/pivotal-tracker/validation.rb +69 -0
- data/lib/pivotal_tracker.rb +2 -0
- data/lrd-pivotal-tracker.gemspec +68 -0
- data/spec/fixtures/vcr_cassettes/default_vcr_cassette.yml +6936 -0
- data/spec/pivotal-tracker/activity_spec.rb +53 -0
- data/spec/pivotal-tracker/attachment_spec.rb +63 -0
- data/spec/pivotal-tracker/client_spec.rb +156 -0
- data/spec/pivotal-tracker/iteration_spec.rb +76 -0
- data/spec/pivotal-tracker/membership_spec.rb +21 -0
- data/spec/pivotal-tracker/note_spec.rb +62 -0
- data/spec/pivotal-tracker/project_spec.rb +105 -0
- data/spec/pivotal-tracker/story_spec.rb +248 -0
- data/spec/pivotal-tracker/task_spec.rb +34 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/support/vcr_config.rb +10 -0
- metadata +169 -0
@@ -0,0 +1,11 @@
|
|
1
|
+
# Happymapper patch for RestClient API Change (response => response.body)
|
2
|
+
|
3
|
+
module HappyMapper
|
4
|
+
module ClassMethods
|
5
|
+
alias_method :orig_parse, :parse
|
6
|
+
def parse(xml, options={})
|
7
|
+
xml = xml.to_s if xml.is_a?(RestClient::Response)
|
8
|
+
orig_parse(xml, options)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module PivotalTracker
|
2
|
+
class Iteration
|
3
|
+
include HappyMapper
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def all(project, options={})
|
7
|
+
params = PivotalTracker.encode_options(options)
|
8
|
+
parse(Client.connection["/projects/#{project.id}/iterations#{params}"].get)
|
9
|
+
end
|
10
|
+
|
11
|
+
def current(project)
|
12
|
+
array = parse(Client.connection["projects/#{project.id}/iterations/current"].get)
|
13
|
+
array.first if array
|
14
|
+
end
|
15
|
+
|
16
|
+
def done(project, options={})
|
17
|
+
params = PivotalTracker.encode_options(options)
|
18
|
+
parse(Client.connection["/projects/#{project.id}/iterations/done#{params}"].get)
|
19
|
+
end
|
20
|
+
|
21
|
+
def backlog(project, options={})
|
22
|
+
params = PivotalTracker.encode_options(options)
|
23
|
+
parse(Client.connection["/projects/#{project.id}/iterations/backlog#{params}"].get)
|
24
|
+
end
|
25
|
+
|
26
|
+
def current_backlog(project, options={})
|
27
|
+
params = PivotalTracker.encode_options(options)
|
28
|
+
parse(Client.connection["/projects/#{project.id}/iterations/current_backlog#{params}"].get)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
element :id, Integer
|
33
|
+
element :number, Integer
|
34
|
+
element :start, DateTime
|
35
|
+
element :finish, DateTime
|
36
|
+
element :team_strength, Float
|
37
|
+
has_many :stories, Story
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module PivotalTracker
|
2
|
+
class Membership
|
3
|
+
include HappyMapper
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def all(project, options={})
|
7
|
+
parse(Client.connection["/projects/#{project.id}/memberships"].get)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
element :id, Integer
|
12
|
+
element :role, String
|
13
|
+
|
14
|
+
# Flattened Attributes from <person>...</person>
|
15
|
+
element :name, String, :deep => true
|
16
|
+
element :email, String, :deep => true
|
17
|
+
element :initials, String, :deep => true
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module PivotalTracker
|
2
|
+
class Note
|
3
|
+
include HappyMapper
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def all(story, options={})
|
7
|
+
notes = parse(Client.connection["/projects/#{story.project_id}/stories/#{story.id}/notes"].get)
|
8
|
+
notes.each { |n| n.project_id, n.story_id = story.project_id, story.id }
|
9
|
+
return notes
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :project_id, :story_id
|
14
|
+
|
15
|
+
element :id, Integer
|
16
|
+
element :text, String
|
17
|
+
element :author, String
|
18
|
+
element :noted_at, DateTime
|
19
|
+
has_one :story, Story
|
20
|
+
|
21
|
+
def initialize(attributes={})
|
22
|
+
if attributes[:owner]
|
23
|
+
self.story = attributes.delete(:owner)
|
24
|
+
self.project_id = self.story.project_id
|
25
|
+
self.story_id = self.story.id
|
26
|
+
end
|
27
|
+
|
28
|
+
update_attributes(attributes)
|
29
|
+
end
|
30
|
+
|
31
|
+
def create
|
32
|
+
response = Client.connection["/projects/#{project_id}/stories/#{story_id}/notes"].post(self.to_xml, :content_type => 'application/xml')
|
33
|
+
return Note.parse(response)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Pivotal Tracker API doesn't seem to support updating or deleting notes at this time.
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def to_xml
|
41
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
42
|
+
xml.note {
|
43
|
+
#xml.author "#{author}"
|
44
|
+
xml.text_ "#{text}"
|
45
|
+
xml.noted_at "#{noted_at}"
|
46
|
+
}
|
47
|
+
end
|
48
|
+
return builder.to_xml
|
49
|
+
end
|
50
|
+
|
51
|
+
def update_attributes(attrs)
|
52
|
+
attrs.each do |key, value|
|
53
|
+
self.send("#{key}=", value.is_a?(Array) ? value.join(',') : value )
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module PivotalTracker
|
2
|
+
class Project
|
3
|
+
include HappyMapper
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def all
|
7
|
+
@found = parse(Client.connection['/projects'].get)
|
8
|
+
end
|
9
|
+
|
10
|
+
def find(id)
|
11
|
+
if @found
|
12
|
+
project = @found.detect { |document| document.id == id }
|
13
|
+
if !project
|
14
|
+
project = parse(Client.connection["/projects/#{id}"].get)
|
15
|
+
end
|
16
|
+
project
|
17
|
+
else
|
18
|
+
parse(Client.connection["/projects/#{id}"].get)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
element :id, Integer
|
24
|
+
element :name, String
|
25
|
+
element :account, String
|
26
|
+
element :week_start_day, String
|
27
|
+
element :point_scale, String
|
28
|
+
element :labels, String
|
29
|
+
element :velocity_scheme, String
|
30
|
+
element :iteration_length, Integer
|
31
|
+
element :initial_velocity, Integer
|
32
|
+
element :current_velocity, Integer
|
33
|
+
element :last_activity_at, DateTime
|
34
|
+
element :use_https, Boolean
|
35
|
+
element :first_iteration_start_time, DateTime
|
36
|
+
element :current_iteration_number, Integer
|
37
|
+
|
38
|
+
def initialize(attributes={})
|
39
|
+
update_attributes(attributes)
|
40
|
+
end
|
41
|
+
|
42
|
+
def create
|
43
|
+
response = Client.connection["/projects"].post(self.to_xml, :content_type => 'application/xml')
|
44
|
+
project = Project.parse(response)
|
45
|
+
return project
|
46
|
+
end
|
47
|
+
|
48
|
+
def activities
|
49
|
+
@activities ||= Proxy.new(self, Activity)
|
50
|
+
end
|
51
|
+
|
52
|
+
def iterations
|
53
|
+
@iterations ||= Proxy.new(self, Iteration)
|
54
|
+
end
|
55
|
+
|
56
|
+
def stories
|
57
|
+
@stories ||= Proxy.new(self, Story)
|
58
|
+
end
|
59
|
+
|
60
|
+
def memberships
|
61
|
+
@memberships ||= Proxy.new(self, Membership)
|
62
|
+
end
|
63
|
+
|
64
|
+
def iteration(group)
|
65
|
+
case group.to_sym
|
66
|
+
when :done then Iteration.done(self)
|
67
|
+
when :current then Iteration.current(self)
|
68
|
+
when :backlog then Iteration.backlog(self)
|
69
|
+
when :current_backlog then Iteration.current_backlog(self)
|
70
|
+
else
|
71
|
+
raise ArgumentError, "Invalid group. Use :done, :current, :backlog or :current_backlog instead."
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
def to_xml
|
78
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
79
|
+
xml.project {
|
80
|
+
xml.name "#{name}"
|
81
|
+
xml.iteration_length.integer "#{iteration_length}" unless iteration_length.nil?
|
82
|
+
xml.point_scale "#{point_scale}" unless point_scale.nil?
|
83
|
+
}
|
84
|
+
end
|
85
|
+
return builder.to_xml
|
86
|
+
end
|
87
|
+
|
88
|
+
def update_attributes(attrs)
|
89
|
+
attrs.each do |key, value|
|
90
|
+
self.send("#{key}=", value.is_a?(Array) ? value.join(',') : value )
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
class Project
|
96
|
+
include Validation
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class BasicObject #:nodoc:
|
2
|
+
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|instance_eval|proxy_|^object_id$)/ }
|
3
|
+
end unless defined?(BasicObject)
|
4
|
+
|
5
|
+
module PivotalTracker
|
6
|
+
class Proxy < BasicObject
|
7
|
+
|
8
|
+
def initialize(owner, target)
|
9
|
+
@owner = owner
|
10
|
+
@target = target
|
11
|
+
@opts = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def all(options={})
|
15
|
+
proxy_found(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def find(param, options={})
|
19
|
+
return all(options) if param == :all
|
20
|
+
return @target.find(param, @owner.id) if @target.respond_to?("find")
|
21
|
+
return proxy_found(options).detect { |document| document.id == param }
|
22
|
+
end
|
23
|
+
|
24
|
+
def <<(*objects)
|
25
|
+
objects.flatten.each do |object|
|
26
|
+
if obj = object.create
|
27
|
+
return obj
|
28
|
+
else
|
29
|
+
return object
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def create(args = {})
|
35
|
+
object = @target.new(args.merge({:owner => @owner}))
|
36
|
+
if obj = object.create
|
37
|
+
return obj
|
38
|
+
else
|
39
|
+
return object
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
def proxy_found(options)
|
46
|
+
if @found.nil? or @opts != options
|
47
|
+
@opts = options
|
48
|
+
@found = load_found()
|
49
|
+
end
|
50
|
+
@found
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def method_missing(method, *args, &block)
|
56
|
+
@target.send(method, *args, &block)
|
57
|
+
end
|
58
|
+
|
59
|
+
def load_found()
|
60
|
+
@target.all(@owner, @opts)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module PivotalTracker
|
2
|
+
class Story
|
3
|
+
include HappyMapper
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def all(project, options={})
|
7
|
+
params = PivotalTracker.encode_options(options)
|
8
|
+
stories = parse(Client.connection["/projects/#{project.id}/stories#{params}"].get)
|
9
|
+
stories.each { |s| s.project_id = project.id }
|
10
|
+
return stories
|
11
|
+
end
|
12
|
+
|
13
|
+
def find(param, project_id)
|
14
|
+
begin
|
15
|
+
story = parse(Client.connection["/projects/#{project_id}/stories/#{param}"].get)
|
16
|
+
story.project_id = project_id
|
17
|
+
rescue RestClient::ExceptionWithResponse
|
18
|
+
story = nil
|
19
|
+
end
|
20
|
+
return story
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
tag "story"
|
25
|
+
|
26
|
+
element :id, Integer
|
27
|
+
element :url, String
|
28
|
+
element :created_at, DateTime
|
29
|
+
element :accepted_at, DateTime
|
30
|
+
element :project_id, Integer
|
31
|
+
|
32
|
+
element :name, String
|
33
|
+
element :description, String
|
34
|
+
element :story_type, String
|
35
|
+
element :estimate, Integer
|
36
|
+
element :current_state, String
|
37
|
+
element :requested_by, String
|
38
|
+
element :owned_by, String
|
39
|
+
element :labels, String
|
40
|
+
element :jira_id, Integer
|
41
|
+
element :jira_url, String
|
42
|
+
element :other_id, String
|
43
|
+
element :integration_id, Integer
|
44
|
+
element :deadline, DateTime # Only available for Release stories
|
45
|
+
|
46
|
+
has_many :attachments, Attachment, :tag => 'attachment', :xpath => '//attachments'
|
47
|
+
|
48
|
+
def initialize(attributes={})
|
49
|
+
if attributes[:owner]
|
50
|
+
self.project_id = attributes.delete(:owner).id
|
51
|
+
end
|
52
|
+
update_attributes(attributes)
|
53
|
+
end
|
54
|
+
|
55
|
+
def create
|
56
|
+
return self if project_id.nil?
|
57
|
+
response = Client.connection["/projects/#{project_id}/stories"].post(self.to_xml, :content_type => 'application/xml')
|
58
|
+
new_story = Story.parse(response)
|
59
|
+
new_story.project_id = project_id
|
60
|
+
return new_story
|
61
|
+
end
|
62
|
+
|
63
|
+
def update(attrs={})
|
64
|
+
update_attributes(attrs)
|
65
|
+
response = Client.connection["/projects/#{project_id}/stories/#{id}"].put(self.to_xml, :content_type => 'application/xml')
|
66
|
+
return Story.parse(response)
|
67
|
+
end
|
68
|
+
|
69
|
+
def move(position, story)
|
70
|
+
raise ArgumentError, "Can only move :before or :after" unless [:before, :after].include? position
|
71
|
+
Story.parse(Client.connection["/projects/#{project_id}/stories/#{id}/moves?move\[move\]=#{position}&move\[target\]=#{story.id}"].post(''))
|
72
|
+
end
|
73
|
+
|
74
|
+
def delete
|
75
|
+
Client.connection["/projects/#{project_id}/stories/#{id}"].delete
|
76
|
+
end
|
77
|
+
|
78
|
+
def notes
|
79
|
+
@notes ||= Proxy.new(self, Note)
|
80
|
+
end
|
81
|
+
|
82
|
+
def tasks
|
83
|
+
@tasks ||= Proxy.new(self, Task)
|
84
|
+
end
|
85
|
+
|
86
|
+
def upload_attachment(filename)
|
87
|
+
Attachment.parse(Client.connection["/projects/#{project_id}/stories/#{id}/attachments"].post(:Filedata => File.new(filename)))
|
88
|
+
end
|
89
|
+
|
90
|
+
def move_to_project(new_project)
|
91
|
+
move = true
|
92
|
+
old_project_id = self.project_id
|
93
|
+
target_project = -1
|
94
|
+
case new_project.class.to_s
|
95
|
+
when 'PivotalTracker::Story'
|
96
|
+
target_project = new_project.project_id
|
97
|
+
when 'PivotalTracker::Project'
|
98
|
+
target_project = new_project.id
|
99
|
+
when 'String'
|
100
|
+
target_project = new_project.to_i
|
101
|
+
when 'Fixnum', 'Integer'
|
102
|
+
target_project = new_project
|
103
|
+
else
|
104
|
+
move = false
|
105
|
+
end
|
106
|
+
if move
|
107
|
+
move_builder = Nokogiri::XML::Builder.new do |story|
|
108
|
+
story.story {
|
109
|
+
story.project_id "#{target_project}"
|
110
|
+
}
|
111
|
+
end
|
112
|
+
Story.parse(Client.connection["/projects/#{old_project_id}/stories/#{id}"].put(move_builder.to_xml, :content_type => 'application/xml'))
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
protected
|
117
|
+
|
118
|
+
def to_xml
|
119
|
+
builder = Nokogiri::XML::Builder.new do |xml|
|
120
|
+
xml.story {
|
121
|
+
xml.name "#{name}"
|
122
|
+
xml.description "#{description}"
|
123
|
+
xml.story_type "#{story_type}"
|
124
|
+
xml.estimate "#{estimate}"
|
125
|
+
xml.current_state "#{current_state}"
|
126
|
+
xml.requested_by "#{requested_by}"
|
127
|
+
xml.owned_by "#{owned_by}"
|
128
|
+
xml.labels "#{labels}"
|
129
|
+
xml.project_id "#{project_id}"
|
130
|
+
# See spec
|
131
|
+
# xml.jira_id "#{jira_id}"
|
132
|
+
# xml.jira_url "#{jira_url}"
|
133
|
+
xml.other_id "#{other_id}" if other_id
|
134
|
+
xml.integration_id "#{integration_id}" if integration_id
|
135
|
+
xml.created_at DateTime.parse(created_at.to_s).to_s if created_at
|
136
|
+
xml.accepted_at DateTime.parse(accepted_at.to_s).to_s if accepted_at
|
137
|
+
xml.deadline DateTime.parse(deadline.to_s).to_s if deadline
|
138
|
+
}
|
139
|
+
end
|
140
|
+
return builder.to_xml
|
141
|
+
end
|
142
|
+
|
143
|
+
def update_attributes(attrs)
|
144
|
+
attrs.each do |key, value|
|
145
|
+
self.send("#{key}=", value.is_a?(Array) ? value.join(',') : value )
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
class Story
|
151
|
+
include Validation
|
152
|
+
end
|
153
|
+
end
|