basecamp 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -15,8 +15,21 @@ for more details.
15
15
 
16
16
  == Installation
17
17
 
18
+ Install the gem
19
+
18
20
  gem install basecamp
19
21
 
22
+ Include the system gems and require the library in your script
23
+
24
+ require 'rubygems'
25
+ require 'basecamp'
26
+
27
+ === Dependencies
28
+
29
+ * activeresource >= 2.3.0
30
+ * xml-simple
31
+ * oauth2
32
+
20
33
  == Establishing a Connection
21
34
 
22
35
  The first thing you need to do is establish a connection to Basecamp. This
@@ -27,13 +40,6 @@ requires your Basecamp site address and your login credentials. Example:
27
40
  This is the same whether you're accessing using the ActiveResource interface,
28
41
  or the legacy interface.
29
42
 
30
- == Getting API token
31
-
32
- If you only have username/password you can get the API token to use in future calls.
33
-
34
- Basecamp.establish_connection!('you.grouphub.com', 'username', 'password')
35
- Basecamp.get_token
36
-
37
43
  == Using the REST interface via ActiveResource
38
44
 
39
45
  The REST interface is accessed via ActiveResource, a popular Ruby library
@@ -43,7 +49,6 @@ information on working with ActiveResource, see:
43
49
  * http://api.rubyonrails.org/files/activeresource/README.html
44
50
  * http://api.rubyonrails.org/classes/ActiveResource/Base.html
45
51
 
46
-
47
52
  === Finding a Resource
48
53
 
49
54
  Find a specific resource using the +find+ method. Attributes of the resource
@@ -60,6 +65,21 @@ project_id as a parameter to find. Example:
60
65
  messages = Basecamp::Message.find(:all, params => { :project_id => 1037 })
61
66
  messages.size # => 25
62
67
 
68
+ To get the current logged in user:
69
+
70
+ Basecamp::Person.me
71
+
72
+ Note: You can access the API token using this object. This is useful if you only have username/password and you want to use the API token in future calls:
73
+
74
+ Basecamp::Person.me.token
75
+
76
+ To get all people by company:
77
+
78
+ Basecamp::Person.find(:all, :params => {:company_id => company.id})
79
+
80
+ To get all people by project:
81
+
82
+ Basecamp::Person.find(:all, :params => {:project_id => project.id})
63
83
 
64
84
  === Creating a Resource
65
85
 
@@ -115,20 +135,29 @@ be an array of Basecamp::Attachment objects. Example:
115
135
  m.attachments = [a1, a2]
116
136
  m.save # => true
117
137
 
138
+ === Milestones
139
+
140
+ Is not implemented as an active resource, it uses the non-REST interface.
141
+ Browse the source (lib/basecamp/resources/milestone) to see all available methods.
142
+
143
+ For example, to list all milestones in a project:
118
144
 
119
- = Using the non-REST inteface
145
+ milestones = Basecamp::Milestone.list(1037)
146
+ milestones.first.title # => "The Milestone"
120
147
 
121
- The non-REST interface is accessed via instance methods on the Basecamp
122
- class. Ensure you've established a connection, then create a new Basecamp
123
- instance and call methods on it. Object attributes are accessible as methods.
124
- Example:
148
+ = Using the non-REST interface
125
149
 
126
- session = Basecamp.new
127
- person = session.person(93832) # => #<Record(person)..>
150
+ You can access other resources not included in this wrapper yet using "record" and "records".
151
+
152
+ person = Basecamp.record("/contacts/person/93832")
128
153
  person.first_name # => "Jason"
129
154
 
155
+ people_in_company = Basecamp.records("person", "/contacts/people/85")
156
+ people_in_company.first.first_name # => "Jason"
157
+
130
158
  == Contributors
131
159
 
132
160
  * jamesarosen
133
161
  * defeated
134
- * justinbarry
162
+ * justinbarry
163
+ * fmiopensource
data/lib/basecamp.rb CHANGED
@@ -5,507 +5,20 @@ require 'time'
5
5
  require 'active_resource'
6
6
  require 'xmlsimple'
7
7
 
