ticketinghub 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/CONTRIBUTING.md +18 -0
- data/LICENSE.md +7 -0
- data/Rakefile +19 -0
- data/lib/ticketing_hub.rb +22 -0
- data/lib/ticketing_hub/authentication.rb +11 -0
- data/lib/ticketing_hub/client.rb +27 -0
- data/lib/ticketing_hub/collection.rb +52 -0
- data/lib/ticketing_hub/configuration.rb +67 -0
- data/lib/ticketing_hub/connection.rb +52 -0
- data/lib/ticketing_hub/consumer.rb +27 -0
- data/lib/ticketing_hub/error.rb +70 -0
- data/lib/ticketing_hub/order.rb +67 -0
- data/lib/ticketing_hub/request.rb +80 -0
- data/lib/ticketing_hub/resource.rb +168 -0
- data/lib/ticketing_hub/schema.rb +30 -0
- data/lib/ticketing_hub/ticket.rb +23 -0
- data/lib/ticketing_hub/tier.rb +24 -0
- data/lib/ticketing_hub/venue.rb +14 -0
- data/lib/ticketing_hub/version.rb +3 -0
- data/spec/helper.rb +87 -0
- data/spec/ticketing_hub/venue_spec.rb +0 -0
- data/ticketinghub.gemspec +30 -0
- metadata +168 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 130aebb5aef14ed672ce1cde1860e5e0b448ed02
|
4
|
+
data.tar.gz: 7d9fc8c378fae6e5c96674ec0d2ec9adda1c36d8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3874d989f510f5c9f311362bb73a2c6113fd46a558a9654884411756156ad986e3e439b759e184e1eeeeb7d1d8251abb7dbba439857a29ebf270a90ee764ed32
|
7
|
+
data.tar.gz: 97acdb59c6818625a952420cf489e6f5bf7245f23dcc624f788e982ec9da92e8cc438c0daf02a5686a542d8832e3fd015edd1ae82971f1b3b660de8ef826d536
|
data/.document
ADDED
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
## Submitting a Pull Request
|
2
|
+
1. [Fork the repository.][fork]
|
3
|
+
2. [Create a topic branch.][branch]
|
4
|
+
3. Add specs for your unimplemented feature or bug fix.
|
5
|
+
4. Run `bundle exec rake spec`. If your specs pass, return to step 3.
|
6
|
+
5. Implement your feature or bug fix.
|
7
|
+
6. Run `bundle exec rake spec`. If your specs fail, return to step 5.
|
8
|
+
7. Run `open coverage/index.html`. If your changes are not completely covered
|
9
|
+
by your tests, return to step 3.
|
10
|
+
8. Add documentation for your feature or bug fix.
|
11
|
+
9. Run `bundle exec rake doc:yard`. If your changes are not 100% documented, go
|
12
|
+
back to step 8.
|
13
|
+
10. Add, commit, and push your changes.
|
14
|
+
11. [Submit a pull request.][pr]
|
15
|
+
|
16
|
+
[fork]: https://help.github.com/articles/fork-a-repo
|
17
|
+
[branch]: http://learn.github.com/p/branching.html
|
18
|
+
[pr]: https://help.github.com/articles/using-pull-requests
|
data/LICENSE.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2013 TicketingHub Ltd
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
RSpec::Core::RakeTask.new(:spec)
|
6
|
+
|
7
|
+
task :test => :spec
|
8
|
+
task :default => :spec
|
9
|
+
|
10
|
+
namespace :doc do
|
11
|
+
require 'yard'
|
12
|
+
YARD::Rake::YardocTask.new do |task|
|
13
|
+
task.files = ['README.md', 'LICENSE.md', 'lib/**/*.rb']
|
14
|
+
task.options = [
|
15
|
+
'--output-dir', 'doc/yard',
|
16
|
+
'--markup', 'markdown',
|
17
|
+
]
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative 'ticketing_hub/configuration'
|
2
|
+
require_relative 'ticketing_hub/client'
|
3
|
+
require_relative 'ticketing_hub/error'
|
4
|
+
|
5
|
+
module TicketingHub
|
6
|
+
extend Configuration
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def new(options={})
|
10
|
+
TicketingHub::Client.new options
|
11
|
+
end
|
12
|
+
|
13
|
+
def method_missing(method, *args, &block)
|
14
|
+
return super unless new.respond_to? method
|
15
|
+
new.send method, *args, &block
|
16
|
+
end
|
17
|
+
|
18
|
+
def respond_to?(method, include_private=false)
|
19
|
+
new.respond_to?(method, include_private) || super(method, include_private)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative 'authentication'
|
2
|
+
require_relative 'connection'
|
3
|
+
require_relative 'request'
|
4
|
+
require_relative 'resource'
|
5
|
+
|
6
|
+
require_relative 'venue'
|
7
|
+
require_relative 'order'
|
8
|
+
require_relative 'consumer'
|
9
|
+
require_relative 'tier'
|
10
|
+
require_relative 'ticket'
|
11
|
+
|
12
|
+
module TicketingHub
|
13
|
+
class Client
|
14
|
+
include TicketingHub::Authentication
|
15
|
+
include TicketingHub::Connection
|
16
|
+
include TicketingHub::Request
|
17
|
+
|
18
|
+
attr_accessor(*Configuration::VALID_OPTIONS_KEYS)
|
19
|
+
|
20
|
+
def initialize(options={})
|
21
|
+
options = TicketingHub.options.merge(options)
|
22
|
+
Configuration::VALID_OPTIONS_KEYS.each do |key|
|
23
|
+
send("#{key}=", options[key])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
module TicketingHub
|
4
|
+
class Collection
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def self.from_association parent, child_class, options={}
|
8
|
+
path = [ parent.model_name.route_key, parent.id,
|
9
|
+
child_class.model_name.route_key ].join '/'
|
10
|
+
new path, parent, child_class
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :elements, :parent, :child_class
|
14
|
+
|
15
|
+
def initialize(elements, parent, child_class)
|
16
|
+
self.elements = elements
|
17
|
+
self.parent = parent
|
18
|
+
self.child_class = child_class
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_a
|
22
|
+
elements.dup
|
23
|
+
end
|
24
|
+
|
25
|
+
def elements
|
26
|
+
unless @elements.is_a? Array
|
27
|
+
@elements = parent.client.get(path, options)
|
28
|
+
.map { |attrs| child_class.new attrs, parent.client }
|
29
|
+
end
|
30
|
+
|
31
|
+
@elements
|
32
|
+
end
|
33
|
+
|
34
|
+
def method_missing(method, *args, &block)
|
35
|
+
return super unless [].respond_to? method
|
36
|
+
elements.send method, *args, &block
|
37
|
+
end
|
38
|
+
|
39
|
+
def build(attributes = {})
|
40
|
+
child_class.new.tap do |resource|
|
41
|
+
{ parent.foreign_key => parent.id,
|
42
|
+
parent.model_name.singular_route_key => parent }.merge(attributes).each do |key, value|
|
43
|
+
resource.send "#{key}=", value if resource.respond_to? "#{key}="
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def create(*args)
|
49
|
+
build.create *args
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require_relative 'version'
|
3
|
+
|
4
|
+
module TicketingHub
|
5
|
+
module Configuration
|
6
|
+
|
7
|
+
VALID_OPTIONS_KEYS = [
|
8
|
+
:adapter,
|
9
|
+
:faraday_config_block,
|
10
|
+
:api_version,
|
11
|
+
:api_endpoint,
|
12
|
+
:web_endpoint,
|
13
|
+
:status_api_endpoint,
|
14
|
+
:token,
|
15
|
+
:secret,
|
16
|
+
:proxy,
|
17
|
+
:user_agent,
|
18
|
+
:request_host,
|
19
|
+
:auto_traversal].freeze
|
20
|
+
|
21
|
+
DEFAULT_ADAPTER = Faraday.default_adapter
|
22
|
+
DEFAULT_API_VERSION = 1
|
23
|
+
DEFAULT_API_ENDPOINT = ENV['TH_API_ENDPOINT'] || 'https://api.ticketinghub.com/reseller/'
|
24
|
+
DEFAULT_USER_AGENT = "TicketingHub Ruby Gem #{TicketingHub::VERSION}".freeze
|
25
|
+
DEFAULT_AUTO_TRAVERSAL = false
|
26
|
+
|
27
|
+
attr_accessor(*VALID_OPTIONS_KEYS)
|
28
|
+
|
29
|
+
def self.extended(base)
|
30
|
+
base.reset
|
31
|
+
end
|
32
|
+
|
33
|
+
def configure
|
34
|
+
yield self
|
35
|
+
end
|
36
|
+
|
37
|
+
def options
|
38
|
+
VALID_OPTIONS_KEYS.each_with_object Hash.new do |key, memo|
|
39
|
+
memo[key] = send key
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def api_endpoint=(value)
|
44
|
+
@api_endpoint = File.join value, ''
|
45
|
+
end
|
46
|
+
|
47
|
+
def web_endpoint=(value)
|
48
|
+
@web_endpoint = File.join value, ''
|
49
|
+
end
|
50
|
+
|
51
|
+
def faraday_config(&block)
|
52
|
+
@faraday_config_block = block
|
53
|
+
end
|
54
|
+
|
55
|
+
def reset
|
56
|
+
self.adapter = DEFAULT_ADAPTER
|
57
|
+
self.api_version = DEFAULT_API_VERSION
|
58
|
+
self.api_endpoint = DEFAULT_API_ENDPOINT
|
59
|
+
self.token = nil
|
60
|
+
self.secret = nil
|
61
|
+
self.proxy = nil
|
62
|
+
self.request_host = nil
|
63
|
+
self.user_agent = DEFAULT_USER_AGENT
|
64
|
+
self.auto_traversal = DEFAULT_AUTO_TRAVERSAL
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'faraday_middleware'
|
2
|
+
require_relative 'error'
|
3
|
+
|
4
|
+
module TicketingHub
|
5
|
+
class ErrorHandler < Faraday::Response::Middleware
|
6
|
+
ERROR_MAP = {
|
7
|
+
400 => TicketingHub::BadRequest,
|
8
|
+
401 => TicketingHub::Unauthorized,
|
9
|
+
403 => TicketingHub::Forbidden,
|
10
|
+
404 => TicketingHub::NotFound,
|
11
|
+
406 => TicketingHub::NotAcceptable,
|
12
|
+
422 => TicketingHub::UnprocessableEntity,
|
13
|
+
500 => TicketingHub::InternalServerError,
|
14
|
+
501 => TicketingHub::NotImplemented,
|
15
|
+
502 => TicketingHub::BadGateway,
|
16
|
+
503 => TicketingHub::ServiceUnavailable
|
17
|
+
}
|
18
|
+
|
19
|
+
def on_complete(response)
|
20
|
+
key = response[:status].to_i
|
21
|
+
raise ERROR_MAP[key].new(response) if ERROR_MAP.has_key? key
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# @private
|
26
|
+
module Connection
|
27
|
+
private
|
28
|
+
def connection(options={})
|
29
|
+
options = {
|
30
|
+
authenticate: true,
|
31
|
+
force_urlencoded: false,
|
32
|
+
raw: false,
|
33
|
+
ssl: { verify: false }
|
34
|
+
}.merge(options)
|
35
|
+
|
36
|
+
options.merge! proxy: proxy unless proxy.nil?
|
37
|
+
|
38
|
+
# TODO: Don't build on every request
|
39
|
+
Faraday.new(options) do |builder|
|
40
|
+
builder.request options[:force_urlencoded] ? :url_encoded : :json
|
41
|
+
builder.use ErrorHandler
|
42
|
+
builder.use FaradayMiddleware::FollowRedirects
|
43
|
+
builder.use FaradayMiddleware::ParseJson, content_type: /\bjson$/
|
44
|
+
faraday_config_block.call builder if faraday_config_block
|
45
|
+
builder.adapter *adapter
|
46
|
+
end.tap do |connection|
|
47
|
+
connection.headers[:user_agent] = user_agent
|
48
|
+
connection.basic_auth authentication[:token], authentication[:secret]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module TicketingHub
|
2
|
+
class Consumer < Resource
|
3
|
+
schema do
|
4
|
+
integer :id
|
5
|
+
string :first_name, :last_name, :email, :country
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.from_fields fields
|
9
|
+
new.tap do |consumer|
|
10
|
+
consumer.instance_eval do
|
11
|
+
@schema ||= singleton_class.schema do
|
12
|
+
fields.each do |field|
|
13
|
+
type = Schema::KNOWN_ATTRIBUTE_TYPES.member?(field['type']) ? field['type'] : 'string'
|
14
|
+
send type, field['name']
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
fields.each do |field|
|
19
|
+
singleton_class.validates_presence_of field['name'] if field['required']
|
20
|
+
send "#{field['name']}=", field['value']
|
21
|
+
field['errors'].each { |error| errors.add field['name'], error }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module TicketingHub
|
2
|
+
# Custom error class for rescuing from all GitHub errors
|
3
|
+
class Error < StandardError
|
4
|
+
attr_accessor :response
|
5
|
+
|
6
|
+
def initialize(response=nil)
|
7
|
+
@response = response
|
8
|
+
super build_error_message
|
9
|
+
end
|
10
|
+
|
11
|
+
def response_body
|
12
|
+
@response_body ||=
|
13
|
+
if (body = @response[:body]) && !body.empty?
|
14
|
+
if body.is_a?(String)
|
15
|
+
MultiJson.load(body, :symbolize_keys => true)
|
16
|
+
else
|
17
|
+
body
|
18
|
+
end
|
19
|
+
else
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def build_error_message
|
27
|
+
return nil if @response.nil?
|
28
|
+
|
29
|
+
message = if response_body
|
30
|
+
": #{response_body[:error] || response_body[:message] || ''}"
|
31
|
+
else
|
32
|
+
''
|
33
|
+
end
|
34
|
+
errors = unless message.empty?
|
35
|
+
response_body[:errors] ? ": #{response_body[:errors].map{|e|e[:message]}.join(', ')}" : ''
|
36
|
+
end
|
37
|
+
"#{@response[:method].to_s.upcase} #{@response[:url].to_s}: #{@response[:status]}#{message}#{errors}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Raised when GitHub returns a 400 HTTP status code
|
42
|
+
class BadRequest < Error; end
|
43
|
+
|
44
|
+
# Raised when GitHub returns a 401 HTTP status code
|
45
|
+
class Unauthorized < Error; end
|
46
|
+
|
47
|
+
# Raised when GitHub returns a 403 HTTP status code
|
48
|
+
class Forbidden < Error; end
|
49
|
+
|
50
|
+
# Raised when GitHub returns a 404 HTTP status code
|
51
|
+
class NotFound < Error; end
|
52
|
+
|
53
|
+
# Raised when GitHub returns a 406 HTTP status code
|
54
|
+
class NotAcceptable < Error; end
|
55
|
+
|
56
|
+
# Raised when GitHub returns a 422 HTTP status code
|
57
|
+
class UnprocessableEntity < Error; end
|
58
|
+
|
59
|
+
# Raised when GitHub returns a 500 HTTP status code
|
60
|
+
class InternalServerError < Error; end
|
61
|
+
|
62
|
+
# Raised when GitHub returns a 501 HTTP status code
|
63
|
+
class NotImplemented < Error; end
|
64
|
+
|
65
|
+
# Raised when GitHub returns a 502 HTTP status code
|
66
|
+
class BadGateway < Error; end
|
67
|
+
|
68
|
+
# Raised when GitHub returns a 503 HTTP status code
|
69
|
+
class ServiceUnavailable < Error; end
|
70
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'money'
|
2
|
+
require 'active_support'
|
3
|
+
|
4
|
+
module TicketingHub
|
5
|
+
class Order < Resource
|
6
|
+
schema do
|
7
|
+
integer :id, :venue_id, :consumer_id
|
8
|
+
float :commission, :total, default: 0
|
9
|
+
string :currency
|
10
|
+
datetime :confirmed_at
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :consumer_fields
|
14
|
+
|
15
|
+
has_many :tickets
|
16
|
+
|
17
|
+
belongs_to :venue
|
18
|
+
belongs_to :consumer
|
19
|
+
|
20
|
+
def total
|
21
|
+
attributes[:total].to_money currency
|
22
|
+
end
|
23
|
+
|
24
|
+
def commission
|
25
|
+
attributes[:commission].to_money currency
|
26
|
+
end
|
27
|
+
|
28
|
+
def consumer_fields
|
29
|
+
@consumer_fields ||= client.get("venues/#{venue_id}/orders/new")['consumer_fields'].map do |field|
|
30
|
+
HashWithIndifferentAccess.new field
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def tickets=(tickets)
|
35
|
+
@tickets = tickets.map { |ticket| Ticket.new ticket, client }
|
36
|
+
end
|
37
|
+
|
38
|
+
def consumer_fields=(fields)
|
39
|
+
@consumer_fields = fields
|
40
|
+
self.consumer = Consumer.from_fields consumer_fields
|
41
|
+
end
|
42
|
+
|
43
|
+
def pdf
|
44
|
+
client.get("orders/#{id}", accept: 'application/pdf')
|
45
|
+
end
|
46
|
+
|
47
|
+
def create attributes={}
|
48
|
+
self.attributes = attributes
|
49
|
+
if tiers = attributes.delete(:tiers)
|
50
|
+
tickets = attributes[:tickets_attributes] ||= []
|
51
|
+
tiers.each do |tier_id, qty|
|
52
|
+
qty.to_i.times { tickets.push tier_id: tier_id.to_i }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
if (consumer = attributes.delete(:consumer)).try :persisted?
|
57
|
+
(attributes[:consumer_attributes] ||= {})[:email] = consumer.email
|
58
|
+
end
|
59
|
+
|
60
|
+
venue.orders.build client.post("venues/#{venue_id}/orders", attributes)
|
61
|
+
rescue TicketingHub::BadRequest => error
|
62
|
+
self.tap { errors.add :tickets, error.response_body['message'] }
|
63
|
+
rescue TicketingHub::UnprocessableEntity => error
|
64
|
+
venue.orders.build error.response_body
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
|
3
|
+
module TicketingHub
|
4
|
+
module Request
|
5
|
+
|
6
|
+
def delete(path, options={})
|
7
|
+
request(:delete, path, options).body
|
8
|
+
end
|
9
|
+
|
10
|
+
def get(path, options={})
|
11
|
+
response = request(:get, path, options)
|
12
|
+
body = response.body
|
13
|
+
|
14
|
+
if auto_traversal && body.is_a?(Array)
|
15
|
+
while next_url = links(response)['next']
|
16
|
+
response = request(:get, next_url, options)
|
17
|
+
body += response.body
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
body
|
22
|
+
end
|
23
|
+
|
24
|
+
def patch(path, options={})
|
25
|
+
request(:patch, path, options).body
|
26
|
+
end
|
27
|
+
|
28
|
+
def post(path, options={})
|
29
|
+
request(:post, path, options).body
|
30
|
+
end
|
31
|
+
|
32
|
+
def put(path, options={})
|
33
|
+
request(:put, path, options).body
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Executes the request, checking if it was successful
|
39
|
+
#
|
40
|
+
# @return [Boolean] True on success, false otherwise
|
41
|
+
def boolean_from_response(method, path, options={})
|
42
|
+
request(method, path, options).status == 204
|
43
|
+
rescue TicketingHub::NotFound
|
44
|
+
false
|
45
|
+
end
|
46
|
+
|
47
|
+
def request(method, path, options={})
|
48
|
+
force_urlencoded = options.delete(:force_urlencoded) || false
|
49
|
+
url = options.delete(:endpoint) || api_endpoint
|
50
|
+
conn_options = { force_urlencoded: force_urlencoded, url: url }
|
51
|
+
|
52
|
+
connection(conn_options).send(method) do |request|
|
53
|
+
request.headers['Accept'] = options.delete(:accept) || 'application/json'
|
54
|
+
|
55
|
+
case method
|
56
|
+
when :get, :delete, :head
|
57
|
+
request.url(path, options)
|
58
|
+
when :patch, :post, :put
|
59
|
+
request.path = path
|
60
|
+
unless options.empty?
|
61
|
+
request.body = force_urlencoded ? options : MultiJson.dump(options)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if TicketingHub.request_host
|
66
|
+
request.headers['Host'] = TicketingHub.request_host
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def links(response)
|
72
|
+
links = ( response.headers["Link"] || "" ).split(', ').map do |link|
|
73
|
+
url, type = link.match(/<(.*?)>; rel="(\w+)"/).captures
|
74
|
+
[ type, url ]
|
75
|
+
end
|
76
|
+
|
77
|
+
Hash[ *links.flatten ]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,168 @@
|
|
1
|
+
require 'active_model'
|
2
|
+
require 'active_support'
|
3
|
+
require_relative 'schema'
|
4
|
+
require_relative 'collection'
|
5
|
+
|
6
|
+
module TicketingHub
|
7
|
+
class Resource
|
8
|
+
extend ActiveModel::Callbacks
|
9
|
+
extend ActiveModel::Naming
|
10
|
+
|
11
|
+
include ActiveModel::AttributeMethods
|
12
|
+
include ActiveModel::Serialization
|
13
|
+
include ActiveModel::Serializers::JSON
|
14
|
+
include ActiveModel::Validations
|
15
|
+
include ActiveModel::Dirty
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def has_many key
|
19
|
+
attr_accessor key
|
20
|
+
define_method key do
|
21
|
+
klass = "TicketingHub::#{key.to_s.singularize.classify}".constantize
|
22
|
+
value = instance_variable_get "@#{key}"
|
23
|
+
value || Collection.from_association(self, klass).tap do |collection|
|
24
|
+
instance_variable_set "@#{key}", collection
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
define_method "#{key}=" do |values|
|
29
|
+
klass = "TicketingHub::#{key.to_s.singularize.classify}".constantize
|
30
|
+
instance_variable_set "@#{key}", Collection.new(values.map do |value|
|
31
|
+
value.is_a?(klass) ? value : send(key).build(value)
|
32
|
+
end, self, klass)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def belongs_to key
|
37
|
+
attr_accessor key
|
38
|
+
|
39
|
+
define_method "#{key}=" do |value|
|
40
|
+
klass = "TicketingHub::#{key.to_s.classify}".constantize
|
41
|
+
value = value.is_a?(Hash) ? klass.new(value.merge({
|
42
|
+
foreign_key => id,
|
43
|
+
model_name.singular_route_key => self
|
44
|
+
})) : value
|
45
|
+
send "#{klass.foreign_key}=", value.id
|
46
|
+
instance_variable_set "@#{key}", value
|
47
|
+
end
|
48
|
+
|
49
|
+
define_method key do
|
50
|
+
klass = "TicketingHub::#{key.to_s.classify}".constantize
|
51
|
+
value = instance_variable_get "@#{key}"
|
52
|
+
if value.nil? && (fk = send(klass.foreign_key)).present?
|
53
|
+
klass.find(fk).tap do |record|
|
54
|
+
instance_variable_set "@#{key}", record
|
55
|
+
end
|
56
|
+
else value end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def create attributes={}
|
61
|
+
new.create attributes
|
62
|
+
end
|
63
|
+
|
64
|
+
def model_name
|
65
|
+
@_model_name ||= ActiveModel::Name.new self, TicketingHub
|
66
|
+
end
|
67
|
+
|
68
|
+
def foreign_key
|
69
|
+
name.to_s.foreign_key
|
70
|
+
end
|
71
|
+
|
72
|
+
def default_client
|
73
|
+
TicketingHub::Client.new
|
74
|
+
end
|
75
|
+
|
76
|
+
def all
|
77
|
+
default_client.get(model_name.route_key).map { |attributes| new attributes }
|
78
|
+
end
|
79
|
+
|
80
|
+
def find id
|
81
|
+
new default_client.get("#{model_name.route_key}/#{id}")
|
82
|
+
end
|
83
|
+
|
84
|
+
def schema(&block)
|
85
|
+
if block_given?
|
86
|
+
undefine_attribute_methods @schema.keys if @schema.present?
|
87
|
+
@schema = TicketingHub::Schema.new(&block).tap do |schema|
|
88
|
+
attr_accessor *schema.keys
|
89
|
+
define_attribute_methods schema.keys
|
90
|
+
end
|
91
|
+
else
|
92
|
+
@schema ||= TicketingHub::Schema.new
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def endpoint=(value)
|
97
|
+
@endpoint = value unless value.empty?
|
98
|
+
end
|
99
|
+
|
100
|
+
def endpoint
|
101
|
+
@endpoint || model_name.plural
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
delegate :foreign_key, :model_name, :schema, to: :class
|
106
|
+
|
107
|
+
attr_accessor :client, :errors, :attributes
|
108
|
+
define_model_callbacks :create, :save, :update, :destroy
|
109
|
+
|
110
|
+
def initialize(attributes = {}, client = self.class.default_client)
|
111
|
+
self.client = client
|
112
|
+
self.attributes = attributes
|
113
|
+
@errors = ActiveModel::Errors.new self
|
114
|
+
end
|
115
|
+
|
116
|
+
def ==(other)
|
117
|
+
other.equal?(self) ||
|
118
|
+
(other.instance_of?(self.class) && other.id == id && ! other.new_record?)
|
119
|
+
end
|
120
|
+
|
121
|
+
def ===(other)
|
122
|
+
object.is_a? self
|
123
|
+
end
|
124
|
+
|
125
|
+
def eql?(other)
|
126
|
+
self == other
|
127
|
+
end
|
128
|
+
|
129
|
+
def hash
|
130
|
+
id.hash
|
131
|
+
end
|
132
|
+
|
133
|
+
def endpoint
|
134
|
+
new_record?? model_name.plural : [model_name.singular, id].join('/')
|
135
|
+
end
|
136
|
+
|
137
|
+
def create attributes={}
|
138
|
+
throw 'Cannot create existing record.' if persisted?
|
139
|
+
self.attributes = client.post(endpoint, self.attributes.merge(attributes))
|
140
|
+
end
|
141
|
+
|
142
|
+
def new_record?
|
143
|
+
id.nil?
|
144
|
+
end
|
145
|
+
|
146
|
+
def persisted?
|
147
|
+
! new_record?
|
148
|
+
end
|
149
|
+
|
150
|
+
def attributes
|
151
|
+
schema.keys.each_with_object Hash.new do |key, memo|
|
152
|
+
memo[key.to_sym] = instance_variable_get "@#{key}"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def attributes=(values)
|
157
|
+
values.each do |key, value|
|
158
|
+
send("#{key}=", value) if respond_to? "#{key}="
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def column_for_attribute(name)
|
163
|
+
Class.new do
|
164
|
+
def type; schema[name] end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module TicketingHub
|
2
|
+
class Schema
|
3
|
+
KNOWN_ATTRIBUTE_TYPES = %w(string text integer float decimal datetime timestamp time date binary boolean)
|
4
|
+
|
5
|
+
def [] key
|
6
|
+
@attrs[key.to_s].to_sym
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize &block
|
10
|
+
@attrs = Hash.new
|
11
|
+
instance_eval &block if block_given?
|
12
|
+
end
|
13
|
+
|
14
|
+
def keys
|
15
|
+
@attrs.keys
|
16
|
+
end
|
17
|
+
|
18
|
+
def attribute name, type, options = {}
|
19
|
+
raise ArgumentError, "Unknown Attribute type: #{type.inspect} for key: #{name.inspect}" unless type.nil? || Schema::KNOWN_ATTRIBUTE_TYPES.include?(type.to_s)
|
20
|
+
tap { @attrs[name.to_s] = [type.to_s, options] }
|
21
|
+
end
|
22
|
+
|
23
|
+
KNOWN_ATTRIBUTE_TYPES.each do |type|
|
24
|
+
define_method type do |*args|
|
25
|
+
options = args.last.is_a?(::Hash) ? args.pop : {}
|
26
|
+
args.each { |name| attribute name, type, options }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'money'
|
2
|
+
|
3
|
+
module TicketingHub
|
4
|
+
class Ticket < Resource
|
5
|
+
schema do
|
6
|
+
integer :id, :tier_id, :order_id
|
7
|
+
float :commission, :price, default: 0
|
8
|
+
string :code
|
9
|
+
end
|
10
|
+
|
11
|
+
belongs_to :tier
|
12
|
+
belongs_to :order
|
13
|
+
delegate :currency, to: :order
|
14
|
+
|
15
|
+
def price
|
16
|
+
attributes[:price].to_money currency
|
17
|
+
end
|
18
|
+
|
19
|
+
def commission
|
20
|
+
attributes[:commission].to_money currency
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'money'
|
2
|
+
|
3
|
+
module TicketingHub
|
4
|
+
class Tier < Resource
|
5
|
+
schema do
|
6
|
+
integer :id, :venue_id
|
7
|
+
string :name
|
8
|
+
text :description
|
9
|
+
float :commission, :price, default: 0
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :venue
|
13
|
+
delegate :currency, to: :venue
|
14
|
+
belongs_to :venue
|
15
|
+
|
16
|
+
def price
|
17
|
+
attributes[:price].to_money currency
|
18
|
+
end
|
19
|
+
|
20
|
+
def commission
|
21
|
+
attributes[:commission].to_money currency
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module TicketingHub
|
2
|
+
class Venue < Resource
|
3
|
+
attr_accessor :tiers, :orders
|
4
|
+
|
5
|
+
schema do
|
6
|
+
integer :id
|
7
|
+
string :name, :street_1, :street_2, :city, :region, :postcode, :country, :category, :currency
|
8
|
+
float :lat, :lng
|
9
|
+
end
|
10
|
+
|
11
|
+
has_many :tiers
|
12
|
+
has_many :orders
|
13
|
+
end
|
14
|
+
end
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
require 'coveralls'
|
3
|
+
|
4
|
+
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
|
5
|
+
SimpleCov::Formatter::HTMLFormatter,
|
6
|
+
Coveralls::SimpleCov::Formatter
|
7
|
+
]
|
8
|
+
SimpleCov.start
|
9
|
+
|
10
|
+
require 'ticketing_hub'
|
11
|
+
require 'rspec'
|
12
|
+
require 'webmock/rspec'
|
13
|
+
|
14
|
+
WebMock.disable_net_connect! allow: 'coveralls.io'
|
15
|
+
|
16
|
+
RSpec.configure do |config|
|
17
|
+
config.expect_with :rspec do |c|
|
18
|
+
c.syntax = :expect
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def a_delete(url)
|
23
|
+
a_request(:delete, ticketinghub_url(url))
|
24
|
+
end
|
25
|
+
|
26
|
+
def a_get(url)
|
27
|
+
a_request(:get, ticketinghub_url(url))
|
28
|
+
end
|
29
|
+
|
30
|
+
def a_patch(url)
|
31
|
+
a_request(:patch, ticketinghub_url(url))
|
32
|
+
end
|
33
|
+
|
34
|
+
def a_post(url)
|
35
|
+
a_request(:post, ticketinghub_url(url))
|
36
|
+
end
|
37
|
+
|
38
|
+
def a_put(url)
|
39
|
+
a_request(:put, ticketinghub_url(url))
|
40
|
+
end
|
41
|
+
|
42
|
+
def stub_delete(url)
|
43
|
+
stub_request(:delete, ticketinghub_url(url))
|
44
|
+
end
|
45
|
+
|
46
|
+
def stub_get(url)
|
47
|
+
stub_request(:get, ticketinghub_url(url))
|
48
|
+
end
|
49
|
+
|
50
|
+
def stub_head(url)
|
51
|
+
stub_request(:head, ticketinghub_url(url))
|
52
|
+
end
|
53
|
+
|
54
|
+
def stub_patch(url)
|
55
|
+
stub_request(:patch, ticketinghub_url(url))
|
56
|
+
end
|
57
|
+
|
58
|
+
def stub_post(url)
|
59
|
+
stub_request(:post, ticketinghub_url(url))
|
60
|
+
end
|
61
|
+
|
62
|
+
def stub_put(url)
|
63
|
+
stub_request(:put, ticketinghub_url(url))
|
64
|
+
end
|
65
|
+
|
66
|
+
def fixture_path
|
67
|
+
File.expand_path("../fixtures", __FILE__)
|
68
|
+
end
|
69
|
+
|
70
|
+
def fixture(file)
|
71
|
+
File.new(fixture_path + '/' + file)
|
72
|
+
end
|
73
|
+
|
74
|
+
def json_response(file)
|
75
|
+
{ body: fixture(file),
|
76
|
+
headers: { content_type: 'application/json; charset=utf-8' } }
|
77
|
+
end
|
78
|
+
|
79
|
+
def ticketinghub_url(url)
|
80
|
+
if url =~ /^http/
|
81
|
+
url
|
82
|
+
elsif @client && @client.authenticated?
|
83
|
+
"https://#{@client.token}:#{@client.secret}@api.ticketinghub.com#{url}"
|
84
|
+
else
|
85
|
+
"https://api.ticketinghub.com#{url}"
|
86
|
+
end
|
87
|
+
end
|
File without changes
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'ticketing_hub/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.add_development_dependency 'bundler', '~> 1.0'
|
8
|
+
spec.add_dependency 'addressable', '~> 2.3.3'
|
9
|
+
spec.add_dependency 'faraday', '~> 0.8.6'
|
10
|
+
spec.add_dependency 'faraday_middleware', '~> 0.9.0'
|
11
|
+
spec.add_dependency 'multi_json', '~> 1.6.1'
|
12
|
+
spec.add_dependency 'money', '~> 5.1.1'
|
13
|
+
spec.add_dependency 'activemodel', '>= 3.2.12'
|
14
|
+
spec.authors = ['Oliver Morgan']
|
15
|
+
spec.description = %q{Wrapper for TicketingHub API}
|
16
|
+
spec.email = ['olly@ticketinghub.com']
|
17
|
+
spec.files = %w(.document CONTRIBUTING.md LICENSE.md Rakefile ticketinghub.gemspec)
|
18
|
+
spec.files += Dir['lib/**/*.rb']
|
19
|
+
spec.files += Dir['spec/**/*']
|
20
|
+
spec.homepage = 'https://github.com/ticketinghub/ruby-api'
|
21
|
+
spec.licenses = ['MIT']
|
22
|
+
spec.name = 'ticketinghub'
|
23
|
+
spec.platform = Gem::Platform::RUBY
|
24
|
+
spec.required_ruby_version = '>= 1.9.3'
|
25
|
+
spec.require_paths = ['lib']
|
26
|
+
spec.required_rubygems_version = '>= 1.3.6'
|
27
|
+
spec.summary = spec.description
|
28
|
+
spec.test_files = Dir['spec/**/*']
|
29
|
+
spec.version = TicketingHub::VERSION
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ticketinghub
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Oliver Morgan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-03-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: addressable
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.3.3
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ~>
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.3.3
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: faraday
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ~>
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.8.6
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.8.6
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: faraday_middleware
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.9.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.9.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: multi_json
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ~>
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.6.1
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ~>
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.6.1
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: money
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 5.1.1
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ~>
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 5.1.1
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: activemodel
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 3.2.12
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 3.2.12
|
111
|
+
description: Wrapper for TicketingHub API
|
112
|
+
email:
|
113
|
+
- olly@ticketinghub.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- .document
|
119
|
+
- CONTRIBUTING.md
|
120
|
+
- LICENSE.md
|
121
|
+
- Rakefile
|
122
|
+
- ticketinghub.gemspec
|
123
|
+
- lib/ticketing_hub/authentication.rb
|
124
|
+
- lib/ticketing_hub/client.rb
|
125
|
+
- lib/ticketing_hub/collection.rb
|
126
|
+
- lib/ticketing_hub/configuration.rb
|
127
|
+
- lib/ticketing_hub/connection.rb
|
128
|
+
- lib/ticketing_hub/consumer.rb
|
129
|
+
- lib/ticketing_hub/error.rb
|
130
|
+
- lib/ticketing_hub/order.rb
|
131
|
+
- lib/ticketing_hub/request.rb
|
132
|
+
- lib/ticketing_hub/resource.rb
|
133
|
+
- lib/ticketing_hub/schema.rb
|
134
|
+
- lib/ticketing_hub/ticket.rb
|
135
|
+
- lib/ticketing_hub/tier.rb
|
136
|
+
- lib/ticketing_hub/venue.rb
|
137
|
+
- lib/ticketing_hub/version.rb
|
138
|
+
- lib/ticketing_hub.rb
|
139
|
+
- spec/helper.rb
|
140
|
+
- spec/ticketing_hub/venue_spec.rb
|
141
|
+
homepage: https://github.com/ticketinghub/ruby-api
|
142
|
+
licenses:
|
143
|
+
- MIT
|
144
|
+
metadata: {}
|
145
|
+
post_install_message:
|
146
|
+
rdoc_options: []
|
147
|
+
require_paths:
|
148
|
+
- lib
|
149
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - '>='
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: 1.9.3
|
154
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - '>='
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: 1.3.6
|
159
|
+
requirements: []
|
160
|
+
rubyforge_project:
|
161
|
+
rubygems_version: 2.0.0
|
162
|
+
signing_key:
|
163
|
+
specification_version: 4
|
164
|
+
summary: Wrapper for TicketingHub API
|
165
|
+
test_files:
|
166
|
+
- spec/helper.rb
|
167
|
+
- spec/ticketing_hub/venue_spec.rb
|
168
|
+
has_rdoc:
|