turingstudio-basecamp-rb 0.0.1
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/History.txt +4 -0
- data/README.rdoc +89 -0
- data/lib/basecamp.rb +31 -0
- data/lib/basecamp/attachment.rb +53 -0
- data/lib/basecamp/base.rb +210 -0
- data/lib/basecamp/comment.rb +23 -0
- data/lib/basecamp/connection.rb +16 -0
- data/lib/basecamp/message.rb +32 -0
- data/lib/basecamp/record.rb +58 -0
- data/lib/basecamp/resource.rb +29 -0
- data/lib/basecamp/time_entry.rb +17 -0
- data/lib/basecamp/todoitem.rb +25 -0
- data/lib/basecamp/todolist.rb +22 -0
- data/lib/basecamp/version.rb +3 -0
- data/spec/basecamp_spec.rb +61 -0
- data/spec/lib/basecamp_base.rb +75 -0
- data/spec/lib/basecamp_message_spec.rb +42 -0
- data/spec/lib/basecamp_todo_item_spec.rb +40 -0
- data/spec/lib/basecamp_todo_list_spec.rb +32 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +7 -0
- data/tasks/rspec.rake +33 -0
- metadata +94 -0
data/History.txt
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
= basecamp-rb
|
2
|
+
|
3
|
+
http://github.com/turingstudio/basecamp-rb/
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
A Ruby gem for working with the Basecamp web-services API.
|
8
|
+
|
9
|
+
== FEATURES/PROBLEMS:
|
10
|
+
|
11
|
+
|
12
|
+
== SYNOPSIS:
|
13
|
+
|
14
|
+
The first thing you need to do is establish a connection to Basecamp. This
|
15
|
+
requires your Basecamp site address and your login credentials. Example:
|
16
|
+
Basecamp::Base.establish_connection!('your.grouphub.com', 'login', 'password')
|
17
|
+
|
18
|
+
m = Basecamp::Message.find(8675309)
|
19
|
+
m.title # => 'Jenny'
|
20
|
+
|
21
|
+
>> Creating a Resource
|
22
|
+
|
23
|
+
m = Basecamp::Message.new(:project_id => 1037)
|
24
|
+
m.category_id = 7301
|
25
|
+
m.title = 'Message in a bottle'
|
26
|
+
m.body = 'Another lonely day, with no one here but me'
|
27
|
+
m.save # => true
|
28
|
+
|
29
|
+
>> Updating a Resource
|
30
|
+
|
31
|
+
m = Basecamp::Message.find(8675309)
|
32
|
+
m.body = 'Changed'
|
33
|
+
m.save # => true
|
34
|
+
|
35
|
+
>> Deleting a Resource
|
36
|
+
|
37
|
+
Basecamp::Message.delete(1037)
|
38
|
+
|
39
|
+
>> Using the non-REST inteface
|
40
|
+
|
41
|
+
# The non-REST interface is accessed via instance methods on the Basecamp
|
42
|
+
# class. Ensure you've established a connection, then create a new Basecamp
|
43
|
+
# instance and call methods on it. Examples:
|
44
|
+
|
45
|
+
basecamp = Basecamp::Base.new
|
46
|
+
|
47
|
+
basecamp.projects.length # => 5
|
48
|
+
basecamp.person(93832) # => #<Record(person)..>
|
49
|
+
basecamp.file_categories(123) # => [#<Record(file-category)>,#<Record..>]
|
50
|
+
|
51
|
+
# Object attributes are accessible as methods. Example:
|
52
|
+
|
53
|
+
person = basecamp.person(93832)
|
54
|
+
person.first_name # => "Jason"
|
55
|
+
|
56
|
+
|
57
|
+
== REQUIREMENTS:
|
58
|
+
|
59
|
+
xml-simple >= 1.0.11
|
60
|
+
activeresource >= 2.2.2
|
61
|
+
|
62
|
+
== INSTALL:
|
63
|
+
|
64
|
+
gem install turingstudio-basecamp-rb
|
65
|
+
|
66
|
+
== LICENSE:
|
67
|
+
|
68
|
+
(The MIT License)
|
69
|
+
|
70
|
+
Copyright (c) 2008 The Turing Studio, Inc.
|
71
|
+
|
72
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
73
|
+
a copy of this software and associated documentation files (the
|
74
|
+
'Software'), to deal in the Software without restriction, including
|
75
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
76
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
77
|
+
permit persons to whom the Software is furnished to do so, subject to
|
78
|
+
the following conditions:
|
79
|
+
|
80
|
+
The above copyright notice and this permission notice shall be
|
81
|
+
included in all copies or substantial portions of the Software.
|
82
|
+
|
83
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
84
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
85
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
86
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
87
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
88
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
89
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/lib/basecamp.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'date'
|
5
|
+
require 'time'
|
6
|
+
require 'xmlsimple'
|
7
|
+
require 'activeresource'
|
8
|
+
|
9
|
+
require 'basecamp/base'
|
10
|
+
require 'basecamp/resource'
|
11
|
+
require 'basecamp/attachment'
|
12
|
+
require 'basecamp/comment'
|
13
|
+
require 'basecamp/connection'
|
14
|
+
require 'basecamp/message'
|
15
|
+
require 'basecamp/record'
|
16
|
+
require 'basecamp/time_entry'
|
17
|
+
require 'basecamp/todoitem'
|
18
|
+
require 'basecamp/todolist'
|
19
|
+
|
20
|
+
# A minor hack to let Xml-Simple serialize symbolic keys in hashes
|
21
|
+
class Symbol
|
22
|
+
def [](*args)
|
23
|
+
to_s[*args]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Hash
|
28
|
+
def to_legacy_xml
|
29
|
+
XmlSimple.xml_out({:request => self}, 'keeproot' => true, 'noattr' => true)
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Basecamp
|
2
|
+
class Attachment
|
3
|
+
|
4
|
+
# === Attaching Files to a Resource
|
5
|
+
#
|
6
|
+
# If the resource accepts file attachments, the +attachments+ parameter should
|
7
|
+
# be an array of Basecamp::Attachment objects. Example:
|
8
|
+
#
|
9
|
+
# a1 = Basecamp::Atachment.create('primary', File.read('primary.doc'))
|
10
|
+
# a2 = Basecamp::Atachment.create('another', File.read('another.doc'))
|
11
|
+
#
|
12
|
+
# m = Basecamp::Message.new(:project_id => 1037)
|
13
|
+
# ...
|
14
|
+
# m.attachments = [a1, a2]
|
15
|
+
# m.save # => true
|
16
|
+
#
|
17
|
+
|
18
|
+
attr_accessor :id, :filename, :content, :content_type
|
19
|
+
|
20
|
+
def self.create(filename, content)
|
21
|
+
returning new(filename, content) do |attachment|
|
22
|
+
attachment.save
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(filename, content, content_type = 'application/octet-stream')
|
27
|
+
@filename, @content, @content_type = filename, content, content_type
|
28
|
+
end
|
29
|
+
|
30
|
+
def attributes
|
31
|
+
{ :file => id, :original_filename => filename, :content_type => content_type }
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_xml(options = {})
|
35
|
+
{ :file => attributes }.to_xml(options)
|
36
|
+
end
|
37
|
+
|
38
|
+
def inspect
|
39
|
+
to_s
|
40
|
+
end
|
41
|
+
|
42
|
+
def save
|
43
|
+
response = Basecamp::Base.connection.post('/upload', content, 'Content-Type' => content_type)
|
44
|
+
|
45
|
+
if response.code == '200'
|
46
|
+
self.id = Hash.from_xml(response.body)['upload']['id']
|
47
|
+
true
|
48
|
+
else
|
49
|
+
raise "Could not save attachment: #{response.message} (#{response.code})"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
module Basecamp
|
2
|
+
class Base
|
3
|
+
attr_accessor :use_xml
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_reader :site, :user, :password, :use_ssl
|
7
|
+
|
8
|
+
def establish_connection!(site, user, password, use_ssl = false)
|
9
|
+
@site = site
|
10
|
+
@user = user
|
11
|
+
@password = password
|
12
|
+
@use_ssl = use_ssl
|
13
|
+
Resource.user = user
|
14
|
+
Resource.password = password
|
15
|
+
Resource.site = (use_ssl ? "https" : "http") + "://" + site
|
16
|
+
@connection = Connection.new(self)
|
17
|
+
end
|
18
|
+
|
19
|
+
def connection
|
20
|
+
@connection || raise('No connection established')
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
@use_xml = false
|
27
|
+
end
|
28
|
+
|
29
|
+
# ==========================================================================
|
30
|
+
# GENERAL
|
31
|
+
# ==========================================================================
|
32
|
+
|
33
|
+
# Return the list of all accessible projects
|
34
|
+
def projects
|
35
|
+
records "project", "/project/list"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the list of message categories for the given project
|
39
|
+
def message_categories(project_id)
|
40
|
+
records "post-category", "/projects/#{project_id}/post_categories"
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the list of file categories for the given project
|
44
|
+
def file_categories(project_id)
|
45
|
+
records "attachment-category", "/projects/#{project_id}/attachment_categories"
|
46
|
+
end
|
47
|
+
|
48
|
+
# ==========================================================================
|
49
|
+
# CONTACT MANAGEMENT
|
50
|
+
# ==========================================================================
|
51
|
+
|
52
|
+
# Return information for the company with the given id
|
53
|
+
def company(id)
|
54
|
+
record "/contacts/company/#{id}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Return an array of the people in the given company. If the project-id is
|
58
|
+
# given, only people who have access to the given project will be returned.
|
59
|
+
def people(company_id, project_id=nil)
|
60
|
+
url = project_id ? "/projects/#{project_id}" : ""
|
61
|
+
url << "/contacts/people/#{company_id}"
|
62
|
+
records "person", url
|
63
|
+
end
|
64
|
+
|
65
|
+
# Return information about the person with the given id
|
66
|
+
def person(id)
|
67
|
+
record "/contacts/person/#{id}"
|
68
|
+
end
|
69
|
+
|
70
|
+
# ==========================================================================
|
71
|
+
# MILESTONES
|
72
|
+
# ==========================================================================
|
73
|
+
|
74
|
+
# Complete the milestone with the given id
|
75
|
+
def complete_milestone(id)
|
76
|
+
record "/milestones/complete/#{id}"
|
77
|
+
end
|
78
|
+
|
79
|
+
# Create a new milestone for the given project. +data+ must be hash of the
|
80
|
+
# values to set, including +title+, +deadline+, +responsible_party+, and
|
81
|
+
# +notify+.
|
82
|
+
def create_milestone(project_id, data)
|
83
|
+
create_milestones(project_id, [data]).first
|
84
|
+
end
|
85
|
+
|
86
|
+
# As #create_milestone, but can create multiple milestones in a single
|
87
|
+
# request. The +milestones+ parameter must be an array of milestone values as
|
88
|
+
# descrbed in #create_milestone.
|
89
|
+
def create_milestones(project_id, milestones)
|
90
|
+
records "milestone", "/projects/#{project_id}/milestones/create", :milestone => milestones
|
91
|
+
end
|
92
|
+
|
93
|
+
# Destroys the milestone with the given id.
|
94
|
+
def delete_milestone(id)
|
95
|
+
record "/milestones/delete/#{id}"
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns a list of all milestones for the given project, optionally filtered
|
99
|
+
# by whether they are completed, late, or upcoming.
|
100
|
+
def milestones(project_id, find="all")
|
101
|
+
records "milestone", "/projects/#{project_id}/milestones/list", :find => find
|
102
|
+
end
|
103
|
+
|
104
|
+
# Uncomplete the milestone with the given id
|
105
|
+
def uncomplete_milestone(id)
|
106
|
+
record "/milestones/uncomplete/#{id}"
|
107
|
+
end
|
108
|
+
|
109
|
+
# Updates an existing milestone.
|
110
|
+
def update_milestone(id, data, move=false, move_off_weekends=false)
|
111
|
+
record "/milestones/update/#{id}", :milestone => data,
|
112
|
+
:move_upcoming_milestones => move,
|
113
|
+
:move_upcoming_milestones_off_weekends => move_off_weekends
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
# Make a raw web-service request to Basecamp. This will return a Hash of
|
119
|
+
# Arrays of the response, and may seem a little odd to the uninitiated.
|
120
|
+
def request(path, parameters = {})
|
121
|
+
response = Base.connection.post(path, convert_body(parameters), "Content-Type" => content_type)
|
122
|
+
|
123
|
+
if response.code.to_i / 100 == 2
|
124
|
+
result = XmlSimple.xml_in(response.body, 'keeproot' => true, 'contentkey' => '__content__', 'forcecontent' => true)
|
125
|
+
typecast_value(result)
|
126
|
+
else
|
127
|
+
raise "#{response.message} (#{response.code})"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# A convenience method for wrapping the result of a query in a Record
|
132
|
+
# object. This assumes that the result is a singleton, not a collection.
|
133
|
+
def record(path, parameters={})
|
134
|
+
result = request(path, parameters)
|
135
|
+
(result && !result.empty?) ? Record.new(result.keys.first, result.values.first) : nil
|
136
|
+
end
|
137
|
+
|
138
|
+
# A convenience method for wrapping the result of a query in Record
|
139
|
+
# objects. This assumes that the result is a collection--any singleton
|
140
|
+
# result will be wrapped in an array.
|
141
|
+
def records(node, path, parameters={})
|
142
|
+
result = request(path, parameters).values.first or return []
|
143
|
+
result = result[node] or return []
|
144
|
+
result = [result] unless Array === result
|
145
|
+
result.map { |row| Record.new(node, row) }
|
146
|
+
end
|
147
|
+
|
148
|
+
def convert_body(body)
|
149
|
+
body = use_xml ? body.to_legacy_xml : body.to_yaml
|
150
|
+
end
|
151
|
+
|
152
|
+
def content_type
|
153
|
+
use_xml ? "application/xml" : "application/x-yaml"
|
154
|
+
end
|
155
|
+
|
156
|
+
def typecast_value(value)
|
157
|
+
case value
|
158
|
+
when Hash
|
159
|
+
if value.has_key?("__content__")
|
160
|
+
content = translate_entities(value["__content__"]).strip
|
161
|
+
case value["type"]
|
162
|
+
when "integer" then content.to_i
|
163
|
+
when "boolean" then content == "true"
|
164
|
+
when "datetime" then Time.parse(content)
|
165
|
+
when "date" then Date.parse(content)
|
166
|
+
else content
|
167
|
+
end
|
168
|
+
# a special case to work-around a bug in XmlSimple. When you have an empty
|
169
|
+
# tag that has an attribute, XmlSimple will not add the __content__ key
|
170
|
+
# to the returned hash. Thus, we check for the presense of the 'type'
|
171
|
+
# attribute to look for empty, typed tags, and simply return nil for
|
172
|
+
# their value.
|
173
|
+
elsif value.keys == %w(type)
|
174
|
+
nil
|
175
|
+
elsif value["nil"] == "true"
|
176
|
+
nil
|
177
|
+
# another special case, introduced by the latest rails, where an array
|
178
|
+
# type now exists. This is parsed by XmlSimple as a two-key hash, where
|
179
|
+
# one key is 'type' and the other is the actual array value.
|
180
|
+
elsif value.keys.length == 2 && value["type"] == "array"
|
181
|
+
value.delete("type")
|
182
|
+
typecast_value(value)
|
183
|
+
else
|
184
|
+
value.empty? ? nil : value.inject({}) do |h,(k,v)|
|
185
|
+
h[k] = typecast_value(v)
|
186
|
+
h
|
187
|
+
end
|
188
|
+
end
|
189
|
+
when Array
|
190
|
+
value.map! { |i| typecast_value(i) }
|
191
|
+
case value.length
|
192
|
+
when 0 then nil
|
193
|
+
when 1 then value.first
|
194
|
+
else value
|
195
|
+
end
|
196
|
+
else
|
197
|
+
raise "can't typecast #{value.inspect}"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def translate_entities(value)
|
202
|
+
value.gsub(/</, "<").
|
203
|
+
gsub(/>/, ">").
|
204
|
+
gsub(/"/, '"').
|
205
|
+
gsub(/'/, "'").
|
206
|
+
gsub(/&/, "&")
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
@@ -0,0 +1,23 @@
|
|
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
|
+
module Basecamp
|
20
|
+
class Comment < Resource
|
21
|
+
parent_resources :post, :milestone, :todo_item
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Basecamp
|
2
|
+
class Connection #:nodoc:
|
3
|
+
def initialize(master)
|
4
|
+
@master = master
|
5
|
+
@connection = Net::HTTP.new(master.site, master.use_ssl ? 443 : 80)
|
6
|
+
@connection.use_ssl = master.use_ssl
|
7
|
+
@connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if master.use_ssl
|
8
|
+
end
|
9
|
+
|
10
|
+
def post(path, body, headers = {})
|
11
|
+
request = Net::HTTP::Post.new(path, headers.merge('Accept' => 'application/xml'))
|
12
|
+
request.basic_auth(@master.user, @master.password)
|
13
|
+
@connection.request(request, body)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Basecamp
|
2
|
+
class Message < Resource
|
3
|
+
parent_resources :project
|
4
|
+
self.element_name = 'post'
|
5
|
+
|
6
|
+
# Returns the most recent 25 messages in the given project (and category,
|
7
|
+
# if specified). If you need to retrieve older messages, use the archive
|
8
|
+
# method instead. Example:
|
9
|
+
#
|
10
|
+
# Basecamp::Message.list(1037)
|
11
|
+
# Basecamp::Message.list(1037, :category_id => 7301)
|
12
|
+
#
|
13
|
+
def self.list(project_id, options = {})
|
14
|
+
find(:all, :params => options.merge(:project_id => project_id))
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns a summary of all messages in the given project (and category, if
|
18
|
+
# specified). The summary is simply the title and category of the message,
|
19
|
+
# as well as the number of attachments (if any). Example:
|
20
|
+
#
|
21
|
+
# Basecamp::Message.archive(1037)
|
22
|
+
# Basecamp::Message.archive(1037, :category_id => 7301)
|
23
|
+
#
|
24
|
+
def self.archive(project_id, options = {})
|
25
|
+
find(:all, :params => options.merge(:project_id => project_id), :from => :archive)
|
26
|
+
end
|
27
|
+
|
28
|
+
def comments(options = {})
|
29
|
+
@comments ||= Comment.find(:all, :params => options.merge(:post_id => id))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Basecamp
|
2
|
+
class Record #:nodoc:
|
3
|
+
attr_reader :type
|
4
|
+
|
5
|
+
def initialize(type, hash)
|
6
|
+
@type, @hash = type, hash
|
7
|
+
end
|
8
|
+
|
9
|
+
def [](name)
|
10
|
+
name = dashify(name)
|
11
|
+
|
12
|
+
case @hash[name]
|
13
|
+
when Hash then
|
14
|
+
@hash[name] = if (@hash[name].keys.length == 1 && @hash[name].values.first.is_a?(Array))
|
15
|
+
@hash[name].values.first.map { |v| Record.new(@hash[name].keys.first, v) }
|
16
|
+
else
|
17
|
+
Record.new(name, @hash[name])
|
18
|
+
end
|
19
|
+
else
|
20
|
+
@hash[name]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def id
|
25
|
+
@hash['id']
|
26
|
+
end
|
27
|
+
|
28
|
+
def attributes
|
29
|
+
@hash.keys
|
30
|
+
end
|
31
|
+
|
32
|
+
def respond_to?(sym)
|
33
|
+
super || @hash.has_key?(dashify(sym))
|
34
|
+
end
|
35
|
+
|
36
|
+
def method_missing(sym, *args)
|
37
|
+
if args.empty? && !block_given? && respond_to?(sym)
|
38
|
+
self[sym]
|
39
|
+
else
|
40
|
+
super
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_s
|
45
|
+
"\#<Record(#{@type}) #{@hash.inspect[1..-2]}>"
|
46
|
+
end
|
47
|
+
|
48
|
+
def inspect
|
49
|
+
to_s
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def dashify(name)
|
55
|
+
name.to_s.tr("_", "-")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Basecamp
|
2
|
+
class Resource < ActiveResource::Base #:nodoc:
|
3
|
+
class << self
|
4
|
+
def parent_resources(*parents)
|
5
|
+
@parent_resources = parents
|
6
|
+
end
|
7
|
+
|
8
|
+
def element_name
|
9
|
+
name.split(/::/).last.underscore
|
10
|
+
end
|
11
|
+
|
12
|
+
def prefix_source
|
13
|
+
@parent_resources.map { |resource| "/#{resource.to_s.pluralize}/:#{resource}_id/" }.join
|
14
|
+
end
|
15
|
+
|
16
|
+
def prefix(options={})
|
17
|
+
if options.any?
|
18
|
+
options.map { |name, value| "/#{name.to_s.chomp('_id').pluralize}/#{value}/" }.join
|
19
|
+
else
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def prefix_options
|
26
|
+
id ? {} : super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Basecamp
|
2
|
+
class TimeEntry < Resource
|
3
|
+
parent_resources :project, :todo_item
|
4
|
+
|
5
|
+
def self.all(project_id, page=0)
|
6
|
+
find(:all, :params => { :project_id => project_id, :page => page })
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.report(options={})
|
10
|
+
find(:all, :from => :report, :params => options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def todo_item(options={})
|
14
|
+
@todo_item ||= todo_item_id && TodoItem.find(todo_item_id, options)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Basecamp
|
2
|
+
class TodoItem < Resource
|
3
|
+
parent_resources :todo_list
|
4
|
+
|
5
|
+
def todo_list(options={})
|
6
|
+
@todo_list ||= TodoList.find(todo_list_id, options)
|
7
|
+
end
|
8
|
+
|
9
|
+
def time_entries(options={})
|
10
|
+
@time_entries ||= TimeEntry.find(:all, :params => options.merge(:todo_item_id => id))
|
11
|
+
end
|
12
|
+
|
13
|
+
def comments(options = {})
|
14
|
+
@comments ||= Comment.find(:all, :params => options.merge(:todo_item_id => id))
|
15
|
+
end
|
16
|
+
|
17
|
+
def complete!
|
18
|
+
put(:complete)
|
19
|
+
end
|
20
|
+
|
21
|
+
def uncomplete!
|
22
|
+
put(:uncomplete)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Basecamp
|
2
|
+
class TodoList < Resource
|
3
|
+
parent_resources :project
|
4
|
+
|
5
|
+
# Returns all lists for a project. If complete is true, only completed lists
|
6
|
+
# are returned. If complete is false, only uncompleted lists are returned.
|
7
|
+
def self.all(project_id, complete=nil)
|
8
|
+
filter = case complete
|
9
|
+
when nil then "all"
|
10
|
+
when true then "finished"
|
11
|
+
when false then "pending"
|
12
|
+
else raise ArgumentError, "invalid value for `complete'"
|
13
|
+
end
|
14
|
+
|
15
|
+
find(:all, :params => { :project_id => project_id, :filter => filter })
|
16
|
+
end
|
17
|
+
|
18
|
+
def todo_items(options={})
|
19
|
+
@todo_items ||= TodoItem.find(:all, :params => options.merge(:todo_list_id => id))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
require File.dirname(__FILE__) + '/../lib/basecamp.rb'
|
3
|
+
|
4
|
+
describe Basecamp do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
Basecamp::Base.establish_connection!('flatsoft-test.grouphub.com', 'flatsoft', '123456')
|
8
|
+
@basecamp = Basecamp::Base.new
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "Creating a resource" do
|
12
|
+
it "should create a comment for post" do
|
13
|
+
comment = Basecamp::Comment.new(:post_id => 15797423)
|
14
|
+
comment.body = "test comment"
|
15
|
+
comment.save
|
16
|
+
c = Basecamp::Comment.find(comment.id)
|
17
|
+
c.body.should == "test comment"
|
18
|
+
c.id.should == comment.id.to_i
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "Finding a resource" do
|
23
|
+
it "should find message" do
|
24
|
+
message = Basecamp::Message.find(15797423)
|
25
|
+
message.body.should_not be_blank
|
26
|
+
message.category_id.should_not be_blank
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "Updating a Resource" do
|
31
|
+
it "should update message" do
|
32
|
+
m = Basecamp::Message.find(15797423)
|
33
|
+
m.body = 'Changed'
|
34
|
+
m.save
|
35
|
+
message = Basecamp::Message.find(15797423)
|
36
|
+
message.body.should == 'Changed'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "Deleting a Resource" do
|
41
|
+
it "should delete todo item" do
|
42
|
+
todo = Basecamp::TodoItem.create(:todo_list_id => 4501767, :content => 'Todo for destroy')
|
43
|
+
Basecamp::TodoItem.delete(todo.id)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "Using the non-REST inteface" do
|
48
|
+
it "should return array of projects" do
|
49
|
+
@basecamp.projects.should be_kind_of(Array)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should return valid project with name" do
|
53
|
+
@basecamp.projects.first.name.should_not be_empty
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should find person" do
|
57
|
+
person = @basecamp.person(2926255)
|
58
|
+
person.should_not be_blank
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
2
|
+
require File.dirname(__FILE__) + '/../../lib/basecamp.rb'
|
3
|
+
|
4
|
+
describe Basecamp::Base do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
Basecamp::Base.establish_connection!('flatsoft-test.grouphub.com', 'flatsoft', '123456')
|
8
|
+
@basecamp = Basecamp::Base.new
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should return the list of all accessible projects" do
|
12
|
+
@basecamp.projects.should_not be_blank
|
13
|
+
@basecamp.projects.should be_kind_of(Array)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should return valid project with name" do
|
17
|
+
@basecamp.projects.first.name.should_not be_empty
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should find person" do
|
21
|
+
person = @basecamp.person(2926255)
|
22
|
+
person.should_not be_blank
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should return the list of message categories for the given project" do
|
26
|
+
categories = @basecamp.message_categories(2388627)
|
27
|
+
categories.should_not be_blank
|
28
|
+
categories.should be_kind_of(Array)
|
29
|
+
end
|
30
|
+
|
31
|
+
# CONTACT MANAGEMENT
|
32
|
+
|
33
|
+
it "should return information for the company with the given id" do
|
34
|
+
company = @basecamp.company(1098114)
|
35
|
+
company.should_not be_blank
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should return an array of the people in the given company" do
|
39
|
+
people = @basecamp.people(1098114)
|
40
|
+
people.should_not be_blank
|
41
|
+
people.should be_kind_of(Array)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should return information about the person with the given id" do
|
45
|
+
@basecamp.person(2926255).should_not be_nil
|
46
|
+
end
|
47
|
+
|
48
|
+
# MILESTONES
|
49
|
+
|
50
|
+
it "should return a list of all milestones for the given project" do
|
51
|
+
milestones = @basecamp.milestones(2388627)
|
52
|
+
milestones.should be_kind_of(Array)
|
53
|
+
milestones.should_not be_blank
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should complete milestone with the given id" do
|
57
|
+
milestone = @basecamp.milestones(2388627).first
|
58
|
+
@basecamp.complete_milestone(milestone.id)
|
59
|
+
|
60
|
+
Basecamp::Base.establish_connection!('flatsoft-test.grouphub.com', 'flatsoft', '123456')
|
61
|
+
|
62
|
+
milestone = @basecamp.milestones(2388627).first
|
63
|
+
milestone.completed.should == true
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should uncomplete milestone with the given id" do
|
67
|
+
milestone = @basecamp.milestones(2388627).first
|
68
|
+
@basecamp.uncomplete_milestone(milestone.id)
|
69
|
+
|
70
|
+
Basecamp::Base.establish_connection!('flatsoft-test.grouphub.com', 'flatsoft', '123456')
|
71
|
+
|
72
|
+
milestone = @basecamp.milestones(2388627).first
|
73
|
+
milestone.completed.should == false
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
2
|
+
require File.dirname(__FILE__) + '/../../lib/basecamp.rb'
|
3
|
+
|
4
|
+
describe Basecamp::Message do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
Basecamp::Base.establish_connection!('flatsoft-test.grouphub.com', 'flatsoft', '123456')
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should return the most recent 25 messages in the given project" do
|
11
|
+
messages = Basecamp::Message.list(2388627)
|
12
|
+
messages.should_not be_blank
|
13
|
+
messages.should be_kind_of(Array)
|
14
|
+
messages.size.should_not > 25
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should get a list of messages for given project and specified category" do
|
18
|
+
messages = Basecamp::Message.list(2388627, :category_id => 23864732)
|
19
|
+
messages.should_not be_blank
|
20
|
+
messages.should be_kind_of(Array)
|
21
|
+
messages.each{ |m| m.category_id.should == 23864732 }
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should return a summary of all messages in the given project" do
|
25
|
+
messages = Basecamp::Message.archive(2388627)
|
26
|
+
messages.should_not be_blank
|
27
|
+
messages.should be_kind_of(Array)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should return a summary of all messages in the given project and specified category " do
|
31
|
+
messages = Basecamp::Message.archive(2388627, :category_id => 23864732)
|
32
|
+
messages.should_not be_blank
|
33
|
+
messages.should be_kind_of(Array)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should return list of message comments" do
|
37
|
+
message = Basecamp::Message.find(15797423)
|
38
|
+
comments = message.comments
|
39
|
+
comments.should_not be_blank
|
40
|
+
comments.should be_kind_of(Array)
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
2
|
+
require File.dirname(__FILE__) + '/../../lib/basecamp.rb'
|
3
|
+
|
4
|
+
describe Basecamp::TodoItem do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
Basecamp::Base.establish_connection!('flatsoft-test.grouphub.com', 'flatsoft', '123456')
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should have todo list" do
|
11
|
+
todo = Basecamp::TodoItem.find(32416216)
|
12
|
+
todo.todo_list.should_not be_blank
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should return list of comments" do
|
16
|
+
todo = Basecamp::TodoItem.find(32416216)
|
17
|
+
todo.comments.should_not be_blank
|
18
|
+
todo.comments.should be_kind_of(Array)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should complete todo item" do
|
22
|
+
todo = Basecamp::TodoItem.find(32416216)
|
23
|
+
todo.complete!
|
24
|
+
|
25
|
+
Basecamp::Base.establish_connection!('flatsoft-test.grouphub.com', 'flatsoft', '123456')
|
26
|
+
|
27
|
+
todo = Basecamp::TodoItem.find(32416216)
|
28
|
+
todo.completed.should == true
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should uncomplete todo item" do
|
32
|
+
todo = Basecamp::TodoItem.find(32416216)
|
33
|
+
todo.uncomplete!
|
34
|
+
|
35
|
+
Basecamp::Base.establish_connection!('flatsoft-test.grouphub.com', 'flatsoft', '123456')
|
36
|
+
|
37
|
+
todo = Basecamp::TodoItem.find(32416216)
|
38
|
+
todo.completed.should == false
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
2
|
+
require File.dirname(__FILE__) + '/../../lib/basecamp.rb'
|
3
|
+
|
4
|
+
describe Basecamp::TodoList do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
Basecamp::Base.establish_connection!('flatsoft-test.grouphub.com', 'flatsoft', '123456')
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should return todo items" do
|
11
|
+
list = Basecamp::TodoList.find(4501767)
|
12
|
+
list.todo_items.should_not be_blank
|
13
|
+
list.todo_items.should be_kind_of(Array)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should return all lists for a specified project" do
|
17
|
+
list = Basecamp::TodoList.all(2388627)
|
18
|
+
list.should_not be_blank
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should return all finished lists for a specified project" do
|
22
|
+
list = Basecamp::TodoList.all(2388627, true)
|
23
|
+
list.should_not be_blank
|
24
|
+
list.each { |item| item.complete.should == "true" }
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should return all pending lists for a specified project" do
|
28
|
+
list = Basecamp::TodoList.all(2388627, false)
|
29
|
+
list.should_not be_blank
|
30
|
+
list.each { |item| item.complete.should == "false" }
|
31
|
+
end
|
32
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec_helper.rb
ADDED
data/tasks/rspec.rake
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
begin
|
2
|
+
require 'spec'
|
3
|
+
rescue LoadError
|
4
|
+
require 'rubygems'
|
5
|
+
require 'spec'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'spec/rake/spectask'
|
9
|
+
rescue LoadError
|
10
|
+
puts <<-EOS
|
11
|
+
To use rspec for testing you must install rspec gem:
|
12
|
+
gem install rspec
|
13
|
+
EOS
|
14
|
+
exit(0)
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Run the specs under spec/models"
|
18
|
+
Spec::Rake::SpecTask.new do |t|
|
19
|
+
t.spec_opts = ['--options', "spec/spec.opts"]
|
20
|
+
t.spec_files = FileList['spec/*_spec.rb']
|
21
|
+
end
|
22
|
+
|
23
|
+
namespace :spec do
|
24
|
+
desc "Run all specs in spec directory with RCov (excluding plugin specs)"
|
25
|
+
Spec::Rake::SpecTask.new(:rcov) do |t|
|
26
|
+
t.spec_opts = ['--options', '"spec/spec.opts"']
|
27
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
28
|
+
t.rcov = true
|
29
|
+
t.rcov_opts = lambda do
|
30
|
+
IO.readlines('spec/rcov.opts').map {|l| l.chomp.split " "}.flatten
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: turingstudio-basecamp-rb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- The Turing Studio, Inc.
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-02-25 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: xml-simple
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.0.11
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: activeresource
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.2.2
|
34
|
+
version:
|
35
|
+
description: FIX (describe your package)
|
36
|
+
email:
|
37
|
+
- operations@turingstudio.com
|
38
|
+
executables: []
|
39
|
+
|
40
|
+
extensions: []
|
41
|
+
|
42
|
+
extra_rdoc_files:
|
43
|
+
- README.rdoc
|
44
|
+
files:
|
45
|
+
- History.txt
|
46
|
+
- README.rdoc
|
47
|
+
- lib/basecamp.rb
|
48
|
+
- lib/basecamp/base.rb
|
49
|
+
- lib/basecamp/version.rb
|
50
|
+
- lib/basecamp/resource.rb
|
51
|
+
- lib/basecamp/attachment.rb
|
52
|
+
- lib/basecamp/comment.rb
|
53
|
+
- lib/basecamp/connection.rb
|
54
|
+
- lib/basecamp/message.rb
|
55
|
+
- lib/basecamp/record.rb
|
56
|
+
- lib/basecamp/time_entry.rb
|
57
|
+
- lib/basecamp/todoitem.rb
|
58
|
+
- lib/basecamp/todolist.rb
|
59
|
+
- spec/basecamp_spec.rb
|
60
|
+
- spec/spec.opts
|
61
|
+
- spec/spec_helper.rb
|
62
|
+
- spec/lib/basecamp_base.rb
|
63
|
+
- spec/lib/basecamp_message_spec.rb
|
64
|
+
- spec/lib/basecamp_todo_item_spec.rb
|
65
|
+
- spec/lib/basecamp_todo_list_spec.rb
|
66
|
+
- tasks/rspec.rake
|
67
|
+
has_rdoc: true
|
68
|
+
homepage: http://github.com/turingstudio/basecamp-rb/
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options: []
|
71
|
+
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: "0"
|
79
|
+
version:
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: "0"
|
85
|
+
version:
|
86
|
+
requirements: []
|
87
|
+
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.2.0
|
90
|
+
signing_key:
|
91
|
+
specification_version: 2
|
92
|
+
summary: FIX (describe your package)
|
93
|
+
test_files: []
|
94
|
+
|