require 'httparty'
gem 'multipart-post'
require 'net/http/post/multipart'
gem 'mime-types'
require 'mime/types'
# multipart POST support from David Balater's fork of HTTParty:
# http://github.com/dbalatero/httparty
# :stopdoc:
module HTTParty
module ClassMethods
def post(path, options={})
klass = options[:multipart] ? Net::HTTP::Post::Multipart : Net::HTTP::Post
perform_request klass, path, options
end
end
class Request
SupportedHTTPMethods << Net::HTTP::Post::Multipart
private
def setup_raw_request
if multipart?
@file_handles = []
io_objects = {}
options[:multipart].each do |field_name, info|
fp = File.open(info[:path])
@file_handles << fp
io_objects[field_name] = UploadIO.new(fp,
info[:type],
info[:path])
end
if options[:body]
options[:body].each do |field_name, value|
io_objects[field_name] = value
end
end
@raw_request = http_method.new(uri.request_uri,
io_objects)
# We have to duplicate and merge the headers set by the
# multipart object to make sure that Net::HTTP
# doesn't override them down the line when it calls
# initialize_http_header.
#
# Otherwise important headers like Content-Length,
# Accept, and Content-Type will be deleted.
original_headers = {}
@raw_request.each do |key, value|
original_headers[key] = value
end
options[:headers] ||= {}
original_headers.merge!(options[:headers])
options[:headers] = original_headers
else
@raw_request = http_method.new(uri.request_uri)
@raw_request.body = body if body
end
@raw_request.initialize_http_header options[:headers]
@raw_request.basic_auth(username, password) if options[:basic_auth]
end
def multipart?
Net::HTTP::Post::Multipart == http_method
end
end
end
# :startdoc:
class WuParty
include HTTParty
format :json
VERSION = '1.0.2'
# Represents a general error connecting to the Wufoo service
class ConnectionError < RuntimeError; end
# Represents a specific error returned from Wufoo.
class HTTPError < RuntimeError
def initialize(code, message) # :nodoc:
@code = code
super(message)
end
# Error code
attr_reader :code
end
ENDPOINT = 'https://%s.wufoo.com/api/v3'
API_VERSION = '3.0'
# Create a new WuParty object
def initialize(account, api_key)
@account = account
@api_key = api_key
@field_numbers = {}
end
# Returns list of forms and details accessible by the user account.
def forms
get(:forms)['Forms'].map do |details|
Form.new(details['Url'], :party => self, :details => details)
end
end
# Returns list of reports and details accessible by the user account.
def reports
get(:reports)['Reports'].map do |details|
Report.new(details['Url'], :party => self, :details => details)
end
end
# Returns list of users and details.
def users
get(:users)['Users'].map do |details|
User.new(details['Url'], :party => self, :details => details)
end
end
# Returns details about the specified form.
def form(form_id)
if f = get("forms/#{form_id}")['Forms']
Form.new(f.first['Url'], :party => self, :details => f.first)
end
end
# Returns details about the specified report.
def report(report_id)
if r = get("reports/#{report_id}")['Reports']
Form.new(r.first['Url'], :party => self, :details => r.first)
end
end
def get(method, options={}) # :nodoc:
options.merge!(:basic_auth => {:username => @api_key})
url = "#{base_url}/#{method}.json"
result = self.class.get(url, options)
if result.is_a?(String)
raise ConnectionError, result
elsif result['HTTPCode']
raise HTTPError.new(result['HTTPCode'], result['Text'])
else
result
end
end
def post(method, options={}) # :nodoc:
options.merge!(:basic_auth => {:username => @api_key})
url = "#{base_url}/#{method}.json"
result = self.class.post(url, options)
if result.is_a?(String)
raise ConnectionError, result
elsif result['HTTPCode']
raise HTTPError.new(result['HTTPCode'], result['Text'])
else
result
end
end
private
def base_url
ENDPOINT % @account
end
public
# ----------
class Entity # :nodoc:
include HTTParty
format :json
def initialize(id, options)
@id = id
if options[:party]
@party = options[:party]
elsif options[:account] and options[:api_key]
@party = WuParty.new(options[:account], options[:api_key])
else
raise WuParty::InitializationException, "You must either specify a :party object or pass the :account and :api_key options. Please see the README."
end
@details = options[:details]
end
attr_accessor :details
end
# Wraps an individual Wufoo Form.
# == Instantiation
# There are two ways to instantiate a Form object:
# 1. Via the parent WuParty object that represents the account.
# 2. Via the WuParty::Form class directly.
# wufoo = WuParty.new(ACCOUNT, API_KEY)
# form = wufoo.form(FORM_ID)
# # or...
# form = WuParty::Form.new(FORM_ID, :account => ACCOUNT, :api_key => API_KEY)
# The first technique makes a call to the Wufoo API to get the form details,
# while the second technique lazily loads the form details, once something is accessed via [].
# == \Form Details
# Access form details like it is a Hash, e.g.:
# form['Name']
# form['RedirectMessage']
class Form < Entity
# Returns field details for the form.
def fields
@party.get("forms/#{@id}/fields")['Fields']
end
# Access form details.
def [](id)
@details ||= @party.form(@id)
@details[id]
end
# Returns fields and subfields, as a flattened array, e.g.
# [{'ID' => 'Field1', 'Title' => 'Name - First', 'Type' => 'shortname', 'Required' => true }, # (subfield)
# {'ID' => 'Field2', 'Title' => 'Name - Last', 'Type' => 'shortname', 'Required' => true }, # (subfield)
# {'ID' => 'Field3', 'Title' => 'Birthday', 'Type' => 'date', 'Required' => flase}] # (field)
# By default, only fields that can be submitted are returned. Pass *true* as the first arg to return all fields.
def flattened_fields(all=false)
flattened = []
fields.each do |field|
next unless all or field['ID'] =~ /^Field/
if field['SubFields']
field['SubFields'].each do |sub_field|
flattened << {'ID' => sub_field['ID'], 'Title' => field['Title'] + ' - ' + sub_field['Label'], 'Type' => field['Type'], 'Required' => field['IsRequired'] == '1'}
end
else
flattened << {'ID' => field['ID'], 'Title' => field['Title'], 'Type' => field['Type'], 'Required' => field['IsRequired'] == '1'}
end
end
flattened
end
# Return entries already submitted to the form.
#
# If you need to filter entries, pass an array as the first argument:
# entries([[field_id, operator, value], ...])
# e.g.:
# entries([['EntryId', 'Is_after', 12], ['EntryId', 'Is_before', 17]])
# entries([['Field1', 'Is_equal', 'Tim']])
# The second arg is the match parameter (AND/OR) and defaults to 'AND', e.g.
# entries([['Field2', 'Is_equal', 'Morgan'], ['Field2', 'Is_equal', 'Smith']], 'OR')
# See http://wufoo.com/docs/api/v3/entries/get/#filter for details
def entries(filters=[], filter_match='AND')
if filters.any?
options = {'match' => filter_match}
filters.each_with_index do |filter, index|
options["Filter#{index+1}"] = filter.join(' ')
end
options = {:query => options}
else
options = {}
end
@party.get("forms/#{@id}/entries", options)['Entries']
end
# Submit form data to the form.
# Pass data as a hash, with field ids as the hash keys, e.g.
# submit('Field1' => 'Tim', 'Field2' => 'Morgan')
# Return value is a Hash that includes the following keys:
# * Status
# * ErrorText
# * FieldErrors
# You must submit values for required fields (including all sub fields),
# and dates must be formatted as <tt>YYYYMMDD</tt>.
def submit(data)
options = {}
data.each do |key, value|
if value.is_a?(Hash)
type = MIME::Types.of(value[:path]).first.content_type rescue 'application/octet-stream'
options[:multipart] ||= {}
options[:multipart][key] = {:type => type, :path => value[:path]}
else
options[:body] ||= {}
options[:body][key] = value
end
end
@party.post("forms/#{@id}/entries", options)
end
# Returns comment details for the form.
# See Wufoo API documentation for possible options,
# e.g. to filter comments for a specific form entry:
# form.comments('entryId' => 123)
def comments(options={})
options = {:query => options} if options.any?
@party.get("forms/#{@id}/comments", options)['Comments']
end
end
# Wraps an individual Wufoo Report.
# == Instantiation
# There are two ways to instantiate a Report object:
# 1. Via the parent WuParty object that represents the account.
# 2. Via the WuParty::Report class directly.
# wufoo = WuParty.new(ACCOUNT, API_KEY)
# report = wufoo.report(REPORT_ID)
# # or...
# report = WuParty::Report.new(REPORT_ID, :account => ACCOUNT, :api_key => API_KEY)
# The first technique makes a call to the Wufoo API to get the report details,
# while the second technique lazily loads the report details, once something is accessed via [].
# == \Report Details
# Access report details like it is a Hash, e.g.:
# report['Name']
class Report < Entity
# Access report details.
def [](id)
@details ||= @party.report(@id)
@details[id]
end
# Returns field details for the report
def fields
@party.get("reports/#{@id}/fields")['Fields']
end
# Returns widget details for the report
def widgets
@party.get("reports/#{@id}/widgets")['Widgets']
end
end
# Wraps an individual Wufoo User.
class User < Entity
# Access user details.
def [](id)
@details ||= @party.report(@id)
@details[id]
end
end
end