8
- module Basecamp
9
- class Connection #:nodoc:
10
- def initialize(master)
11
- @master = master
12
- @connection = Net::HTTP.new(master.site, master.use_ssl ? 443 : 80)
13
- @connection.use_ssl = master.use_ssl
14
- @connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if master.use_ssl
15
- end
16
-
17
- def post(path, body, headers = {})
18
- request = Net::HTTP::Post.new(path, headers.merge('Accept' => 'application/xml'))
19
- request.basic_auth(@master.user, @master.password)
20
- @connection.request(request, body)
21
- end
22
-
23
- def get(path, headers = {})
24
- request = Net::HTTP::Get.new(path, headers.merge('Accept' => 'application/xml'))
25
- request.basic_auth(@master.user, @master.password)
26
- @connection.request(request)
27
- end
28
- end
29
-
30
- class Resource < ActiveResource::Base #:nodoc:
31
- class << self
32
- def parent_resources(*parents)
33
- @parent_resources = parents
34
- end
35
-
36
- def element_name
37
- name.split(/::/).last.underscore
38
- end
39
-
40
- def prefix_source
41
- if @parent_resources
42
- @parent_resources.map { |resource| "/#{resource.to_s.pluralize}/:#{resource}_id" }.join + '/'
43
- else
44
- '/'
45
- end
46
- end
47
-
48
- def prefix(options = {})
49
- if options.any?
50
- options.map { |name, value| "/#{name.to_s.chomp('_id').pluralize}/#{value}" }.join + '/'
51
- else
52
- '/'
53
- end
54
- end
55
- end
56
-
57
- def prefix_options
58
- id ? {} : super
59
- end
60
- end
61
-
62
- class Project < Resource
63
- def time_entries(options = {})
64
- @time_entries ||= TimeEntry.find(:all, :params => options.merge(:project_id => id))
65
- end
66
- end
67
-
68
- class Company < Resource
69
- parent_resources :project
70
-
71
- def self.on_project(project_id, options = {})
72
- find(:all, :params => options.merge(:project_id => project_id))
73
- end
74
- end
75
-
76
- # == Creating different types of categories
77
- #
78
- # The type parameter is required when creating a category. For exampe, to
79
- # create an attachment category for a particular project:
80
- #
81
- # c = Basecamp::Category.new(:project_id => 1037)
82
- # c.type = 'attachment'
83
- # c.name = 'Pictures'
84
- # c.save # => true
85
- #
86
- class Category < Resource
87
- parent_resources :project
88
-
89
- def self.all(project_id, options = {})
90
- find(:all, :params => options.merge(:project_id => project_id))
91
- end
92
-
93
- def self.post_categories(project_id, options = {})
94
- find(:all, :params => options.merge(:project_id => project_id, :type => 'post'))
95
- end
96
-
97
- def self.attachment_categories(project_id, options = {})
98
- find(:all, :params => options.merge(:project_id => project_id, :type => 'attachment'))
99
- end
100
- end
101
-
102
- class Message < Resource
103
- parent_resources :project
104
- set_element_name 'post'
105
-
106
- # Returns the most recent 25 messages in the given project (and category,
107
- # if specified). If you need to retrieve older messages, use the archive
108
- # method instead. Example:
109
- #
110
- # Basecamp::Message.recent(1037)
111
- # Basecamp::Message.recent(1037, :category_id => 7301)
112
- #
113
- def self.recent(project_id, options = {})
114
- find(:all, :params => options.merge(:project_id => project_id))
115
- end
116
-
117
- # Returns a summary of all messages in the given project (and category, if
118
- # specified). The summary is simply the title and category of the message,
119
- # as well as the number of attachments (if any). Example:
120
- #
121
- # Basecamp::Message.archive(1037)
122
- # Basecamp::Message.archive(1037, :category_id => 7301)
123
- #
124
- def self.archive(project_id, options = {})
125
- find(:all, :params => options.merge(:project_id => project_id), :from => :archive)
126
- end
127
-
128
- def comments(options = {})
129
- @comments ||= Comment.find(:all, :params => options.merge(:post_id => id))
130
- end
131
- end
132
-
133
- # == Creating comments for multiple resources
134
- #
135
- # Comments can be created for messages, milestones, and to-dos, identified
136
- # by the <tt>post_id</tt>, <tt>milestone_id</tt>, and <tt>todo_item_id</tt>
137
- # params respectively.
138
- #
139
- # For example, to create a comment on the message with id #8675309:
140
- #
141
- # c = Basecamp::Comment.new(:post_id => 8675309)
142
- # c.body = 'Great tune'
143
- # c.save # => true
144
- #
145
- # Similarly, to create a comment on a milestone:
146
- #
147
- # c = Basecamp::Comment.new(:milestone_id => 8473647)
148
- # c.body = 'Is this done yet?'
149
- # c.save # => true
150
- #
151
- class Comment < Resource
152
- parent_resources :post, :milestone, :todo_item
153
- end
154
-
155
- class TodoList < Resource
156
- parent_resources :project
157
-
158
- # Returns all lists for a project. If complete is true, only completed lists
159
- # are returned. If complete is false, only uncompleted lists are returned.
160
- def self.all(project_id, complete = nil)
161
- filter = case complete
162
- when nil then "all"
163
- when true then "finished"
164
- when false then "pending"
165
- else raise ArgumentError, "invalid value for `complete'"
166
- end
167
-
168
- find(:all, :params => { :project_id => project_id, :filter => filter })
169
- end
170
-
171
- def todo_items(options = {})
172
- @todo_items ||= TodoItem.find(:all, :params => options.merge(:todo_list_id => id))
173
- end
174
- end
175
-
176
- class TodoItem < Resource
177
- parent_resources :todo_list
178
-
179
- def todo_list(options = {})
180
- @todo_list ||= TodoList.find(todo_list_id, options)
181
- end
182
-
183
- def time_entries(options = {})
184
- @time_entries ||= TimeEntry.find(:all, :params => options.merge(:todo_item_id => id))
185
- end
186
-
187
- def comments(options = {})
188
- @comments ||= Comment.find(:all, :params => options.merge(:todo_item_id => id))
189
- end
190
-
191
- def complete!
192
- put(:complete)
193
- end
194
-
195
- def uncomplete!
196
- put(:uncomplete)
197
- end
198
- end
199
-
200
- class TimeEntry < Resource
201
- parent_resources :project, :todo_item
202
-
203
- def self.all(project_id, page = 0)
204
- find(:all, :params => { :project_id => project_id, :page => page })
205
- end
206
-
207
- def self.report(options={})
208
- find(:all, :from => :report, :params => options)
209
- end
210
- end
211
-
212
- class Category < Resource
213
- parent_resources :project
214
- end
215
-
216
- class Attachment
217
- attr_accessor :id, :filename, :content
218
-
219
- def self.create(filename, content)
220
- returning new(filename, content) do |attachment|
221
- attachment.save
222
- end
223
- end
224
-
225
- def initialize(filename, content)
226
- @filename, @content = filename, content
227
- end
228
-
229
- def attributes
230
- { :file => id, :original_filename => filename }
231
- end
232
-
233
- def to_xml(options = {})
234
- { :file => attributes }.to_xml(options)
235
- end
236
-
237
- def inspect
238
- to_s
239
- end
240
-
241
- def save
242
- response = Basecamp.connection.post('/upload', content, 'Content-Type' => 'application/octet-stream')
243
-
244
- if response.code == '200'
245
- self.id = Hash.from_xml(response.body)['upload']['id']
246
- true
247
- else
248
- raise "Could not save attachment: #{response.message} (#{response.code})"
249
- end
250
- end
251
- end
252
-
253
- class Record #:nodoc:
254
- attr_reader :type
255
-
256
- def initialize(type, hash)
257
- @type, @hash = type, hash
258
- end
259
-
260
- def [](name)
261
- name = dashify(name)
262
-
263
- case @hash[name]
264
- when Hash then
265
- @hash[name] = if (@hash[name].keys.length == 1 && @hash[name].values.first.is_a?(Array))
266
- @hash[name].values.first.map { |v| Record.new(@hash[name].keys.first, v) }
267
- else
268
- Record.new(name, @hash[name])
269
- end
270
- else
271
- @hash[name]
272
- end
273
- end
274
-
275
- def id
276
- @hash['id']
277
- end
278
-
279
- def attributes
280
- @hash.keys
281
- end
282
-
283
- def respond_to?(sym)
284
- super || @hash.has_key?(dashify(sym))
285
- end
286
-
287
- def method_missing(sym, *args)
288
- if args.empty? && !block_given? && respond_to?(sym)
289
- self[sym]
290
- else
291
- super
292
- end
293
- end
294
-
295
- def to_s
296
- "\#<Record(#{@type}) #{@hash.inspect[1..-2]}>"
297
- end
298
-
299
- def inspect
300
- to_s
301
- end
302
-
303
- private
304
-
305
- def dashify(name)
306
- name.to_s.tr("_", "-")
307
- end
308
- end
309
-
310
- attr_accessor :use_xml
311
-
312
- class << self
313
- attr_reader :site, :user, :password, :use_ssl
314
-
315
- def establish_connection!(site, user, password, use_ssl = false)
316
- @site = site
317
- @user = user
318
- @password = password
319
- @use_ssl = use_ssl
320
-
321
- Resource.user = user
322
- Resource.password = password
323
- Resource.site = (use_ssl ? "https" : "http") + "://" + site
324
-
325
- @connection = Connection.new(self)
326
- end
327
-
328
- def connection
329
- @connection || raise('No connection established')
330
- end
331
-
332
- def get_token
333
- response = @connection.get('/me.xml')
334
- xml = XmlSimple.xml_in(response.body)
335
- xml['token'][0]
336
- end
337
- end
338
-
339
- def initialize
340
- @use_xml = false
341
- end
342
-
343
- # ==========================================================================
344
- # PEOPLE
345
- # ==========================================================================
346
-
347
- # Return an array of the people in the given company. If the project-id is
348
- # given, only people who have access to the given project will be returned.
349
- def people(company_id, project_id=nil)
350
- url = project_id ? "/projects/#{project_id}" : ""
351
- url << "/contacts/people/#{company_id}"
352
- records "person", url
353
- end
354
-
355
- # Return information about the person with the given id
356
- def person(id)
357
- record "/contacts/person/#{id}"
358
- end
359
-
360
- # ==========================================================================
361
- # MILESTONES
362
- # ==========================================================================
363
-
364
- # Returns a list of all milestones for the given project, optionally filtered
365
- # by whether they are completed, late, or upcoming.
366
- def milestones(project_id, find = 'all')
367
- records "milestone", "/projects/#{project_id}/milestones/list", :find => find
368
- end
369
-
370
- # Create a new milestone for the given project. +data+ must be hash of the
371
- # values to set, including +title+, +deadline+, +responsible_party+, and
372
- # +notify+.
373
- def create_milestone(project_id, data)
374
- create_milestones(project_id, [data]).first
375
- end
376
-
377
- # As #create_milestone, but can create multiple milestones in a single
378
- # request. The +milestones+ parameter must be an array of milestone values as
379
- # described in #create_milestone.
380
- def create_milestones(project_id, milestones)
381
- records "milestone", "/projects/#{project_id}/milestones/create", :milestone => milestones
382
- end
383
-
384
- # Updates an existing milestone.
385
- def update_milestone(id, data, move = false, move_off_weekends = false)
386
- record "/milestones/update/#{id}", :milestone => data,
387
- :move_upcoming_milestones => move,
388
- :move_upcoming_milestones_off_weekends => move_off_weekends
389
- end
390
-
391
- # Destroys the milestone with the given id.
392
- def delete_milestone(id)
393
- record "/milestones/delete/#{id}"
394
- end
395
-
396
- # Complete the milestone with the given id
397
- def complete_milestone(id)
398
- record "/milestones/complete/#{id}"
399
- end
400
-
401
- # Uncomplete the milestone with the given id
402
- def uncomplete_milestone(id)
403
- record "/milestones/uncomplete/#{id}"
404
- end
405
-
406
- private
407
-
408
- # Make a raw web-service request to Basecamp. This will return a Hash of
409
- # Arrays of the response, and may seem a little odd to the uninitiated.
410
- def request(path, parameters = {})
411
- response = Basecamp.connection.post(path, convert_body(parameters), "Content-Type" => content_type)
412
-
413
- if response.code.to_i / 100 == 2
414
- result = XmlSimple.xml_in(response.body, 'keeproot' => true, 'contentkey' => '__content__', 'forcecontent' => true)
415
- typecast_value(result)
416
- else
417
- raise "#{response.message} (#{response.code})"
418
- end
419
- end
420
-
421
- # A convenience method for wrapping the result of a query in a Record
422
- # object. This assumes that the result is a singleton, not a collection.
423
- def record(path, parameters={})
424
- result = request(path, parameters)
425
- (result && !result.empty?) ? Record.new(result.keys.first, result.values.first) : nil
426
- end
427
-
428
- # A convenience method for wrapping the result of a query in Record
429
- # objects. This assumes that the result is a collection--any singleton
430
- # result will be wrapped in an array.
431
- def records(node, path, parameters={})
432
- result = request(path, parameters).values.first or return []
433
- result = result[node] or return []
434
- result = [result] unless Array === result
435
- result.map { |row| Record.new(node, row) }
436
- end
437
-
438
- def convert_body(body)
439
- body = use_xml ? body.to_legacy_xml : body.to_yaml
440
- end
441
-
442
- def content_type
443
- use_xml ? "application/xml" : "application/x-yaml"
444
- end
445
-
446
- def typecast_value(value)
447
- case value
448
- when Hash
449
- if value.has_key?("__content__")
450
- content = translate_entities(value["__content__"]).strip
451
- case value["type"]
452
- when "integer" then content.to_i
453
- when "boolean" then content == "true"
454
- when "datetime" then Time.parse(content)
455
- when "date" then Date.parse(content)
456
- else content
457
- end
458
- # a special case to work-around a bug in XmlSimple. When you have an empty
459
- # tag that has an attribute, XmlSimple will not add the __content__ key
460
- # to the returned hash. Thus, we check for the presense of the 'type'
461
- # attribute to look for empty, typed tags, and simply return nil for
462
- # their value.
463
- elsif value.keys == %w(type)
464
- nil
465
- elsif value["nil"] == "true"
466
- nil
467
- # another special case, introduced by the latest rails, where an array
468
- # type now exists. This is parsed by XmlSimple as a two-key hash, where
469
- # one key is 'type' and the other is the actual array value.
470
- elsif value.keys.length == 2 && value["type"] == "array"
471
- value.delete("type")
472
- typecast_value(value)
473
- else
474
- value.empty? ? nil : value.inject({}) do |h,(k,v)|
475
- h[k] = typecast_value(v)
476
- h
477
- end
478
- end
479
- when Array
480
- value.map! { |i| typecast_value(i) }
481
- case value.length
482
- when 0 then nil
483
- when 1 then value.first
484
- else value
485
- end
486
- else
487
- raise "can't typecast #{value.inspect}"
488
- end
489
- end
490
-
491
- def translate_entities(value)
492
- value.gsub(/&lt;/, "<").
493
- gsub(/&gt;/, ">").
494
- gsub(/&quot;/, '"').
495
- gsub(/&apos;/, "'").
496
- gsub(/&amp;/, "&")
497
- end
498
- end
499
-
500
- # A minor hack to let Xml-Simple serialize symbolic keys in hashes
501
- class Symbol
502
- def [](*args)
503
- to_s[*args]
504
- end
505
- end
506
-
507
- class Hash
508
- def to_legacy_xml
509
- XmlSimple.xml_out({:request => self}, 'keeproot' => true, 'noattr' => true)
510
- end
511
- end
8
+ require 'basecamp/base'
9
+ require 'basecamp/connection'
10
+ require 'basecamp/hash'
11
+ require 'basecamp/record'
12
+ require 'basecamp/resource'
13
+ require 'basecamp/symbol'
14
+ require 'basecamp/resources/attachment'
15
+ require 'basecamp/resources/category'
16
+ require 'basecamp/resources/comment'
17
+ require 'basecamp/resources/company'
18
+ require 'basecamp/resources/message'
19
+ require 'basecamp/resources/milestone'
20
+ require 'basecamp/resources/person'
21
+ require 'basecamp/resources/project'
22
+ require 'basecamp/resources/time_entry'
23
+ require 'basecamp/resources/todo_item'
24
+ require 'basecamp/resources/todo_list'
@@ -0,0 +1,116 @@
1
+ module Basecamp
2
+ class << self
3
+ attr_accessor :use_xml
4
+ attr_reader :site, :user, :password, :use_ssl
5
+
6
+ def establish_connection!(site, user, password, use_ssl = false)
7
+ @site = site
8
+ @user = user
9
+ @password = password
10
+ @use_ssl = use_ssl
11
+
12
+ Resource.user = user
13
+ Resource.password = password
14
+ Resource.site = (use_ssl ? "https" : "http") + "://" + site
15
+
16
+ @connection = Connection.new(self)
17
+ end
18
+
19
+ def connection
20
+ @connection || raise('No connection established')
21
+ end
22
+
23
+ # Make a raw web-service request to Basecamp. This will return a Hash of
24
+ # Arrays of the response, and may seem a little odd to the uninitiated.
25
+ def request(path, parameters = {})
26
+ response = Basecamp.connection.post(path, convert_body(parameters), "Content-Type" => content_type)
27
+
28
+ if response.code.to_i / 100 == 2
29
+ result = XmlSimple.xml_in(response.body, 'keeproot' => true, 'contentkey' => '__content__', 'forcecontent' => true)
30
+ typecast_value(result)
31
+ else
32
+ raise "#{response.message} (#{response.code})"
33
+ end
34
+ end
35
+
36
+ # A convenience method for wrapping the result of a query in a Record
37
+ # object. This assumes that the result is a singleton, not a collection.
38
+ def record(path, parameters={})
39
+ result = request(path, parameters)
40
+ (result && !result.empty?) ? Record.new(result.keys.first, result.values.first) : nil
41
+ end
42
+
43
+ # A convenience method for wrapping the result of a query in Record
44
+ # objects. This assumes that the result is a collection--any singleton
45
+ # result will be wrapped in an array.
46
+ def records(node, path, parameters={})
47
+ result = request(path, parameters).values.first or return []
48
+ result = result[node] or return []
49
+ result = [result] unless Array === result
50
+ result.map { |row| Record.new(node, row) }
51
+ end
52
+
53
+ private
54
+
55
+ def convert_body(body)
56
+ body = use_xml ? body.to_legacy_xml : body.to_yaml
57
+ end
58
+
59
+ def content_type
60
+ use_xml ? "application/xml" : "application/x-yaml"
61
+ end
62
+
63
+ def typecast_value(value)
64
+ case value
65
+ when Hash
66
+ if value.has_key?("__content__")
67
+ content = translate_entities(value["__content__"]).strip
68
+ case value["type"]
69
+ when "integer" then content.to_i
70
+ when "boolean" then content == "true"
71
+ when "datetime" then Time.parse(content)
72
+ when "date" then Date.parse(content)
73
+ else content
74
+ end
75
+ # a special case to work-around a bug in XmlSimple. When you have an empty
76
+ # tag that has an attribute, XmlSimple will not add the __content__ key
77
+ # to the returned hash. Thus, we check for the presense of the 'type'
78
+ # attribute to look for empty, typed tags, and simply return nil for
79
+ # their value.
80
+ elsif value.keys == %w(type)
81
+ nil
82
+ elsif value["nil"] == "true"
83
+ nil
84
+ # another special case, introduced by the latest rails, where an array
85
+ # type now exists. This is parsed by XmlSimple as a two-key hash, where
86
+ # one key is 'type' and the other is the actual array value.
87
+ elsif value.keys.length == 2 && value["type"] == "array"
88
+ value.delete("type")
89
+ typecast_value(value)
90
+ else
91
+ value.empty? ? nil : value.inject({}) do |h,(k,v)|
92
+ h[k] = typecast_value(v)
93
+ h
94
+ end
95
+ end
96
+ when Array
97
+ value.map! { |i| typecast_value(i) }
98
+ case value.length
99
+ when 0 then nil
100
+ when 1 then value.first
101
+ else value
102
+ end
103
+ else
104
+ raise "can't typecast #{value.inspect}"
105
+ end
106
+ end
107
+
108
+ def translate_entities(value)
109
+ value.gsub(/&lt;/, "<").
110
+ gsub(/&gt;/, ">").
111
+ gsub(/&quot;/, '"').
112
+ gsub(/&apos;/, "'").
113
+ gsub(/&amp;/, "&")
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,20 @@
1
+ module Basecamp; class Connection
2
+ def initialize(master)
3
+ @master = master
4
+ @connection = Net::HTTP.new(master.site, master.use_ssl ? 443 : 80)
5
+ @connection.use_ssl = master.use_ssl
6
+ @connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if master.use_ssl
7
+ end
8
+
9
+ def post(path, body, headers = {})
10
+ request = Net::HTTP::Post.new(path, headers.merge('Accept' => 'application/xml'))
11
+ request.basic_auth(@master.user, @master.password)
12
+ @connection.request(request, body)
13
+ end
14
+
15
+ def get(path, headers = {})
16
+ request = Net::HTTP::Get.new(path, headers.merge('Accept' => 'application/xml'))
17
+ request.basic_auth(@master.user, @master.password)
18
+ @connection.request(request)
19
+ end
20
+ end; end
@@ -0,0 +1,5 @@
1
+ class Hash
2
+ def to_legacy_xml
3
+ XmlSimple.xml_out({:request => self}, 'keeproot' => true, 'noattr' => true)
4
+ end
5
+ end
@@ -0,0 +1,56 @@
1
+ module Basecamp; class Record
2
+ attr_reader :type
3
+
4
+ def initialize(type, hash)
5
+ @type, @hash = type, hash
6
+ end
7
+
8
+ def [](name)
9
+ name = dashify(name)
10
+
11
+ case @hash[name]
12
+ when Hash then
13
+ @hash[name] = if (@hash[name].keys.length == 1 && @hash[name].values.first.is_a?(Array))
14
+ @hash[name].values.first.map { |v| Record.new(@hash[name].keys.first, v) }
15
+ else
16
+ Record.new(name, @hash[name])
17
+ end
18
+ else
19
+ @hash[name]
20
+ end
21
+ end
22
+
23
+ def id
24
+ @hash['id']
25
+ end
26
+
27
+ def attributes
28
+ @hash.keys
29
+ end
30
+
31
+ def respond_to?(sym)
32
+ super || @hash.has_key?(dashify(sym))
33
+ end
34
+
35
+ def method_missing(sym, *args)
36
+ if args.empty? && !block_given? && respond_to?(sym)
37
+ self[sym]
38
+ else
39
+ super
40
+ end
41
+ end
42
+
43
+ def to_s
44
+ "\#<Record(#{@type}) #{@hash.inspect[1..-2]}>"
45
+ end
46
+
47
+ def inspect
48
+ to_s
49
+ end
50
+
51
+ private
52
+
53
+ def dashify(name)
54
+ name.to_s.tr("_", "-")
55
+ end
56
+ end; end
@@ -0,0 +1,31 @@
1
+ module Basecamp; class Resource < ActiveResource::Base
2
+ class << self
3
+ def parent_resources(*parents)
4
+ @parent_resources = parents
5
+ end
6
+
7
+ def element_name
8
+ name.split(/::/).last.underscore
9
+ end
10
+
11
+ def prefix_source
12
+ if @parent_resources
13
+ @parent_resources.map { |resource| "/#{resource.to_s.pluralize}/:#{resource}_id" }.join + '/'
14
+ else
15
+ '/'
16
+ end
17
+ end
18
+
19
+ def prefix(options = {})
20
+ if options.any?
21
+ options.map { |name, value| "/#{name.to_s.chomp('_id').pluralize}/#{value}" }.join + '/'
22
+ else
23
+ '/'
24
+ end
25
+ end
26
+ end
27
+
28
+ def prefix_options
29
+ id ? {} : super
30
+ end
31
+ end; end
@@ -0,0 +1,36 @@
1
+ module Basecamp; class Attachment
2
+ attr_accessor :id, :filename, :content
3
+
4
+ def self.create(filename, content)
5
+ returning new(filename, content) do |attachment|
6
+ attachment.save
7
+ end
8
+ end
9
+
10
+ def initialize(filename, content)
11
+ @filename, @content = filename, content
12
+ end
13
+
14
+ def attributes
15
+ { :file => id, :original_filename => filename }
16
+ end
17
+
18
+ def to_xml(options = {})
19
+ { :file => attributes }.to_xml(options)
20
+ end
21
+
22
+ def inspect
23
+ to_s
24
+ end
25
+
26
+ def save
27
+ response = Basecamp.connection.post('/upload', content, 'Content-Type' => 'application/octet-stream')
28
+
29
+ if response.code == '200'
30
+ self.id = Hash.from_xml(response.body)['upload']['id']
31
+ true
32
+ else
33
+ raise "Could not save attachment: #{response.message} (#{response.code})"
34
+ end
35
+ end
36
+ end; end
@@ -0,0 +1,25 @@
1
+ # == Creating different types of categories
2
+ #
3
+ # The type parameter is required when creating a category. For exampe, to
4
+ # create an attachment category for a particular project:
5
+ #
6
+ # c = Basecamp::Category.new(:project_id => 1037)
7
+ # c.type = 'attachment'
8
+ # c.name = 'Pictures'
9
+ # c.save # => true
10
+ #
11
+ module Basecamp; class Category < Basecamp::Resource
12
+ parent_resources :project
13
+
14
+ def self.all(project_id, options = {})
15
+ find(:all, :params => options.merge(:project_id => project_id))
16
+ end
17
+
18
+ def self.post_categories(project_id, options = {})
19
+ find(:all, :params => options.merge(:project_id => project_id, :type => 'post'))
20
+ end
21
+
22
+ def self.attachment_categories(project_id, options = {})
23
+ find(:all, :params => options.merge(:project_id => project_id, :type => 'attachment'))
24
+ end
25
+ end; end
@@ -0,0 +1,22 @@
1
+ # == Creating comments for multiple resources
2
+ #
3
+ # Comments can be created for messages, milestones, and to-dos, identified
4
+ # by the <tt>post_id</tt>, <tt>milestone_id</tt>, and <tt>todo_item_id</tt>
5
+ # params respectively.
6
+ #
7
+ # For example, to create a comment on the message with id #8675309:
8
+ #
9
+ # c = Basecamp::Comment.new(:post_id => 8675309)
10
+ # c.body = 'Great tune'
11
+ # c.save # => true
12
+ #
13
+ # Similarly, to create a comment on a milestone:
14
+ #
15
+ # c = Basecamp::Comment.new(:milestone_id => 8473647)
16
+ # c.body = 'Is this done yet?'
17
+ # c.save # => true
18
+ #
19
+
20
+ module Basecamp; class Comment < Basecamp::Resource
21
+ parent_resources :post, :milestone, :todo_item
22
+ end; end
@@ -0,0 +1,7 @@
1
+ module Basecamp; class Company < Basecamp::Resource
2
+ parent_resources :project
3
+
4
+ def self.on_project(project_id, options = {})
5
+ find(:all, :params => options.merge(:project_id => project_id))
6
+ end
7
+ end; end
@@ -0,0 +1,30 @@
1
+ module Basecamp; class Message < Basecamp::Resource
2
+ parent_resources :project
3
+ set_element_name 'post'
4
+
5
+ # Returns the most recent 25 messages in the given project (and category,
6
+ # if specified). If you need to retrieve older messages, use the archive
7
+ # method instead. Example:
8
+ #
9
+ # Basecamp::Message.recent(1037)
10
+ # Basecamp::Message.recent(1037, :category_id => 7301)
11
+ #
12
+ def self.recent(project_id, options = {})
13
+ find(:all, :params => options.merge(:project_id => project_id))
14
+ end
15
+
16
+ # Returns a summary of all messages in the given project (and category, if
17
+ # specified). The summary is simply the title and category of the message,
18
+ # as well as the number of attachments (if any). Example:
19
+ #
20
+ # Basecamp::Message.archive(1037)
21
+ # Basecamp::Message.archive(1037, :category_id => 7301)
22
+ #
23
+ def self.archive(project_id, options = {})
24
+ find(:all, :params => options.merge(:project_id => project_id), :from => :archive)
25
+ end
26
+
27
+ def comments(options = {})
28
+ @comments ||= Comment.find(:all, :params => options.merge(:post_id => id))
29
+ end
30
+ end; end
@@ -0,0 +1,45 @@
1
+ module Basecamp; class Milestone
2
+ class << self
3
+ # Returns a list of all milestones for the given project, optionally filtered
4
+ # by whether they are completed, late, or upcoming.
5
+ def list(project_id, find = 'all')
6
+ Basecamp.records "milestone", "/projects/#{project_id}/milestones/list", :find => find
7
+ end
8
+
9
+ # Create a new milestone for the given project. +data+ must be hash of the
10
+ # values to set, including +title+, +deadline+, +responsible_party+, and
11
+ # +notify+.
12
+ def create(project_id, data)
13
+ create_milestones(project_id, [data]).first
14
+ end
15
+
16
+ # As #create_milestone, but can create multiple milestones in a single
17
+ # request. The +milestones+ parameter must be an array of milestone values as
18
+ # described in #create_milestone.
19
+ def create_milestones(project_id, milestones)
20
+ Basecamp.records "milestone", "/projects/#{project_id}/milestones/create", :milestone => milestones
21
+ end
22
+
23
+ # Updates an existing milestone.
24
+ def update(id, data, move = false, move_off_weekends = false)
25
+ Basecamp.record "/milestones/update/#{id}", :milestone => data,
26
+ :move_upcoming_milestones => move,
27
+ :move_upcoming_milestones_off_weekends => move_off_weekends
28
+ end
29
+
30
+ # Destroys the milestone with the given id.
31
+ def delete(id)
32
+ Basecamp.record "/milestones/delete/#{id}"
33
+ end
34
+
35
+ # Complete the milestone with the given id
36
+ def complete(id)
37
+ Basecamp.record "/milestones/complete/#{id}"
38
+ end
39
+
40
+ # Uncomplete the milestone with the given id
41
+ def uncomplete(id)
42
+ Basecamp.record "/milestones/uncomplete/#{id}"
43
+ end
44
+ end
45
+ end; end
@@ -0,0 +1,8 @@
1
+ module Basecamp; class Person < Basecamp::Resource
2
+ parent_resources :company, :projects
3
+
4
+ def self.me
5
+ hash = get(:me)
6
+ Basecamp::Person.new(hash)
7
+ end
8
+ end; end
@@ -0,0 +1,5 @@
1
+ module Basecamp; class Project < Basecamp::Resource
2
+ def time_entries(options = {})
3
+ @time_entries ||= TimeEntry.find(:all, :params => options.merge(:project_id => id))
4
+ end
5
+ end; end
@@ -0,0 +1,11 @@
1
+ module Basecamp; class TimeEntry < Basecamp::Resource
2
+ parent_resources :project, :todo_item
3
+
4
+ def self.all(project_id, page = 0)
5
+ find(:all, :params => { :project_id => project_id, :page => page })
6
+ end
7
+
8
+ def self.report(options={})
9
+ find(:all, :from => :report, :params => options)
10
+ end
11
+ end; end
@@ -0,0 +1,23 @@
1
+ module Basecamp; class TodoItem < Basecamp::Resource
2
+ parent_resources :todo_list
3
+
4
+ def todo_list(options = {})
5
+ @todo_list ||= TodoList.find(todo_list_id, options)
6
+ end
7
+
8
+ def time_entries(options = {})
9
+ @time_entries ||= TimeEntry.find(:all, :params => options.merge(:todo_item_id => id))
10
+ end
11
+
12
+ def comments(options = {})
13
+ @comments ||= Comment.find(:all, :params => options.merge(:todo_item_id => id))
14
+ end
15
+
16
+ def complete!
17
+ put(:complete)
18
+ end
19
+
20
+ def uncomplete!
21
+ put(:uncomplete)
22
+ end
23
+ end; end
@@ -0,0 +1,20 @@
1
+ module Basecamp; class TodoList < Basecamp::Resource
2
+ parent_resources :project
3
+
4
+ # Returns all lists for a project. If complete is true, only completed lists
5
+ # are returned. If complete is false, only uncompleted lists are returned.
6
+ def self.all(project_id, complete = nil)
7
+ filter = case complete
8
+ when nil then "all"
9
+ when true then "finished"
10
+ when false then "pending"
11
+ else raise ArgumentError, "invalid value for `complete'"
12
+ end
13
+
14
+ find(:all, :params => { :project_id => project_id, :filter => filter })
15
+ end
16
+
17
+ def todo_items(options = {})
18
+ @todo_items ||= TodoItem.find(:all, :params => options.merge(:todo_list_id => id))
19
+ end
20
+ end; end
@@ -0,0 +1,6 @@
1
+ # A minor hack to let Xml-Simple serialize symbolic keys in hashes
2
+ class Symbol
3
+ def [](*args)
4
+ to_s[*args]
5
+ end
6
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: basecamp
3
3
  version: !ruby/object:Gem::Version
