lrd-pivotal-tracker 0.5.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -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