taskmapper-basecamp 0.6.4 → 0.7.0

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.
@@ -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