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 +7 -0
- data/README.rdoc +278 -0
- data/lib/basecamp/active_resource.rb +23 -0
- data/lib/basecamp/base.rb +142 -0
- data/lib/basecamp/connection.rb +22 -0
- data/lib/basecamp/hash.rb +5 -0
- data/lib/basecamp/record.rb +56 -0
- data/lib/basecamp/resource.rb +42 -0
- data/lib/basecamp/resources/attachment.rb +36 -0
- data/lib/basecamp/resources/category.rb +25 -0
- data/lib/basecamp/resources/comment.rb +22 -0
- data/lib/basecamp/resources/company.rb +7 -0
- data/lib/basecamp/resources/message.rb +30 -0
- data/lib/basecamp/resources/milestone.rb +45 -0
- data/lib/basecamp/resources/person.rb +8 -0
- data/lib/basecamp/resources/project.rb +5 -0
- data/lib/basecamp/resources/time_entry.rb +11 -0
- data/lib/basecamp/resources/todo_item.rb +25 -0
- data/lib/basecamp/resources/todo_list.rb +26 -0
- data/lib/basecamp/symbol.rb +6 -0
- data/lib/basecamp.rb +25 -0
- metadata +160 -0
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(/</, "<").
|
|
136
|
+
gsub(/>/, ">").
|
|
137
|
+
gsub(/"/, '"').
|
|
138
|
+
gsub(/'/, "'").
|
|
139
|
+
gsub(/&/, "&")
|
|
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,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,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,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
|
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: []
|