podio 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +32 -0
- data/Rakefile +13 -1
- data/lib/podio.rb +39 -10
- data/lib/podio/areas/app_store.rb +45 -0
- data/lib/podio/areas/application.rb +19 -0
- data/lib/podio/areas/contact.rb +36 -0
- data/lib/podio/areas/file.rb +23 -0
- data/lib/podio/areas/form.rb +11 -0
- data/lib/podio/areas/item.rb +32 -0
- data/lib/podio/areas/organization.rb +67 -0
- data/lib/podio/areas/organization_member.rb +35 -0
- data/lib/podio/areas/search.rb +15 -0
- data/lib/podio/areas/space.rb +46 -0
- data/lib/podio/areas/user.rb +19 -0
- data/lib/podio/areas/user_status.rb +11 -0
- data/lib/podio/areas/widget.rb +45 -0
- data/lib/podio/client.rb +51 -22
- data/lib/podio/error.rb +10 -0
- data/lib/podio/middleware/date_conversion.rb +40 -0
- data/lib/podio/middleware/error_response.rb +6 -6
- data/lib/podio/middleware/logger.rb +7 -1
- data/lib/podio/middleware/oauth2.rb +2 -2
- data/lib/podio/middleware/response_recorder.rb +16 -0
- data/lib/podio/response_wrapper.rb +15 -0
- data/lib/podio/version.rb +1 -1
- data/podio.gemspec +2 -4
- data/test/app_store_test.rb +40 -0
- data/test/client_test.rb +50 -57
- data/test/contact_test.rb +26 -0
- data/test/item_test.rb +37 -0
- data/test/organization_test.rb +20 -0
- data/test/space_test.rb +17 -0
- data/test/test_helper.rb +44 -46
- data/test/widget_test.rb +55 -0
- metadata +36 -31
- data/lib/podio/middleware/mashify.rb +0 -27
@@ -0,0 +1,19 @@
|
|
1
|
+
module Podio
|
2
|
+
module User
|
3
|
+
include Podio::ResponseWrapper
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def current
|
7
|
+
member Podio.connection.get("/user/").body
|
8
|
+
end
|
9
|
+
|
10
|
+
def create(token, attributes)
|
11
|
+
response = Podio.connection.post do |req|
|
12
|
+
req.url '/user/'
|
13
|
+
req.body = attributes.merge(:token => token)
|
14
|
+
end
|
15
|
+
|
16
|
+
response.body['user_id']
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Podio
|
2
|
+
module Widget
|
3
|
+
include Podio::ResponseWrapper
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def create(ref_type, ref_id, attributes)
|
7
|
+
response = Podio.connection.post do |req|
|
8
|
+
req.url "/widget/#{ref_type}/#{ref_id}/"
|
9
|
+
req.body = attributes
|
10
|
+
end
|
11
|
+
|
12
|
+
response.body['widget_id']
|
13
|
+
end
|
14
|
+
|
15
|
+
def update(id, attributes)
|
16
|
+
response = Podio.connection.put do |req|
|
17
|
+
req.url "/widget/#{id}"
|
18
|
+
req.body = attributes
|
19
|
+
end
|
20
|
+
|
21
|
+
response.status
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete(id)
|
25
|
+
Podio.connection.delete("/widget/#{id}").status
|
26
|
+
end
|
27
|
+
|
28
|
+
def update_order(ref_type, ref_id, widget_list)
|
29
|
+
response = Podio.connection.put do |req|
|
30
|
+
req.url "/widget/#{ref_type}/#{ref_id}/order"
|
31
|
+
req.body = widget_list
|
32
|
+
end
|
33
|
+
|
34
|
+
response.status
|
35
|
+
end
|
36
|
+
|
37
|
+
def find(id)
|
38
|
+
member Podio.connection.get("/widget/#{id}").body
|
39
|
+
end
|
40
|
+
|
41
|
+
def find_all_for_reference(ref_type, ref_id)
|
42
|
+
list Podio.connection.get("/widget/#{ref_type}/#{ref_id}/display/").body
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/podio/client.rb
CHANGED
@@ -1,13 +1,7 @@
|
|
1
|
-
require 'podio/middleware/logger'
|
2
|
-
require 'podio/middleware/oauth2'
|
3
|
-
require 'podio/middleware/mashify'
|
4
|
-
require 'podio/middleware/podio_api'
|
5
|
-
require 'podio/middleware/yajl_response'
|
6
|
-
require 'podio/middleware/error_response'
|
7
|
-
|
8
1
|
module Podio
|
9
2
|
class Client
|
10
|
-
attr_reader :api_url, :api_key, :api_secret, :debug, :
|
3
|
+
attr_reader :api_url, :api_key, :api_secret, :debug, :connection
|
4
|
+
attr_accessor :oauth_token, :stubs
|
11
5
|
|
12
6
|
def initialize(options = {})
|
13
7
|
@api_url = options[:api_url] || Podio.api_url || 'https://api.podio.com'
|
@@ -16,12 +10,23 @@ module Podio
|
|
16
10
|
@debug = options[:debug] || Podio.debug
|
17
11
|
@oauth_token = options[:oauth_token]
|
18
12
|
|
19
|
-
|
20
|
-
|
13
|
+
if options[:enable_stubs]
|
14
|
+
@enable_stubs = true
|
15
|
+
@stubs = Faraday::Adapter::Test::Stubs.new
|
16
|
+
end
|
17
|
+
@test_mode = options[:test_mode]
|
18
|
+
@record_mode = options[:record_mode]
|
19
|
+
|
20
|
+
setup_connections
|
21
|
+
end
|
22
|
+
|
23
|
+
def reset
|
24
|
+
setup_connections
|
21
25
|
end
|
22
26
|
|
27
|
+
# Sign in as a user
|
23
28
|
def get_access_token(username, password)
|
24
|
-
response =
|
29
|
+
response = @oauth_connection.post do |req|
|
25
30
|
req.url '/oauth/token', :grant_type => 'password', :client_id => api_key, :client_secret => api_secret, :username => username, :password => password
|
26
31
|
end
|
27
32
|
|
@@ -29,6 +34,17 @@ module Podio
|
|
29
34
|
configure_oauth
|
30
35
|
@oauth_token
|
31
36
|
end
|
37
|
+
|
38
|
+
# Sign in as an app
|
39
|
+
def get_access_token_as_app(app_id, app_token)
|
40
|
+
response = @oauth_connection.post do |req|
|
41
|
+
req.url '/oauth/token', :grant_type => 'app', :client_id => api_key, :client_secret => api_secret, :app_id => app_id, :app_token => app_token
|
42
|
+
end
|
43
|
+
|
44
|
+
@oauth_token = OAuthToken.new(response.body)
|
45
|
+
configure_oauth
|
46
|
+
@oauth_token
|
47
|
+
end
|
32
48
|
|
33
49
|
def refresh_access_token
|
34
50
|
response = @oauth_connection.post do |req|
|
@@ -39,37 +55,50 @@ module Podio
|
|
39
55
|
configure_oauth
|
40
56
|
end
|
41
57
|
|
58
|
+
def configured_headers
|
59
|
+
headers = {}
|
60
|
+
headers['User-Agent'] = 'Podio Ruby Library'
|
61
|
+
headers['authorization'] = "OAuth2 #{oauth_token.access_token}" if oauth_token
|
62
|
+
headers['X-Podio-Dry-Run'] = '1' if @test_mode
|
63
|
+
|
64
|
+
headers
|
65
|
+
end
|
66
|
+
|
42
67
|
private
|
43
68
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
69
|
+
def setup_connections
|
70
|
+
@connection = configure_connection
|
71
|
+
@oauth_connection = configure_oauth_connection
|
72
|
+
end
|
47
73
|
|
48
|
-
|
74
|
+
def configure_connection
|
75
|
+
Faraday::Connection.new(:url => api_url, :headers => configured_headers, :request => {:client => self}) do |builder|
|
49
76
|
builder.use Faraday::Request::Yajl
|
50
77
|
builder.use Middleware::PodioApi
|
51
78
|
builder.use Middleware::OAuth2
|
52
79
|
builder.use Middleware::Logger
|
53
|
-
builder.adapter
|
80
|
+
builder.adapter(*default_adapter)
|
81
|
+
builder.use Middleware::ResponseRecorder if @record_mode
|
54
82
|
builder.use Middleware::YajlResponse
|
55
|
-
builder.use Middleware::Mashify
|
56
83
|
builder.use Middleware::ErrorResponse
|
84
|
+
builder.use Middleware::DateConversion
|
57
85
|
end
|
58
86
|
end
|
59
87
|
|
88
|
+
def default_adapter
|
89
|
+
@enable_stubs ? [:test, @stubs] : Faraday.default_adapter
|
90
|
+
end
|
91
|
+
|
60
92
|
def configure_oauth_connection
|
61
93
|
conn = @connection.dup
|
62
94
|
conn.options[:client] = self
|
63
|
-
conn.
|
95
|
+
conn.headers.delete('authorization')
|
96
|
+
conn.headers.delete('X-Podio-Dry-Run') if @test_mode # oauth requests don't really work well in test mode
|
64
97
|
conn
|
65
98
|
end
|
66
99
|
|
67
100
|
def configure_oauth
|
68
101
|
@connection = configure_connection
|
69
102
|
end
|
70
|
-
|
71
|
-
def default_headers
|
72
|
-
{ :user_agent => 'Podio Ruby Library' }
|
73
|
-
end
|
74
103
|
end
|
75
104
|
end
|
data/lib/podio/error.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
module Podio
|
2
|
+
module Error
|
3
|
+
class TokenExpired < StandardError; end
|
4
|
+
class AuthorizationError < StandardError; end
|
5
|
+
class BadRequestError < StandardError; end
|
6
|
+
class ServerError < StandardError; end
|
7
|
+
class NotFoundError < StandardError; end
|
8
|
+
class GoneError < StandardError; end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Podio
|
2
|
+
module Middleware
|
3
|
+
class DateConversion < Faraday::Response::Middleware
|
4
|
+
def self.register_on_complete(env)
|
5
|
+
env[:response].on_complete do |finished_env|
|
6
|
+
convert_dates(finished_env[:body])
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
# Converts all attributes ending with "_on" to datetime and ending with "date" to date
|
11
|
+
def self.convert_dates(body)
|
12
|
+
[body].flatten.compact.each do |hash|
|
13
|
+
hash.each do |key, value|
|
14
|
+
if value.is_a?(Hash)
|
15
|
+
self.convert_dates(value)
|
16
|
+
elsif value.is_a?(Array)
|
17
|
+
value.each_with_index { |item, index| hash[key][index] = self.convert_field(key, item) }
|
18
|
+
else
|
19
|
+
hash[key] = self.convert_field(key, value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def self.convert_field(key, value)
|
28
|
+
if key.present?
|
29
|
+
if key[-3..-1] == "_on"
|
30
|
+
return value.try(:to_s).try(:to_datetime)
|
31
|
+
elsif key[-4..-1] == "date"
|
32
|
+
return value.try(:to_s).try(:to_date)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
value
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -7,21 +7,21 @@ module Podio
|
|
7
7
|
env[:response].on_complete do |finished_env|
|
8
8
|
case finished_env[:status]
|
9
9
|
when 400
|
10
|
-
raise Error::BadRequestError, finished_env[:body]
|
10
|
+
raise Error::BadRequestError, finished_env[:body]
|
11
11
|
when 401
|
12
|
-
if finished_env[:body]['
|
13
|
-
raise Error::TokenExpired, finished_env[:body].
|
12
|
+
if finished_env[:body]['error_description'] =~ /expired_token/
|
13
|
+
raise Error::TokenExpired, finished_env[:body].inspect
|
14
14
|
else
|
15
|
-
raise Error::AuthorizationError, finished_env[:body].
|
15
|
+
raise Error::AuthorizationError, finished_env[:body].inspect
|
16
16
|
end
|
17
17
|
when 403
|
18
|
-
raise Error::AuthorizationError, finished_env[:body].
|
18
|
+
raise Error::AuthorizationError, finished_env[:body].inspect
|
19
19
|
when 404
|
20
20
|
raise Error::NotFoundError, "#{finished_env[:method].to_s.upcase} #{finished_env[:url]}"
|
21
21
|
when 410
|
22
22
|
raise Error::GoneError, "#{finished_env[:method].to_s.upcase} #{finished_env[:url]}"
|
23
23
|
when 500
|
24
|
-
raise Error::ServerError, finished_env[:body].
|
24
|
+
raise Error::ServerError, finished_env[:body].inspect
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
@@ -8,7 +8,13 @@ module Podio
|
|
8
8
|
puts "\n==> #{env[:method].to_s.upcase} #{env[:url]} \n\n"
|
9
9
|
end
|
10
10
|
|
11
|
-
|
11
|
+
begin
|
12
|
+
@app.call(env)
|
13
|
+
ensure
|
14
|
+
if env[:request][:client].debug
|
15
|
+
puts "\n== (#{env[:status]}) ==> #{env[:body]}\n\n"
|
16
|
+
end
|
17
|
+
end
|
12
18
|
end
|
13
19
|
|
14
20
|
def initialize(app)
|
@@ -12,8 +12,8 @@ module Podio
|
|
12
12
|
rescue Error::TokenExpired
|
13
13
|
podio_client.refresh_access_token
|
14
14
|
|
15
|
-
|
16
|
-
orig_env[:
|
15
|
+
# new access token needs to be put into the header
|
16
|
+
orig_env[:request_headers].merge!(podio_client.configured_headers)
|
17
17
|
|
18
18
|
# redo the request with the new access token
|
19
19
|
@app.call(orig_env)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module Podio
|
4
|
+
module Middleware
|
5
|
+
class ResponseRecorder < Faraday::Response::Middleware
|
6
|
+
def self.register_on_complete(env)
|
7
|
+
env[:response].on_complete do |finished_env|
|
8
|
+
response = "['#{Faraday::Utils.normalize_path(finished_env[:url])}', :#{finished_env[:method]}, #{finished_env[:status]}, #{finished_env[:response_headers]}, '#{finished_env[:body]}']"
|
9
|
+
|
10
|
+
filename = Digest::MD5.hexdigest(finished_env[:url].request_uri)
|
11
|
+
File.open("#{filename}.rack", 'w') { |f| f.write(response) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Podio
|
2
|
+
module ResponseWrapper
|
3
|
+
def member(response)
|
4
|
+
response
|
5
|
+
end
|
6
|
+
|
7
|
+
def list(response)
|
8
|
+
response
|
9
|
+
end
|
10
|
+
|
11
|
+
def collection(response)
|
12
|
+
Struct.new(:all, :count, :total_count).new(response['items'], response['filtered'], response['total'])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/podio/version.rb
CHANGED
data/podio.gemspec
CHANGED
@@ -16,10 +16,8 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.require_paths = ['lib']
|
17
17
|
|
18
18
|
s.add_runtime_dependency 'faraday', '~> 0.5.1'
|
19
|
-
s.add_runtime_dependency '
|
20
|
-
s.add_runtime_dependency 'yajl-ruby', '~> 0.7
|
21
|
-
|
22
|
-
s.add_development_dependency 'fakeweb', '~> 1.3'
|
19
|
+
s.add_runtime_dependency 'activesupport', '~> 3.0'
|
20
|
+
s.add_runtime_dependency 'yajl-ruby', '~> 0.7'
|
23
21
|
|
24
22
|
s.description = <<desc
|
25
23
|
The humble beginnings of the Ruby wrapper for the Podio API.
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class AppStoreTest < ActiveSupport::TestCase
|
4
|
+
test 'should find single category' do
|
5
|
+
category = Podio::Category.find(1)
|
6
|
+
|
7
|
+
assert_equal 1, category['category_id']
|
8
|
+
end
|
9
|
+
|
10
|
+
test 'should find all categories' do
|
11
|
+
categories = Podio::Category.find_all
|
12
|
+
|
13
|
+
assert_equal Array, categories.functional.class
|
14
|
+
assert_not_nil categories.functional.first['category_id']
|
15
|
+
|
16
|
+
assert_equal Array, categories.vertical.class
|
17
|
+
assert_not_nil categories.vertical.first['category_id']
|
18
|
+
end
|
19
|
+
|
20
|
+
test 'should create category' do
|
21
|
+
login_as_user(2)
|
22
|
+
|
23
|
+
status = Podio::Category.create(:type => 'vertical', :name => 'New Category')
|
24
|
+
assert_equal 200, status
|
25
|
+
end
|
26
|
+
|
27
|
+
test 'should update category' do
|
28
|
+
login_as_user(2)
|
29
|
+
|
30
|
+
status = Podio::Category.update(1, :name => 'New name')
|
31
|
+
assert_equal 204, status
|
32
|
+
end
|
33
|
+
|
34
|
+
test 'should delete category' do
|
35
|
+
login_as_user(2)
|
36
|
+
|
37
|
+
status = Podio::Category.delete(3)
|
38
|
+
assert_equal 204, status
|
39
|
+
end
|
40
|
+
end
|
data/test/client_test.rb
CHANGED
@@ -1,81 +1,74 @@
|
|
1
1
|
require_relative 'test_helper'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
config.api_secret = 'client_secret'
|
10
|
-
end
|
3
|
+
class ClientTest < ActiveSupport::TestCase
|
4
|
+
def setup
|
5
|
+
Podio.configure do |config|
|
6
|
+
config.api_url = 'https://api.podio.com'
|
7
|
+
config.api_key = 'client_id'
|
8
|
+
config.api_secret = 'client_secret'
|
11
9
|
end
|
10
|
+
end
|
12
11
|
|
13
|
-
|
14
|
-
|
12
|
+
test 'should configure defaults' do
|
13
|
+
podio = Podio::Client.new
|
15
14
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
assert_equal 'https://api.podio.com', podio.api_url
|
16
|
+
assert_equal 'client_id', podio.api_key
|
17
|
+
assert_equal 'client_secret', podio.api_secret
|
18
|
+
end
|
20
19
|
|
21
|
-
|
22
|
-
|
20
|
+
test 'should overwrite defaults' do
|
21
|
+
podio = Podio::Client.new(:api_url => 'https://new.podio.com', :api_key => 'new_client_id', :api_secret => 'new_client_secret')
|
23
22
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
assert_equal 'https://new.podio.com', podio.api_url
|
24
|
+
assert_equal 'new_client_id', podio.api_key
|
25
|
+
assert_equal 'new_client_secret', podio.api_secret
|
26
|
+
end
|
28
27
|
|
29
|
-
|
30
|
-
|
31
|
-
|
28
|
+
test 'should setup connection' do
|
29
|
+
token = Podio::OAuthToken.new('access_token' => 'access', 'refresh_token' => 'refresh')
|
30
|
+
podio = Podio::Client.new(:oauth_token => token)
|
32
31
|
|
33
|
-
|
32
|
+
assert_equal token, podio.oauth_token
|
34
33
|
|
35
|
-
|
36
|
-
end
|
34
|
+
assert_equal 'OAuth2 access', podio.connection.headers['authorization']
|
37
35
|
end
|
38
36
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
stub_post(
|
45
|
-
'/oauth/token?grant_type=password&client_id=client_id&client_secret=client_secret&username=username&password=password',
|
46
|
-
{'access_token' => 'access', 'refresh_token' => 'refresh', 'expires_in' => 3600}
|
47
|
-
)
|
37
|
+
test 'should get an access token' do
|
38
|
+
client = Podio.client
|
39
|
+
client.oauth_token = nil
|
40
|
+
assert_nil client.oauth_token
|
48
41
|
|
49
|
-
|
42
|
+
client.get_access_token('pollas@hoisthq.com', 'secret')
|
50
43
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
test 'should be able to refresh access token' do
|
56
|
-
client = podio_test_client
|
44
|
+
assert_not_nil client.oauth_token.access_token
|
45
|
+
assert_not_nil client.oauth_token.refresh_token
|
46
|
+
end
|
57
47
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
)
|
48
|
+
test 'should be able to refresh access token' do
|
49
|
+
client = Podio.client
|
50
|
+
old_token = client.oauth_token.dup
|
62
51
|
|
63
|
-
|
52
|
+
client.refresh_access_token
|
64
53
|
|
65
|
-
|
66
|
-
|
67
|
-
end
|
54
|
+
assert_not_equal old_token.access_token, client.oauth_token.access_token
|
55
|
+
assert_equal old_token.refresh_token, client.oauth_token.refresh_token
|
68
56
|
end
|
69
57
|
|
70
|
-
|
71
|
-
|
72
|
-
|
58
|
+
test 'should automatically refresh an expired token' do
|
59
|
+
# this token is already expired in the test database
|
60
|
+
Podio.client.oauth_token = Podio::OAuthToken.new('access_token' => '30da4594eef93528c11df7fb5deb989cd629ea7060a1ce1ced628d19398214c942bcfe0334cf953ef70a80ea1afdfd80183d5c75d19c1f5526ca4c6f6f3471ef', 'refresh_token' => '82e7a11ae187f28a25261599aa6229cd89f8faee48cba18a75d70efae88ba665ced11d43143b7f5bebb31a4103662b851dd2db1879a3947b843259479fccfad3', 'expires_in' => -10)
|
61
|
+
Podio.client.reset
|
73
62
|
|
74
|
-
|
75
|
-
|
76
|
-
response = client.connection.get('/')
|
63
|
+
assert_nothing_raised do
|
64
|
+
response = Podio.client.connection.get('/org/')
|
77
65
|
assert_equal 200, response.status
|
78
|
-
assert_equal({ 'messaging' => true, 'version' => '1.0.1' }, response.body)
|
79
66
|
end
|
80
67
|
end
|
68
|
+
|
69
|
+
test 'should be able to make arbitrary requests' do
|
70
|
+
response = Podio.client.connection.get('/org/')
|
71
|
+
assert_equal 200, response.status
|
72
|
+
assert response.body.is_a?(Array)
|
73
|
+
end
|
81
74
|
end
|