taskmapper-basecamp 0.6.4 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,9 +11,9 @@ describe TaskMapper::Provider::Basecamp::Project do
11
11
  context "When I retrieve projects" do
12
12
  before(:each) do
13
13
  ActiveResource::HttpMock.respond_to do |mock|
14
- mock.get '/projects.json', headers, fixture_for('projects', 'json'), 200
15
- mock.get '/projects/5220065.json', headers, fixture_for('projects/5220065', 'json'), 200
16
- mock.get '/projects.json', headers, fixture_for('projects','json'), 200
14
+ mock.get '/projects.xml', headers, fixture_for('projects'), 200
15
+ mock.get '/projects/5220065.xml', headers, fixture_for('projects/5220065'), 200
16
+ mock.get '/projects.xml', headers, fixture_for('projects'), 200
17
17
  end
18
18
  end
19
19
  describe :projects do
data/spec/tickets_spec.rb CHANGED
@@ -3,21 +3,24 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
3
3
  describe TaskMapper::Provider::Basecamp::Ticket do
4
4
  let(:project_id) { 5220065 }
5
5
  let(:headers) { {'Authorization' => 'Basic MDAwMDAwOkJhc2VjYW1w'} }
6
- let(:wheaders) { headers.merge('Content-Type' => 'application/json') }
6
+ let(:nheaders) { headers.merge('Accept' => 'application/xml') }
7
+ let(:wheaders) { headers.merge('Content-Type' => 'application/xml') }
7
8
  let(:project) { tm.project(project_id) }
8
9
  let(:tm) { TaskMapper.new(:basecamp, :domain => 'ticketmaster.basecamphq.com', :token => '000000') }
9
10
  let(:ticket_class) { TaskMapper::Provider::Basecamp::Ticket }
10
11
 
11
12
  context "Retrieve tickets" do
12
- before(:all) do
13
+ before(:each) do
13
14
  ActiveResource::HttpMock.respond_to do |mock|
14
- mock.get '/projects/5220065.json', headers, fixture_for('projects/5220065', 'json'), 200
15
- mock.get '/todo_lists.json?responsible_party=', headers, fixture_for('todo_list_with_items', 'json'), 200
15
+ mock.get "/projects/#{project_id}.xml", nheaders, fixture_for('projects/5220065'), 200
16
+ mock.get "/projects/#{project_id}/todo_lists.xml?responsible_party=", nheaders, fixture_for('todo_list_with_items'), 200
17
+ mock.get "/todo_lists/19700819/todo_items.xml", nheaders, fixture_for('todo_list_with_items'), 200
18
+ mock.get "/todo_lists/19700382/todo_items.xml", nheaders, fixture_for('todo_list_with_items'), 200
19
+ mock.get "/todo_lists/19700377/todo_items.xml", nheaders, fixture_for('todo_list_with_items'), 200
16
20
  end
17
21
  end
18
-
22
+
19
23
  shared_examples_for "ticket 133184178" do
20
- its(:id) { should == 133184178 }
21
24
  its(:title) { should match /updated/ }
22
25
  its(:priority) { should == 1 }
23
26
  its(:status) { should == 'incomplete' }
@@ -33,37 +36,35 @@ describe TaskMapper::Provider::Basecamp::Ticket do
33
36
  let(:tickets) { project.tickets }
34
37
  subject { tickets }
35
38
 
36
- its(:count) { should == 2 }
37
-
39
+ its(:count) { should == 4 }
40
+
38
41
  describe :first do
39
42
  subject { tickets.first }
40
- it_behaves_like "ticket 133184178"
43
+ its(:id) { should == 19700819 }
41
44
  end
42
45
  end
43
-
46
+
44
47
  pending "Search passing ids array'" do
45
- let(:tickets) { project.tickets [133184178, 133180422] }
48
+ let(:tickets) { project.tickets [133184178, 19700382] }
46
49
  subject { tickets }
47
-
50
+
48
51
  its(:count) { should == 2 }
49
-
52
+
50
53
  describe :first do
51
54
  subject { tickets.first }
52
- it_behaves_like "ticket 133184178"
53
55
  end
54
56
  end
55
57
 
56
58
  describe "Find by id" do
57
59
  subject { project.ticket 133184178 }
58
- it_behaves_like "ticket 133184178"
59
60
  end
60
61
  end
61
62
 
62
63
  pending "Update and creation" do