4
- hash: 21
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 5
10
- version: 0.0.5
9
+ - 6
10
+ version: 0.0.6
11
11
  platform: ruby
12
12
  authors:
13
13
  - Anibal Cucco
@@ -134,6 +134,23 @@ extensions: []
134
134
  extra_rdoc_files: []
135
135
 
136
136
  files:
137
+ - lib/basecamp/base.rb
138
+ - lib/basecamp/connection.rb
139
+ - lib/basecamp/hash.rb
140
+ - lib/basecamp/record.rb
141
+ - lib/basecamp/resource.rb
142
+ - lib/basecamp/resources/attachment.rb
143
+ - lib/basecamp/resources/category.rb
144
+ - lib/basecamp/resources/comment.rb
145
+ - lib/basecamp/resources/company.rb
146
+ - lib/basecamp/resources/message.rb
147
+ - lib/basecamp/resources/milestone.rb
148
+ - lib/basecamp/resources/person.rb
149
+ - lib/basecamp/resources/project.rb
150
+ - lib/basecamp/resources/time_entry.rb
151
+ - lib/basecamp/resources/todo_item.rb
152
+ - lib/basecamp/resources/todo_list.rb
153
+ - lib/basecamp/symbol.rb
137
154
  - lib/basecamp.rb
138
155
  - README.rdoc
139
156
  has_rdoc: true