basecamp 0.0.5 → 0.0.6

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