63
64
  before(:all) do
64
65
  ActiveResource::HttpMock.respond_to do |mock|
65
- mock.post '/todo_lists/9972756/todo_items.json', wheaders, '', 200
66
- mock.post '/projects/5220065/todo_lists.json', wheaders, '', 200
66
+ mock.post '/todo_lists/9972756/todo_items.xml', wheaders, '', 200
67
+ mock.post '/projects/5220065/todo_lists.xml', wheaders, '', 200
67
68
  end
68
69
  end
69
70
 
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "taskmapper-basecamp"
8
- s.version = "0.6.4"
8
+ s.version = "0.7.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["HybridGroup"]
12
- s.date = "2013-01-04"
12
+ s.date = "2013-01-09"
13
13
  s.description = "This gem provides an interface to basecamp through the taskmapper gem"
14
14
  s.email = "hong.quach@abigfisch.com"
15
15
  s.extra_rdoc_files = [
@@ -27,7 +27,6 @@ Gem::Specification.new do |s|
27
27
  "README.md",
28
28
  "Rakefile",
29
29
  "VERSION",
30
- "lib/basecamp/basecamp.rb",
31
30
  "lib/provider/basecamp.rb",
32
31
  "lib/provider/comment.rb",
33
32
  "lib/provider/project.rb",
@@ -78,7 +77,7 @@ Gem::Specification.new do |s|
78
77
 
79
78
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
80
79
  s.add_runtime_dependency(%q<taskmapper>, ["~> 0.8"])
81
- s.add_runtime_dependency(%q<xml-simple>, ["~> 1.1"])
80
+ s.add_runtime_dependency(%q<basecamp>, [">= 0"])
82
81
  s.add_development_dependency(%q<rspec>, [">= 0"])
83
82
  s.add_development_dependency(%q<jeweler>, ["~> 1.6"])
84
83
  s.add_development_dependency(%q<guard>, [">= 0"])
@@ -89,7 +88,7 @@ Gem::Specification.new do |s|
89
88
  s.add_development_dependency(%q<rcov>, ["~> 1.0"])
90
89
  else
91
90
  s.add_dependency(%q<taskmapper>, ["~> 0.8"])
92
- s.add_dependency(%q<xml-simple>, ["~> 1.1"])
91
+ s.add_dependency(%q<basecamp>, [">= 0"])
93
92
  s.add_dependency(%q<rspec>, [">= 0"])
94
93
  s.add_dependency(%q<jeweler>, ["~> 1.6"])
95
94
  s.add_dependency(%q<guard>, [">= 0"])
@@ -101,7 +100,7 @@ Gem::Specification.new do |s|
101
100
  end
102
101
  else
103
102
  s.add_dependency(%q<taskmapper>, ["~> 0.8"])
104
- s.add_dependency(%q<xml-simple>, ["~> 1.1"])
103
+ s.add_dependency(%q<basecamp>, [">= 0"])
105
104
  s.add_dependency(%q<rspec>, [">= 0"])
106
105
  s.add_dependency(%q<jeweler>, ["~> 1.6"])
107
106
  s.add_dependency(%q<guard>, [">= 0"])
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: taskmapper-basecamp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.4
4
+ version: 0.7.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-04 00:00:00.000000000 Z
12
+ date: 2013-01-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: taskmapper
@@ -28,21 +28,21 @@ dependencies:
28
28
  - !ruby/object:Gem::Version
29
29
  version: '0.8'
30
30
  - !ruby/object:Gem::Dependency
31
- name: xml-simple
31
+ name: basecamp
32
32
  requirement: !ruby/object:Gem::Requirement
33
33
  none: false
34
34
  requirements:
35
- - - ~>
35
+ - - ! '>='
36
36
  - !ruby/object:Gem::Version
37
- version: '1.1'
37
+ version: '0'
38
38
  type: :runtime
39
39
  prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
41
41
  none: false
42
42
  requirements:
43
- - - ~>
43
+ - - ! '>='
44
44
  - !ruby/object:Gem::Version
45
- version: '1.1'
45
+ version: '0'
46
46
  - !ruby/object:Gem::Dependency
47
47
  name: rspec
48
48
  requirement: !ruby/object:Gem::Requirement
@@ -189,7 +189,6 @@ files:
189
189
  - README.md
190
190
  - Rakefile
191
191
  - VERSION
192
- - lib/basecamp/basecamp.rb
193
192
  - lib/provider/basecamp.rb
194
193
  - lib/provider/comment.rb
195
194
  - lib/provider/project.rb
@@ -243,7 +242,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
243
242
  version: '0'
244
243
  segments:
245
244
  - 0
246
- hash: -1891770358354267207
245
+ hash: 2426728134394223182
247
246
  required_rubygems_version: !ruby/object:Gem::Requirement
248
247
  none: false
249
248
  requirements:
@@ -1,654 +0,0 @@
1
- require 'net/https'
2
- require 'yaml'
3
- require 'date'
4
- require 'time'
5
-
6
- begin
7
- require 'xmlsimple'
8
- rescue LoadError
9
- begin
10
- require 'rubygems'
11
- require 'xmlsimple'
12
- rescue LoadError
13
- abort <<-ERROR
14
- The 'xml-simple' library could not be loaded. If you have RubyGems installed
15
- you can install xml-simple by doing "gem install xml-simple".
16
- ERROR
17
- end
18
- end
19
-
20
- begin
21
- require 'active_resource'
22
- rescue LoadError
23
- begin
24
- require 'rubygems'
25
- require 'active_resource'
26
- rescue LoadError
27
- abort <<-ERROR
28
- The 'active_resource' library could not be loaded. If you have RubyGems
29
- installed you can install ActiveResource by doing "gem install activeresource".
30
- ERROR
31
- end
32
- end
33
-
34
- # = A Ruby library for working with the Basecamp web-services API.
35
- #
36
- # For more information about the Basecamp web-services API, visit:
37
- #
38
- # http://developer.37signals.com/basecamp
39
- #
40
- # NOTE: not all of Basecamp's web-services are accessible via REST. This
41
- # library provides access to RESTful services via ActiveResource. Services not
42
- # yet upgraded to REST are accessed via the Basecamp class. Continue reading
43
- # for more details.
44
- #
45
- #
46
- # == Establishing a Connection
47
- #
48
- # The first thing you need to do is establish a connection to Basecamp. This
49
- # requires your Basecamp site address and your login credentials. Example:
50
- #
51
- # Basecamp.establish_connection!('you.grouphub.com', 'username', 'password')
52
- #
53
- # This is the same whether you're accessing using the ActiveResource interface,
54
- # or the legacy interface.
55
- #
56
- #
57
- # == Using the REST interface via ActiveResource
58
- #
59
- # The REST interface is accessed via ActiveResource, a popular Ruby library
60
- # that implements object-relational mapping for REST web-services. For more
61
- # information on working with ActiveResource, see:
62
- #
63
- # * http://api.rubyonrails.org/files/activeresource/README.html
64
- # * http://api.rubyonrails.org/classes/ActiveResource/Base.html
65
- #
66
- #
67
- # === Finding a Resource
68
- #
69
- # Find a specific resource using the +find+ method. Attributes of the resource
70
- # are available as instance methods on the resulting object. For example, to
71
- # find a message with the ID of 8675309 and access its title attribute, you
72
- # would do the following:
73
- #
74
- # m = Basecamp::Message.find(8675309)
75
- # m.title # => 'Jenny'
76
- #
77
- # To find all messages for a given project, use find(:all), passing the
78
- # project_id as a parameter to find. Example:
79
- #
80
- # messages = Basecamp::Message.find(:all, params => { :project_id => 1037 })
81
- # messages.size # => 25
82
- #
83
- #
84
- # === Creating a Resource
85
- #
86
- # Create a resource by making a new instance of that resource, setting its
87
- # attributes, and saving it. If the resource requires a prefix to identify
88
- # it (as is the case with resources that belong to a sub-resource, such as a
89
- # project), it should be specified when instantiating the object. Examples:
90
- #
91
- # m = Basecamp::Message.new(:project_id => 1037)
92
- # m.category_id = 7301
93
- # m.title = 'Message in a bottle'
94
- # m.body = 'Another lonely day, with no one here but me'
95
- # m.save # => true
96
- #
97
- # c = Basecamp::Comment.new(:post_id => 25874)
98
- # c.body = 'Did you get those TPS reports?'
99
- # c.save # => true
100
- #
101
- # You can also create a resource using the +create+ method, which will create
102
- # and save it in one step. Example:
103
- #
104
- # Basecamp::TodoItem.create(:todo_list_id => 3422, :contents => 'Do it')
105
- #
106
- #
107
- # === Updating a Resource
108
- #
109
- # To update a resource, first find it by its id, change its attributes, and
110
- # save it. Example:
111
- #
112
- # m = Basecamp::Message.find(8675309)
113
- # m.body = 'Changed'
114
- # m.save # => true
115
- #
116
- #
117
- # === Deleting a Resource
118
- #
119
- # To delete a resource, use the +delete+ method with the ID of the resource
120
- # you want to delete. Example:
121
- #
122
- # Basecamp::Message.delete(1037)
123
- #
124
- #
125
- # === Attaching Files to a Resource
126
- #
127
- # If the resource accepts file attachments, the +attachments+ parameter should
128
- # be an array of Basecamp::Attachment objects. Example:
129
- #
130
- # a1 = Basecamp::Attachment.create('primary', File.read('primary.doc'))
131
- # a2 = Basecamp::Attachment.create('another', File.read('another.doc'))
132
- #
133
- # m = Basecamp::Message.new(:project_id => 1037)
134
- # ...
135
- # m.attachments = [a1, a2]
136
- # m.save # => true
137
- #
138
- #
139
- # = Using the non-REST inteface
140
- #
141
- # The non-REST interface is accessed via instance methods on the Basecamp
142
- # class. Ensure you've established a connection, then create a new Basecamp
143
- # instance and call methods on it. Object attributes are accessible as methods.
144
- # Example:
145
- #
146
- # session = Basecamp.new
147
- # person = session.person(93832) # => #<Record(person)..>
148
- # person.first_name # => "Jason"
149
- #
150
- class BasecampAPI
151
- class Connection #:nodoc:
152
- def initialize(master)
153
- @master = master
154
- @connection = Net::HTTP.new(master.site, master.use_ssl ? 443 : 80)
155
- @connection.use_ssl = master.use_ssl
156
- @connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if master.use_ssl
157
- end
158
-
159
- def post(path, body, headers = {})
160
- request = Net::HTTP::Post.new(path, headers.merge('Accept' => 'application/xml'))
161
- request.basic_auth(@master.user, @master.password)
162
- @connection.request(request, body)
163
- end
164
- end
165
-
166
- class Resource < ActiveResource::Base #:nodoc:
167
- self.format = :json
168
- class << self
169
- def parent_resources(*parents)
170
- @parent_resources = parents
171
- end
172
-
173
- def element_name
174
- name.split(/::/).last.underscore
175
- end
176
-
177
- def prefix_source
178
- if @parent_resources
179
- @parent_resources.map { |resource| "/#{resource.to_s.pluralize}/:#{resource}_id" }.join + '/'
180
- else
181
- '/'
182
- end
183
- end
184
-
185
- def prefix(options = {})
186
- if options.any?
187
- options.map { |name, value| "/#{name.to_s.chomp('_id').pluralize}/#{value}" }.join + '/'
188
- else
189
- '/'
190
- end
191
- end
192
- end
193
-
194
- # Commented because it was causing a Missing prefix error when updating a TodoItem
195
- #def prefix_options
196
- # id ? {} : super
197
- #end
198
- end
199
-
200
- class Account < Resource
201
- end
202
-
203
- class People < Resource
204
- end
205
-
206
- class Project < Resource
207
- end
208
-
209
- class Company < Resource
210
- parent_resources :project
211
-
212
- def self.on_project(project_id, options = {})
213
- find(:all, :params => options.merge(:project_id => project_id))
214
- end
215
- end
216
-
217
- # == Creating different types of categories
218
- #
219
- # The type parameter is required when creating a category. For exampe, to
220
- # create an attachment category for a particular project:
221
- #
222
- # c = Basecamp::Category.new(:project_id => 1037)
223
- # c.type = 'attachment'
224
- # c.name = 'Pictures'
225
- # c.save # => true
226
- #
227
- class Category < Resource
228
- parent_resources :project
229
-
230
- def self.all(project_id, options = {})
231
- find(:all, :params => options.merge(:project_id => project_id))
232
- end
233
-
234
- def self.post_categories(project_id, options = {})
235
- find(:all, :params => options.merge(:project_id => project_id, :type => 'post'))
236
- end
237
-
238
- def self.attachment_categories(project_id, options = {})
239
- find(:all, :params => options.merge(:project_id => project_id, :type => 'attachment'))
240
- end
241
- end
242
-
243
- class Message < Resource
244
- parent_resources :project
245
- set_element_name 'post'
246
-
247
- # Returns the most recent 25 messages in the given project (and category,
248
- # if specified). If you need to retrieve older messages, use the archive
249
- # method instead. Example:
250
- #
251
- # Basecamp::Message.recent(1037)
252
- # Basecamp::Message.recent(1037, :category_id => 7301)
253
- #
254
- def self.recent(project_id, options = {})
255
- find(:all, :params => options.merge(:project_id => project_id))
256
- end
257
-
258
- # Returns a summary of all messages in the given project (and category, if
259
- # specified). The summary is simply the title and category of the message,
260
- # as well as the number of attachments (if any). Example:
261
- #
262
- # Basecamp::Message.archive(1037)
263
- # Basecamp::Message.archive(1037, :category_id => 7301)
264
- #
265
- def self.archive(project_id, options = {})
266
- find(:all, :params => options.merge(:project_id => project_id), :from => :archive)
267
- end
268
-
269
- def comments(options = {})
270
- @comments ||= Comment.find(:all, :params => options.merge(:post_id => id))
271
- end
272
- end
273
-
274
- # == Creating comments for multiple resources
275
- #
276
- # Comments can be created for messages, milestones, and to-dos, identified
277
- # by the <tt>post_id</tt>, <tt>milestone_id</tt>, and <tt>todo_item_id</tt>
278
- # params respectively.
279
- #
280
- # For example, to create a comment on the message with id #8675309:
281
- #
282
- # c = Basecamp::Comment.new(:post_id => 8675309)
283
- # c.body = 'Great tune'
284
- # c.save # => true
285
- #
286
- # Similarly, to create a comment on a milestone:
287
- #
288
- # c = Basecamp::Comment.new(:milestone_id => 8473647)
289
- # c.body = 'Is this done yet?'
290
- # c.save # => true
291
- #
292
- class Comment < Resource
293
- parent_resources :todo_item
294
- end
295
-
296
- class TodoList < Resource
297
- parent_resources :project
298
-
299
- # Returns all lists for a project. If complete is true, only completed lists
300
- # are returned. If complete is false, only uncompleted lists are returned.
301
- def self.all(project_id, complete = nil)
302
- filter = case complete
303
- when nil then "all"
304
- when true then "finished"
305
- when false then "pending"
306
- else raise ArgumentError, "invalid value for `complete'"
307
- end
308
-
309
- find(:all, :params => { :project_id => project_id, :filter => filter })
310
- end
311
-
312
- def todo_items(options = {})
313
- @todo_items ||= TodoItem.find(:all, :params => options.merge(:todo_list_id => id))
314
- end
315
- end
316
-
317
- # This resource is to address GET /todo_lists.xml?responsible_party=#{id}
318
- # To retrieve Todo lists with items inside in one request
319
- class TodoListWithItems < Resource
320
- def self.element_name
321
- "todo_lists"
322
- end
323
- end
324
-
325
- class TodoItem < Resource
326
- parent_resources :todo_list
327
-
328
- def todo_list(options = {})
329
- @todo_list ||= TodoList.find(todo_list_id, options)
330
- end
331
-
332
- def time_entries(options = {})
333
- @time_entries ||= TimeEntry.find(:all, :params => options.merge(:todo_item_id => id))
334
- end
335
-
336
- def comments(options = {})
337
- @comments ||= Comment.find(:all, :params => options.merge(:todo_item_id => id))
338
- end
339
-
340
- def complete!
341
- put(:complete)
342
- end
343
-
344
- def uncomplete!
345
- put(:uncomplete)
346
- end
347
- end
348
-
349
- class TimeEntry < Resource
350
- parent_resources :project, :todo_item
351
-
352
- def self.all(project_id, page = 0)
353
- find(:all, :params => { :project_id => project_id, :page => page })
354
- end
355
-
356
- def self.report(options={})
357
- find(:all, :from => :report, :params => options)
358
- end
359
- end
360
-
361
- class Category < Resource
362
- parent_resources :project
363
- end
364
-
365
- class Attachment
366
- attr_accessor :id, :filename, :content
367
-
368
- def self.create(filename, content)
369
- returning new(filename, content) do |attachment|
370
- attachment.save
371
- end
372
- end
373
-
374
- def initialize(filename, content)
375
- @filename, @content = filename, content
376
- end
377
-
378
- def attributes
379
- { :file => id, :original_filename => filename }
380
- end
381
-
382
- def to_xml(options = {})
383
- { :file => attributes }.to_xml(options)
384
- end
385
-
386
- def inspect
387
- to_s
388
- end
389
-
390
- def save
391
- response = Basecamp.connection.post('/upload', content, 'Content-Type' => 'application/octet-stream')
392
-
393
- if response.code == '200'
394
- self.id = Hash.from_xml(response.body)['upload']['id']
395
- true
396
- else
397
- raise "Could not save attachment: #{response.message} (#{response.code})"
398
- end
399
- end
400
- end
401
-
402
- class Record #:nodoc:
403
- attr_reader :type
404
-
405
- def initialize(type, hash)
406
- @type, @hash = type, hash
407
- end
408
-
409
- def [](name)
410
- name = dashify(name)
411
-
412
- case @hash[name]
413
- when Hash then
414
- @hash[name] = if (@hash[name].keys.length == 1 && @hash[name].values.first.is_a?(Array))
415
- @hash[name].values.first.map { |v| Record.new(@hash[name].keys.first, v) }
416
- else
417
- Record.new(name, @hash[name])
418
- end
419
- else
420
- @hash[name]
421
- end
422
- end
423
-
424
- def id
425
- @hash['id']
426
- end
427
-
428
- def attributes
429
- @hash.keys
430
- end
431
-
432
- def respond_to?(sym)
433
- super || @hash.has_key?(dashify(sym))
434
- end
435
-
436
- def method_missing(sym, *args)
437
- if args.empty? && !block_given? && respond_to?(sym)
438
- self[sym]
439
- else
440
- super
441
- end
442
- end
443
-
444
- def to_s
445
- "\#<Record(#{@type}) #{@hash.inspect[1..-2]}>"
446
- end
447
-
448
- def inspect
449
- to_s
450
- end
451
-
452
- private
453
-
454
- def dashify(name)
455
- name.to_s.tr("_", "-")
456
- end
457
- end
458
-
459
- attr_accessor :use_xml
460
-
461
- class << self
462
- attr_reader :site, :user, :password, :use_ssl
463
-
464
- def establish_connection!(site, user, password, use_ssl = false)
465
- @site = site
466
- @user = user
467
- @password = password
468
- @use_ssl = use_ssl
469
-
470
- Resource.user = user
471
- Resource.password = password
472
- Resource.site = (use_ssl ? "https" : "http") + "://" + site
473
-
474
- @connection = Connection.new(self)
475
- end
476
-
477
- def connection
478
- @connection || raise('No connection established')
479
- end
480
- end
481
-
482
- def initialize
483
- @use_xml = false
484
- end
485
-
486
- # ==========================================================================
487
- # PEOPLE
488
- # ==========================================================================
489
-
490
- # Return an array of the people in the given company. If the project-id is
491
- # given, only people who have access to the given project will be returned.
492
- def people(company_id, project_id=nil)
493
- url = project_id ? "/projects/#{project_id}" : ""
494
- url << "/contacts/people/#{company_id}"
495
- records "person", url
496
- end
497
-
498
- # Return information about the person with the given id
499
- def person(id)
500
- record "/contacts/person/#{id}"
501
- end
502
-
503
- # ==========================================================================
504
- # MILESTONES
505
- # ==========================================================================
506
-
507
- # Returns a list of all milestones for the given project, optionally filtered
508
- # by whether they are completed, late, or upcoming.
509
- def milestones(project_id, find = 'all')
510
- records "milestone", "/projects/#{project_id}/milestones/list", :find => find
511
- end
512
-
513
- # Create a new milestone for the given project. +data+ must be hash of the
514
- # values to set, including +title+, +deadline+, +responsible_party+, and
515
- # +notify+.
516
- def create_milestone(project_id, data)
517
- create_milestones(project_id, [data]).first
518
- end
519
-
520
- # As #create_milestone, but can create multiple milestones in a single
521
- # request. The +milestones+ parameter must be an array of milestone values as
522
- # described in #create_milestone.
523
- def create_milestones(project_id, milestones)
524
- records "milestone", "/projects/#{project_id}/milestones/create", :milestone => milestones
525
- end
526
-
527
- # Updates an existing milestone.
528
- def update_milestone(id, data, move = false, move_off_weekends = false)
529
- record "/milestones/update/#{id}", :milestone => data,
530
- :move_upcoming_milestones => move,
531
- :move_upcoming_milestones_off_weekends => move_off_weekends
532
- end
533
-
534
- # Destroys the milestone with the given id.
535
- def delete_milestone(id)
536
- record "/milestones/delete/#{id}"
537
- end
538
-
539
- # Complete the milestone with the given id
540
- def complete_milestone(id)
541
- record "/milestones/complete/#{id}"
542
- end
543
-
544
- # Uncomplete the milestone with the given id
545
- def uncomplete_milestone(id)
546
- record "/milestones/uncomplete/#{id}"
547
- end
548
-
549
- private
550
-
551
- # Make a raw web-service request to Basecamp. This will return a Hash of
552
- # Arrays of the response, and may seem a little odd to the uninitiated.
553
- def request(path, parameters = {})
554
- response = Basecamp.connection.post(path, convert_body(parameters), "Content-Type" => content_type)
555
-
556
- if response.code.to_i / 100 == 2
557
- result = XmlSimple.xml_in(response.body, 'keeproot' => true, 'contentkey' => '__content__', 'forcecontent' => true)
558
- typecast_value(result)
559
- else
560
- raise "#{response.message} (#{response.code})"
561
- end
562
- end
563
-
564
- # A convenience method for wrapping the result of a query in a Record
565
- # object. This assumes that the result is a singleton, not a collection.
566
- def record(path, parameters={})
567
- result = request(path, parameters)
568
- (result && !result.empty?) ? Record.new(result.keys.first, result.values.first) : nil
569
- end
570
-
571
- # A convenience method for wrapping the result of a query in Record
572
- # objects. This assumes that the result is a collection--any singleton
573
- # result will be wrapped in an array.
574
- def records(node, path, parameters={})
575
- result = request(path, parameters).values.first or return []
576
- result = result[node] or return []
577
- result = [result] unless Array === result
578
- result.map { |row| Record.new(node, row) }
579
- end
580
-
581
- def convert_body(body)
582
- body = use_xml ? body.to_legacy_xml : body.to_yaml
583
- end
584
-
585
- def content_type
586
- use_xml ? "application/xml" : "application/x-yaml"
587
- end
588
-
589
- def typecast_value(value)
590
- case value
591
- when Hash
592
- if value.has_key?("__content__")
593
- content = translate_entities(value["__content__"]).strip
594
- case value["type"]
595
- when "integer" then content.to_i
596
- when "boolean" then content == "true"
597
- when "datetime" then Time.parse(content)
598
- when "date" then Date.parse(content)
599
- else content
600
- end
601
- # a special case to work-around a bug in XmlSimple. When you have an empty
602
- # tag that has an attribute, XmlSimple will not add the __content__ key
603
- # to the returned hash. Thus, we check for the presense of the 'type'
604
- # attribute to look for empty, typed tags, and simply return nil for
605
- # their value.
606
- elsif value.keys == %w(type)
607
- nil
608
- elsif value["nil"] == "true"
609
- nil
610
- # another special case, introduced by the latest rails, where an array
611
- # type now exists. This is parsed by XmlSimple as a two-key hash, where
612
- # one key is 'type' and the other is the actual array value.
613
- elsif value.keys.length == 2 && value["type"] == "array"
614
- value.delete("type")
615
- typecast_value(value)
616
- else
617
- value.empty? ? nil : value.inject({}) do |h,(k,v)|
618
- h[k] = typecast_value(v)
619
- h
620
- end
621
- end
622
- when Array
623
- value.map! { |i| typecast_value(i) }
624
- case value.length
625
- when 0 then nil
626
- when 1 then value.first
627
- else value
628
- end
629
- else
630
- raise "can't typecast #{value.inspect}"
631
- end
632
- end
633
-
634
- def translate_entities(value)
635
- value.gsub(/&lt;/, "<").
636
- gsub(/&gt;/, ">").
637
- gsub(/&quot;/, '"').
638
- gsub(/&apos;/, "'").
639
- gsub(/&amp;/, "&")
640
- end
641
- end
642
-
643
- # A minor hack to let Xml-Simple serialize symbolic keys in hashes
644
- class Symbol
645
- def [](*args)
646
- to_s[*args]
647
- end
648
- end
649
-
650
- class Hash
651
- def to_legacy_xml
652
- XmlSimple.xml_out({:request => self}, 'keeproot' => true, 'noattr' => true)
653
- end
654
- end