basecamp-classic 0.0.13

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e6218d06f92cc42fa10b9ce7a609f5222dbdb7788c0b392dc277509d641bbf70
4
+ data.tar.gz: 6542df68bb8d80b4d1cca63830f99fa21a6d90f8630dabf06f81f6445a96ea15
5
+ SHA512:
6
+ metadata.gz: '099c52ef38ca3c99005a985bc2fa029a9d827f973a457b80c919adc838567c32bbbc3d6508bb30bc459ed8ab199cfd1df175e01cd39f8791b8ab348da215a71d'
7
+ data.tar.gz: c93f318cd2923538e511a63bb08991913aca518881219108bef32fc9b8bd38c5d33077ec72bba2e4e541020a4d5637ad7fcf6a97ca3fd4fd70a0dc0ea7c18c86
data/README.rdoc ADDED
@@ -0,0 +1,278 @@
1
+ = A Ruby library for working with the Basecamp web-services API.
2
+
3
+ For more information about the Basecamp web-services API, visit:
4
+
5
+ http://developer.37signals.com/basecamp
6
+
7
+ You can find the original code in:
8
+
9
+ http://developer.37signals.com/basecamp/basecamp.rb
10
+
11
+ NOTE: not all of Basecamp's web-services are accessible via REST. This
12
+ library provides access to RESTful services via ActiveResource. Services not
13
+ yet upgraded to REST are accessed via the Basecamp class. Continue reading
14
+ for more details.
15
+
16
+ == Installation
17
+
18
+ Install the gem
19
+
20
+ gem install basecamp-classic
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
+
33
+ == Establishing a Connection
34
+
35
+ The first thing you need to do is establish a connection to Basecamp. This
36
+ requires your Basecamp site address and your login credentials or API token.
37
+
38
+ === Using username and password
39
+
40
+ Basecamp.establish_connection!('yoururl.basecamphq.com', 'username', 'password')
41
+
42
+ === Using API token (My Info -> Show your tokens)
43
+
44
+ Basecamp.establish_connection!('yoururl.basecamphq.com', 'APITOKEN', 'X')
45
+
46
+ === Using OAuth access token
47
+
48
+ Basecamp.establish_oauth_connection!('yoururl.basecamphq.com', 'oauth_access_token')
49
+
50
+ === Using SSL/Non SSL basecamp accounts (https://yoururl.basecamphq.com)
51
+
52
+ Basecamp uses SSL in all accounts so that's the default value here for establish_connection!. This will use https for the connection:
53
+
54
+ Basecamp.establish_connection!('yoururl.basecamphq.com', 'APITOKEN', 'X')
55
+
56
+ Basecamp.establish_oauth_connection!('yoururl.basecamphq.com', 'oauth_access_token')
57
+
58
+ But if for some reason your basecamp account doesn't use SSL, you can disable with:
59
+
60
+ Basecamp.establish_connection!('yoururl.basecamphq.com', 'APITOKEN', 'X', false)
61
+
62
+ Basecamp.establish_oauth_connection!('yoururl.basecamphq.com', 'oauth_access_token', false)
63
+
64
+ This is the same whether you're accessing using the ActiveResource interface,
65
+ or the legacy interface.
66
+
67
+ == Using the REST interface via ActiveResource
68
+
69
+ The REST interface is accessed via ActiveResource, a popular Ruby library
70
+ that implements object-relational mapping for REST web-services. For more
71
+ information on working with ActiveResource, see:
72
+
73
+ * http://api.rubyonrails.org/files/activeresource/README.html
74
+ * http://api.rubyonrails.org/classes/ActiveResource/Base.html
75
+
76
+ == Basecamp API rate limit and app identification
77
+
78
+ If your app creates a lot of requests, you may hit the (new) basecamp rate limit. ({read more here about rate limiting}[https://github.com/basecamp/basecamp-classic-api#rate-limiting]).
79
+
80
+ To raise your limit you can send a custom user-agent to basecamp with your identifying information ({read more here}[https://github.com/basecamp/basecamp-classic-api#rate-limiting]).
81
+
82
+ For this you need to create an initializer in your rails application, for example: +config/inititalizer/basecamp_user_agent.rb+ with the following line:
83
+
84
+ ActiveResource::Base.headers["User-Agent"] = "Fabian's Ingenious Integration (fabian@example.com)"
85
+
86
+ This initializer ensures that all requests made through ActiveResource (which this gem uses) are identifiable with your information.
87
+
88
+ **NOTE**: If you are using ActiveResource for accessing multiple APIs at once and not only basecamp's, you will need the following initializer instead:
89
+
90
+ Basecamp::Resource.headers["User-Agent"] = "Fabian's Ingenious Integration (fabian@example.com)"
91
+
92
+ === Finding a Resource
93
+
94
+ Find a specific resource using the +find+ method. Attributes of the resource
95
+ are available as instance methods on the resulting object. For example, to
96
+ find a message with the ID of 8675309 and access its title attribute, you
97
+ would do the following:
98
+
99
+ m = Basecamp::Message.find(8675309)
100
+ m.title # => 'Jenny'
101
+
102
+ To find all messages for a given project, use find(:all), passing the
103
+ project_id as a parameter to find. Example:
104
+
105
+ messages = Basecamp::Message.find(:all, params => { :project_id => 1037 })
106
+ messages.size # => 25
107
+
108
+ To get the current logged in user:
109
+
110
+ Basecamp::Person.me
111
+
112
+ 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:
113
+
114
+ Basecamp::Person.me.token
115
+
116
+ To get all people by company:
117
+
118
+ Basecamp::Person.find(:all, :params => {:company_id => company.id})
119
+
120
+ To get all people by project:
121
+
122
+ Basecamp::Person.find(:all, :params => {:project_id => project.id})
123
+
124
+ === Creating a Resource
125
+
126
+ Create a resource by making a new instance of that resource, setting its
127
+ attributes, and saving it. If the resource requires a prefix to identify
128
+ it (as is the case with resources that belong to a sub-resource, such as a
129
+ project), it should be specified when instantiating the object. Examples:
130
+
131
+ m = Basecamp::Message.new(:project_id => 1037)
132
+ m.category_id = 7301
133
+ m.title = 'Message in a bottle'
134
+ m.body = 'Another lonely day, with no one here but me'
135
+ m.save # => true
136
+
137
+ c = Basecamp::Comment.new(:post_id => 25874)
138
+ c.body = 'Did you get those TPS reports?'
139
+ c.save # => true
140
+
141
+ You can also create a resource using the +create+ method, which will create
142
+ and save it in one step. Example:
143
+
144
+ Basecamp::TodoItem.create(:todo_list_id => 3422, :contents => 'Do it')
145
+
146
+
147
+ === Updating a Resource
148
+
149
+ To update a resource, first find it by its id, change its attributes, and
150
+ save it. Example:
151
+
152
+ m = Basecamp::Message.find(8675309)
153
+ m.body = 'Changed'
154
+ m.save # => true
155
+
156
+
157
+ === Deleting a Resource
158
+
159
+ To delete a resource, use the +delete+ method with the ID of the resource
160
+ you want to delete. Example:
161
+
162
+ Basecamp::Message.delete(1037)
163
+
164
+
165
+ === Attaching Files to a Resource
166
+
167
+ If the resource accepts file attachments, the +attachments+ parameter should
168
+ be an array of Basecamp::Attachment objects. Example:
169
+
170
+ f1 = File.open('primary.doc')
171
+ s1 = StringIO.new('a string')
172
+
173
+ a1 = Basecamp::Attachment.create('primary', f1)
174
+ a2 = Basecamp::Attachment.create('another', s1))
175
+
176
+ m = Basecamp::Message.new(:project_id => 1037)
177
+ ...
178
+ m.attachments = [a1, a2]
179
+ m.save # => true
180
+
181
+ f1.close
182
+
183
+ === Milestones
184
+
185
+ Is not implemented as an active resource, it uses the non-REST interface.
186
+ Browse the source (lib/basecamp/resources/milestone) to see all available methods.
187
+
188
+ For example, to list all milestones in a project:
189
+
190
+ milestones = Basecamp::Milestone.list(1037)
191
+ milestones.first.title # => "The Milestone"
192
+
193
+ === More about Todo Items
194
+
195
+ To access all todo items in a todo list:
196
+
197
+ Basecamp::TodoItem.find(:all, :params => { :todo_list_id => 3422 })
198
+
199
+ You can't access all todo items in a project with a single API call.
200
+ So you have to do something like this:
201
+
202
+ def todo_items_on_project(project_id)
203
+ todo_items = []
204
+ todo_lists = TodoList.find(:all, :params => { :project_id => project_id })
205
+ todo_lists.each do |todo_list|
206
+ todo_items += TodoItem.find(:all, :params => { :todo_list_id => todo_list.id })
207
+ end
208
+ todo_items
209
+ end
210
+
211
+ === Time entries
212
+
213
+ There are two ways to create time-entries
214
+
215
+ # TodoItem as Parent Resource
216
+ Basecamp::TimeEntry.create(:todo_item_id => item_id, :date => date, :person_id => person_id, :hours => hours, :description => '')
217
+ # Project as Parent Resource
218
+ Basecamp::TimeEntry.create(:project_id => project_id, :date => date, :person_id => person_id, :hours => hours, :description => '')
219
+
220
+
221
+ = Using the non-REST interface
222
+
223
+ You can access other resources not included in this wrapper yet using "record" and "records".
224
+
225
+ person = Basecamp.record("/contacts/person/93832")
226
+ person.first_name # => "Jason"
227
+
228
+ people_in_company = Basecamp.records("person", "/contacts/people/85")
229
+ people_in_company.first.first_name # => "Jason"
230
+
231
+ = Using json as the default format
232
+
233
+ By default the wrapper will use :xml for the active record connection but you can set :json as the default format:
234
+
235
+ Basecamp.establish_connection!('yoururl.basecamphq.com', 'APITOKEN', 'X', true, false)
236
+
237
+ Note: We recommend using xml. There are some API calls that don't behave well with json.
238
+
239
+ = Access active resource response object
240
+
241
+ You can acces the last response object:
242
+
243
+ Basecamp::Message.find(:all, params => { :project_id => 1037 })
244
+ Basecamp::Message.connection.response["status"] # => "200 OK"
245
+
246
+ = Using the raw response body
247
+
248
+ This is useful for example to get pagination data to access all comments in a commentable resource: https://github.com/37signals/basecamp-classic-api/blob/master/sections/comments.md#get-recent-comments-for-a-commentable-resource
249
+
250
+ def get_threshold
251
+ # Get the last response object
252
+ response = Basecamp::Comment.connection.response
253
+ # Parse the xml
254
+ xml = XmlSimple.xml_in(response.body)
255
+ # continued-at is an attribute specifying the path where the next oldest 75 comments can be retrieved
256
+ if continued_at = xml["continued-at"]
257
+ # There are more comments
258
+ # We need to extract the threshold parameter from the continued-at url
259
+ hash = CGI::parse(URI.parse(continued_at).query)
260
+ hash["threshold"].first
261
+ else
262
+ # We're done
263
+ nil
264
+ end
265
+ end
266
+
267
+ comments = Basecamp::Comment.find(:all, :params => { :post_id => 1037 })
268
+ if threshold = get_threshold
269
+ # Get the next set of comments using the threshold
270
+ Basecamp::Comment.find(:all, :params => { :post_id => 1037, :threshold => threshold })
271
+ end
272
+
273
+ == Contributors
274
+
275
+ * jamesarosen
276
+ * defeated
277
+ * justinbarry
278
+ * fmiopensource
@@ -0,0 +1,23 @@
1
+ # ActiveResource connection patch to let users access the last response object and the headers.
2
+ #
3
+ # Example:
4
+ # >> Basecamp::Message.find(:all, params => { :project_id => 1037 })
5
+ # >> Basecamp::Message.connection.response["status"]
6
+ # => "200 OK"
7
+ class ActiveResource::Connection
8
+ alias_method :original_handle_response, :handle_response
9
+ alias :static_default_header :default_header
10
+
11
+ def handle_response(response)
12
+ Thread.current[:active_resource_connection_headers] = response
13
+ original_handle_response(response)
14
+ end
15
+
16
+ def response
17
+ Thread.current[:active_resource_connection_headers]
18
+ end
19
+
20
+ def set_header(key, value)
21
+ default_header.update(key => value)
22
+ end
23
+ end
@@ -0,0 +1,142 @@
1
+ module Basecamp
2
+ class << self
3
+ attr_accessor :use_xml
4
+ attr_reader :site, :user, :password, :use_ssl, :use_oauth, :access_token
5
+
6
+ def establish_connection!(site, user, password, use_ssl = true, use_xml = true)
7
+ @site = site
8
+ @user = user
9
+ @password = password
10
+ @use_ssl = use_ssl
11
+ @use_xml = use_xml
12
+ @use_oauth = false
13
+
14
+ Resource.user = user
15
+ Resource.password = password
16
+ Resource.site = (use_ssl ? "https" : "http") + "://" + site
17
+ Resource.format = (use_xml ? :xml : :json)
18
+
19
+ @connection = Connection.new(self)
20
+ end
21
+
22
+ def establish_oauth_connection!(site, access_token, use_ssl = true, use_xml = true)
23
+ @site = site
24
+ @use_ssl = use_ssl
25
+ @use_xml = use_xml
26
+ @use_oauth = true
27
+ @access_token = access_token
28
+
29
+ Resource.site = (use_ssl ? "https" : "http") + "://" + site
30
+ Resource.format = (use_xml ? :xml : :json)
31
+ Resource.connection.set_header('Authorization', "Bearer #{access_token}")
32
+
33
+ @connection = Connection.new(self)
34
+ end
35
+
36
+ def connection
37
+ @connection || raise('No connection established')
38
+ end
39
+
40
+ # Make a raw web-service request to Basecamp. This will return a Hash of
41
+ # Arrays of the response, and may seem a little odd to the uninitiated.
42
+ def request(path, parameters = {})
43
+ headers = { "Content-Type" => content_type }
44
+ headers.merge!('Authorization' => "Bearer #{@access_token}") if @use_oauth
45
+ headers.merge!(Resource.headers) if Resource.headers.present?
46
+
47
+ if parameters.empty?
48
+ response = Basecamp.connection.get(path, headers)
49
+ else
50
+ response = Basecamp.connection.post(path, StringIO.new(convert_body(parameters)), headers)
51
+ end
52
+
53
+ if response.code.to_i / 100 == 2
54
+ return {} if response.body.blank?
55
+ result = XmlSimple.xml_in(response.body, 'keeproot' => true, 'contentkey' => '__content__', 'forcecontent' => true)
56
+ typecast_value(result)
57
+ else
58
+ raise "#{response.message} (#{response.code})"
59
+ end
60
+ end
61
+
62
+ # A convenience method for wrapping the result of a query in a Record
63
+ # object. This assumes that the result is a singleton, not a collection.
64
+ def record(path, parameters={})
65
+ result = request(path, parameters)
66
+ (result && !result.empty?) ? Record.new(result.keys.first, result.values.first) : nil
67
+ end
68
+
69
+ # A convenience method for wrapping the result of a query in Record
70
+ # objects. This assumes that the result is a collection--any singleton
71
+ # result will be wrapped in an array.
72
+ def records(node, path, parameters={})
73
+ result = request(path, parameters).values.first or return []
74
+ result = result[node] or return []
75
+ result = [result] unless Array === result
76
+ result.map { |row| Record.new(node, row) }
77
+ end
78
+
79
+ private
80
+
81
+ def convert_body(body)
82
+ body = use_xml ? body.to_legacy_xml : body.to_yaml
83
+ end
84
+
85
+ def content_type
86
+ use_xml ? "application/xml" : "application/x-yaml"
87
+ end
88
+
89
+ def typecast_value(value)
90
+ case value
91
+ when Hash
92
+ if value.has_key?("__content__")
93
+ content = translate_entities(value["__content__"]).strip
94
+ case value["type"]
95
+ when "integer" then content.to_i
96
+ when "boolean" then content == "true"
97
+ when "datetime" then Time.parse(content)
98
+ when "date" then Date.parse(content)
99
+ else content
100
+ end
101
+ # a special case to work-around a bug in XmlSimple. When you have an empty
102
+ # tag that has an attribute, XmlSimple will not add the __content__ key
103
+ # to the returned hash. Thus, we check for the presense of the 'type'
104
+ # attribute to look for empty, typed tags, and simply return nil for
105
+ # their value.
106
+ elsif value.keys == %w(type)
107
+ nil
108
+ elsif value["nil"] == "true"
109
+ nil
110
+ # another special case, introduced by the latest rails, where an array
111
+ # type now exists. This is parsed by XmlSimple as a two-key hash, where
112
+ # one key is 'type' and the other is the actual array value.
113
+ elsif value.keys.length == 2 && value["type"] == "array"
114
+ value.delete("type")
115
+ typecast_value(value)
116
+ else
117
+ value.empty? ? nil : value.inject({}) do |h,(k,v)|
118
+ h[k] = typecast_value(v)
119
+ h
120
+ end
121
+ end
122
+ when Array
123
+ value.map! { |i| typecast_value(i) }
124
+ case value.length
125
+ when 0 then nil
126
+ when 1 then value.first
127
+ else value
128
+ end
129
+ else
130
+ raise "can't typecast #{value.inspect}"
131
+ end
132
+ end
133
+
134
+ def translate_entities(value)
135
+ value.gsub(/&lt;/, "<").
136
+ gsub(/&gt;/, ">").
137
+ gsub(/&quot;/, '"').
138
+ gsub(/&apos;/, "'").
139
+ gsub(/&amp;/, "&")
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,22 @@
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, iostream, headers = {})
10
+ request = Net::HTTP::Post.new(path, headers.merge('Accept' => 'application/xml'))
11
+ request.basic_auth(@master.user, @master.password) unless @master.use_oauth
12
+ request.body_stream = iostream
13
+ request.content_length = iostream.size
14
+ @connection.request(request)
15
+ end
16
+
17
+ def get(path, headers = {})
18
+ request = Net::HTTP::Get.new(path, headers.merge('Accept' => 'application/xml'))
19
+ request.basic_auth(@master.user, @master.password) unless @master.use_oauth
20
+ @connection.request(request)
21
+ end
22
+ 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,42 @@
1
+ module Basecamp; class Resource < ActiveResource::Base
2
+ class << self
3
+ def parent_resources(*parents)
4
+ @parent_resources = parents
5
+ end
6
+
7
+ def prefix_source
8
+ if @parent_resources
9
+ @parent_resources.map { |resource| "/#{resource.to_s.pluralize}/:#{resource}_id" }.join + '/'
10
+ else
11
+ '/'
12
+ end
13
+ end
14
+
15
+ def check_prefix_options(options)
16
+ end
17
+
18
+ def prefix(options = {})
19
+ if options.any?
20
+ options.map { |name, value| "/#{name.to_s.chomp('_id').pluralize}/#{value}" }.join + '/'
21
+ else
22
+ '/'
23
+ end
24
+ end
25
+
26
+ def all(options = {})
27
+ find(:all, options)
28
+ end
29
+
30
+ def first(options = {})
31
+ find(:first, options)
32
+ end
33
+
34
+ def last(options = {})
35
+ find(:last, options)
36
+ end
37
+ end
38
+
39
+ def prefix_options
40
+ id ? {} : super
41
+ end
42
+ end; end
@@ -0,0 +1,36 @@
1
+ module Basecamp; class Attachment
2
+ attr_accessor :id, :filename, :content, :category_id
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, :category_id => category_id }.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,25 @@
1
+ module Basecamp; class TodoItem < Basecamp::Resource
2
+ def todo_list(options = {})
3
+ @todo_list ||= TodoList.find(todo_list_id, options)
4
+ end
5
+
6
+ def time_entries(options = {})
7
+ @time_entries ||= TimeEntry.find(:all, :params => options.merge(:todo_item_id => id))
8
+ end
9
+
10
+ def comments(options = {})
11
+ @comments ||= Comment.find(:all, :params => options.merge(:todo_item_id => id))
12
+ end
13
+
14
+ def complete!
15
+ put(:complete)
16
+ end
17
+
18
+ def uncomplete!
19
+ put(:uncomplete)
20
+ end
21
+
22
+ def prefix_options
23
+ { :todo_list_id => todo_list_id }
24
+ end
25
+ end; end
@@ -0,0 +1,26 @@
1
+ module Basecamp; class TodoList < Basecamp::Resource
2
+ # Returns all lists for a project. If complete is true, only completed lists
3
+ # are returned. If complete is false, only uncompleted lists are returned.
4
+ def self.all(project_id, complete = nil)
5
+ filter = case complete
6
+ when nil then "all"
7
+ when true then "finished"
8
+ when false then "pending"
9
+ else raise ArgumentError, "invalid value for `complete'"
10
+ end
11
+
12
+ find(:all, :params => { :project_id => project_id, :filter => filter })
13
+ end
14
+
15
+ def project
16
+ @project ||= Project.find(project_id)
17
+ end
18
+
19
+ def todo_items(options = {})
20
+ @todo_items ||= TodoItem.find(:all, :params => options.merge(:todo_list_id => id))
21
+ end
22
+
23
+ def prefix_options
24
+ { :project_id => project_id }
25
+ end
26
+ 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
data/lib/basecamp.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'net/https'
2
+ require 'yaml'
3
+ require 'date'
4
+ require 'time'
5
+ require 'active_resource'
6
+ require 'xmlsimple'
7
+
8
+ require 'basecamp/base'
9
+ require 'basecamp/connection'
10
+ require 'basecamp/active_resource'
11
+ require 'basecamp/hash'
12
+ require 'basecamp/record'
13
+ require 'basecamp/resource'
14
+ require 'basecamp/symbol'
15
+ require 'basecamp/resources/attachment'
16
+ require 'basecamp/resources/category'
17
+ require 'basecamp/resources/comment'
18
+ require 'basecamp/resources/company'
19
+ require 'basecamp/resources/message'
20
+ require 'basecamp/resources/milestone'
21
+ require 'basecamp/resources/person'
22
+ require 'basecamp/resources/project'
23
+ require 'basecamp/resources/time_entry'
24
+ require 'basecamp/resources/todo_item'
25
+ require 'basecamp/resources/todo_list'
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: basecamp-classic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.13
5
+ platform: ruby
6
+ authors:
7
+ - Anibal Cucco
8
+ - James A. Rosen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2026-03-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: oauth2
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: xml-simple
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: activeresource
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 2.3.0
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 2.3.0
56
+ - !ruby/object:Gem::Dependency
57
+ name: rake
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: mg
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 0.0.8
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 0.0.8
84
+ - !ruby/object:Gem::Dependency
85
+ name: rspec
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: 1.3.0
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: 1.3.0
98
+ - !ruby/object:Gem::Dependency
99
+ name: webmock
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: 1.2.2
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: 1.2.2
112
+ description: Basecamp API wrapper.
113
+ email: nobody@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - README.rdoc
119
+ - lib/basecamp.rb
120
+ - lib/basecamp/active_resource.rb
121
+ - lib/basecamp/base.rb
122
+ - lib/basecamp/connection.rb
123
+ - lib/basecamp/hash.rb
124
+ - lib/basecamp/record.rb
125
+ - lib/basecamp/resource.rb
126
+ - lib/basecamp/resources/attachment.rb
127
+ - lib/basecamp/resources/category.rb
128
+ - lib/basecamp/resources/comment.rb
129
+ - lib/basecamp/resources/company.rb
130
+ - lib/basecamp/resources/message.rb
131
+ - lib/basecamp/resources/milestone.rb
132
+ - lib/basecamp/resources/person.rb
133
+ - lib/basecamp/resources/project.rb
134
+ - lib/basecamp/resources/time_entry.rb
135
+ - lib/basecamp/resources/todo_item.rb
136
+ - lib/basecamp/resources/todo_list.rb
137
+ - lib/basecamp/symbol.rb
138
+ homepage: http://github.com/anibalcucco/basecamp-wrapper
139
+ licenses: []
140
+ metadata: {}
141
+ post_install_message:
142
+ rdoc_options: []
143
+ require_paths:
144
+ - lib
145
+ required_ruby_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ required_rubygems_version: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ requirements: []
156
+ rubygems_version: 3.5.22
157
+ signing_key:
158
+ specification_version: 4
159
+ summary: Basecamp API wrapper.
160
+ test_files: []