soleone-highrise 0.13.3
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/.gitignore +1 -0
- data/CHANGELOG +91 -0
- data/MIT-LICENSE +20 -0
- data/README.mkdn +86 -0
- data/Rakefile +46 -0
- data/VERSION.yml +4 -0
- data/autotest/discover.rb +3 -0
- data/examples/config_initializers_highrise.rb +12 -0
- data/examples/extending.rb +31 -0
- data/examples/sample.rb +10 -0
- data/highrise.gemspec +131 -0
- data/install.rb +1 -0
- data/lib/cachable.rb +77 -0
- data/lib/highrise.rb +22 -0
- data/lib/highrise/base.rb +9 -0
- data/lib/highrise/comment.rb +4 -0
- data/lib/highrise/company.rb +14 -0
- data/lib/highrise/curlhelper.rb +27 -0
- data/lib/highrise/curly.rb +101 -0
- data/lib/highrise/deal.rb +35 -0
- data/lib/highrise/email.rb +9 -0
- data/lib/highrise/group.rb +7 -0
- data/lib/highrise/kase.rb +8 -0
- data/lib/highrise/membership.rb +4 -0
- data/lib/highrise/note.rb +9 -0
- data/lib/highrise/pagination.rb +29 -0
- data/lib/highrise/person.rb +43 -0
- data/lib/highrise/subject.rb +50 -0
- data/lib/highrise/tag.rb +18 -0
- data/lib/highrise/taggable.rb +33 -0
- data/lib/highrise/task.rb +26 -0
- data/lib/highrise/user.rb +11 -0
- data/spec/cachable_spec.rb +68 -0
- data/spec/highrise/base_spec.rb +13 -0
- data/spec/highrise/comment_spec.rb +14 -0
- data/spec/highrise/companies/16883216.html +1723 -0
- data/spec/highrise/company_spec.rb +70 -0
- data/spec/highrise/curlhelper_spec.rb +35 -0
- data/spec/highrise/curly_spec.rb +36 -0
- data/spec/highrise/email_spec.rb +25 -0
- data/spec/highrise/group_spec.rb +14 -0
- data/spec/highrise/kase_spec.rb +25 -0
- data/spec/highrise/membership_spec.rb +14 -0
- data/spec/highrise/note_spec.rb +23 -0
- data/spec/highrise/pagination_spec.rb +10 -0
- data/spec/highrise/people/16887003.html +1733 -0
- data/spec/highrise/person_spec.rb +83 -0
- data/spec/highrise/subject_spec.rb +43 -0
- data/spec/highrise/tag_spec.rb +24 -0
- data/spec/highrise/task_spec.rb +23 -0
- data/spec/highrise/user_spec.rb +30 -0
- data/spec/spec.opts +7 -0
- data/spec/spec_helper.rb +20 -0
- data/uninstall.rb +1 -0
- metadata +176 -0
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
data/lib/cachable.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# Caching is a way to speed up slow ActiveResource queries by keeping the result of
|
2
|
+
# a request around to be reused by subsequent requests.
|
3
|
+
#
|
4
|
+
# Caching is turned OFF by default.
|
5
|
+
#
|
6
|
+
# == Usage
|
7
|
+
#
|
8
|
+
# require 'cachable'
|
9
|
+
#
|
10
|
+
# module CachedResource
|
11
|
+
# class Base < ActiveResource::Base
|
12
|
+
# end
|
13
|
+
# class ActiveResource::Connection
|
14
|
+
# include Cachable
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
#
|
19
|
+
# == Caching stores
|
20
|
+
#
|
21
|
+
# All the caching stores from ActiveSupport::Cache are available
|
22
|
+
# as backends for caching. See the Rails rdoc for more information on
|
23
|
+
# these stores
|
24
|
+
#
|
25
|
+
# === Configuration examples ('off' is the default):
|
26
|
+
# CachedResource.connection.cache_store = ActiveSupport::Cache.lookup_store :memory_store
|
27
|
+
# CachedResource.connection.cache_store = ActiveSupport::Cache.lookup_store :file_store, "/path/to/cache/directory"
|
28
|
+
# CachedResource.connection.cache_store = ActiveSupport::Cache.lookup_store :drb_store, "druby://localhost:9192"
|
29
|
+
# CachedResource.connection.cache_store = ActiveSupport::Cache.lookup_store :mem_cache_store, "localhost"
|
30
|
+
# CachedResource.connection.cache_store = MyOwnStore.new("parameter")
|
31
|
+
#
|
32
|
+
# === If you are using a store that has write options, you can set them
|
33
|
+
# CachedResource.connection.store_options = { :expires_in => 60.seconds }
|
34
|
+
#
|
35
|
+
# Note: To ensure that caching is turned off, set CachedResource.connection.cache_store = nil
|
36
|
+
#
|
37
|
+
# FYI: You can use this with *any* active resource interface, not just Highrise.
|
38
|
+
|
39
|
+
module Cachable
|
40
|
+
def self.included(base)
|
41
|
+
base.class_eval do
|
42
|
+
include InstanceMethods
|
43
|
+
alias_method_chain :get, :cache
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
module InstanceMethods
|
48
|
+
attr_writer :cache_store, :store_options
|
49
|
+
|
50
|
+
def cache_store
|
51
|
+
@cache_store ||= nil
|
52
|
+
end
|
53
|
+
|
54
|
+
def store_options
|
55
|
+
@store_options ||= {}
|
56
|
+
end
|
57
|
+
|
58
|
+
def is_caching?
|
59
|
+
!@cache_store.nil?
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def get_with_cache(path, headers = {})
|
65
|
+
return get_without_cache(path, headers) unless is_caching?
|
66
|
+
fetch(path) { get_without_cache(path, headers) }
|
67
|
+
end
|
68
|
+
|
69
|
+
def cache_key(*args)
|
70
|
+
args.to_s
|
71
|
+
end
|
72
|
+
|
73
|
+
def fetch(args, &block)
|
74
|
+
cache_store.fetch(cache_key(args), store_options, &block).dup
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/lib/highrise.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'active_resource'
|
2
|
+
require 'activesupport'
|
3
|
+
require 'active_support/basic_object'
|
4
|
+
|
5
|
+
require File.dirname(__FILE__) + '/highrise/base'
|
6
|
+
require File.dirname(__FILE__) + '/highrise/pagination'
|
7
|
+
require File.dirname(__FILE__) + '/highrise/curly'
|
8
|
+
require File.dirname(__FILE__) + '/highrise/curlhelper'
|
9
|
+
require File.dirname(__FILE__) + '/highrise/taggable'
|
10
|
+
require File.dirname(__FILE__) + '/highrise/subject'
|
11
|
+
require File.dirname(__FILE__) + '/highrise/comment'
|
12
|
+
require File.dirname(__FILE__) + '/highrise/company'
|
13
|
+
require File.dirname(__FILE__) + '/highrise/email'
|
14
|
+
require File.dirname(__FILE__) + '/highrise/group'
|
15
|
+
require File.dirname(__FILE__) + '/highrise/kase'
|
16
|
+
require File.dirname(__FILE__) + '/highrise/membership'
|
17
|
+
require File.dirname(__FILE__) + '/highrise/note'
|
18
|
+
require File.dirname(__FILE__) + '/highrise/person'
|
19
|
+
require File.dirname(__FILE__) + '/highrise/task'
|
20
|
+
require File.dirname(__FILE__) + '/highrise/user'
|
21
|
+
require File.dirname(__FILE__) + '/highrise/tag'
|
22
|
+
require File.dirname(__FILE__) + '/highrise/deal'
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Highrise
|
2
|
+
class Company < Subject
|
3
|
+
include Pagination
|
4
|
+
include Taggable
|
5
|
+
|
6
|
+
def self.find_all_across_pages_since(time)
|
7
|
+
find_all_across_pages(:params => { :since => time.utc.to_s(:db).gsub(/[^\d]/, '') })
|
8
|
+
end
|
9
|
+
|
10
|
+
def people
|
11
|
+
Person.find(:all, :from => "/companies/#{id}/people.xml")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Highrise
|
2
|
+
class CurlHelper
|
3
|
+
|
4
|
+
# thanks mislav ;) http://github.com/mislav/curly/tree/master
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@curly = Curly.new
|
8
|
+
@curly.http_auth_types = Curl::CURLAUTH_BASIC
|
9
|
+
end
|
10
|
+
|
11
|
+
def get_userpwd_from_url(url)
|
12
|
+
userpwd_from_url_regex = /(http|https):\/\/((?:[\w\.\-\+%!$&'\(\)*\+,;=]+:)*[\w\.\-\+%!$&'\(\)*\+,;=]+)?/
|
13
|
+
match = userpwd_from_url_regex.match(url)
|
14
|
+
match.captures[1] unless match.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
def get_document(url)
|
18
|
+
@curly.userpwd = get_userpwd_from_url(url)
|
19
|
+
@curly.get(url).doc
|
20
|
+
end
|
21
|
+
|
22
|
+
def get_document_from_id(collection_name_and_id)
|
23
|
+
get_document("#{Highrise::Base.site.to_s}#{collection_name_and_id}")
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'hpricot'
|
2
|
+
require 'iconv'
|
3
|
+
require 'curb'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
class Curly < ActiveSupport::BasicObject
|
7
|
+
attr_reader :uri
|
8
|
+
|
9
|
+
def initialize(uri = nil)
|
10
|
+
@curl = Curl::Easy.new
|
11
|
+
self.uri = uri
|
12
|
+
self.follow_location = true
|
13
|
+
yield self if block_given?
|
14
|
+
end
|
15
|
+
|
16
|
+
def uri=(obj)
|
17
|
+
case obj
|
18
|
+
when String
|
19
|
+
unless @uri
|
20
|
+
@uri = URI.parse(obj)
|
21
|
+
else
|
22
|
+
@uri += obj
|
23
|
+
end
|
24
|
+
when URI::HTTP
|
25
|
+
@uri = obj
|
26
|
+
when nil
|
27
|
+
return
|
28
|
+
else
|
29
|
+
raise "unsupported URI type (#{obj.class.name} given)"
|
30
|
+
end
|
31
|
+
|
32
|
+
self.url = @uri.to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
def method_missing(method, *args, &block)
|
36
|
+
@curl.send(method, *args, &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
def cookiejar=(filename)
|
40
|
+
self.enable_cookies = true
|
41
|
+
@curl.cookiejar = filename
|
42
|
+
end
|
43
|
+
|
44
|
+
def get(uri = nil)
|
45
|
+
self.uri = uri
|
46
|
+
http_get
|
47
|
+
raise "expected 2xx, got #{response_code} (GET #{url})" unless success?
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def success?
|
52
|
+
response_code >= 200 and response_code < 300
|
53
|
+
end
|
54
|
+
|
55
|
+
def doc
|
56
|
+
Hpricot body_unicode
|
57
|
+
end
|
58
|
+
|
59
|
+
def encoding
|
60
|
+
return @encoding unless @encoding == false
|
61
|
+
@encoding = if body_str =~ /;\s*charset=([\w-]+)\s*['"]/
|
62
|
+
$1.downcase
|
63
|
+
else
|
64
|
+
false
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def post(params)
|
69
|
+
fields = params.map do |key, value|
|
70
|
+
Curl::PostField.content(key.to_s, value.to_s)
|
71
|
+
end
|
72
|
+
http_post *fields
|
73
|
+
end
|
74
|
+
|
75
|
+
class Form
|
76
|
+
def initialize(element)
|
77
|
+
@node = element
|
78
|
+
end
|
79
|
+
|
80
|
+
def elements
|
81
|
+
@node.search('input, button, select, textarea')
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
protected
|
86
|
+
|
87
|
+
def body_unicode
|
88
|
+
body = body_str
|
89
|
+
if encoding and encoding != 'utf-8'
|
90
|
+
body = Iconv.conv('UTF-8', encoding, body)
|
91
|
+
end
|
92
|
+
body
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
Hpricot::Doc.class_eval do
|
98
|
+
def forms
|
99
|
+
search('form').map { |f| Curly::Form.new(f) }
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Highrise
|
2
|
+
# name
|
3
|
+
# background
|
4
|
+
# status (pending, won, lost)
|
5
|
+
# category-id
|
6
|
+
# ---------------------------
|
7
|
+
# account-id
|
8
|
+
# author-id
|
9
|
+
# created-at
|
10
|
+
# currency
|
11
|
+
# duration
|
12
|
+
# group-id
|
13
|
+
# owner-id
|
14
|
+
# party-id
|
15
|
+
# price type
|
16
|
+
# price-type
|
17
|
+
# responsible-party-id
|
18
|
+
# status-changed-on
|
19
|
+
# updated-at
|
20
|
+
# visible-to
|
21
|
+
class Deal < Subject
|
22
|
+
|
23
|
+
class DealCategory < Base
|
24
|
+
def self.find_by_name(name)
|
25
|
+
find(:all).select{|category| category.name == name}.first
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.delete!(name)
|
29
|
+
category = self.find_by_name(name)
|
30
|
+
delete(category.id) if category
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Highrise
|
2
|
+
module Pagination
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
def find_all_across_pages(options = {})
|
9
|
+
records = []
|
10
|
+
each(options) { |record| records << record }
|
11
|
+
records
|
12
|
+
end
|
13
|
+
|
14
|
+
def each(options = {})
|
15
|
+
options[:params] ||= {}
|
16
|
+
options[:params][:n] = 0
|
17
|
+
|
18
|
+
loop do
|
19
|
+
if (records = self.find(:all, options)).any?
|
20
|
+
records.each { |record| yield record }
|
21
|
+
options[:params][:n] += records.size
|
22
|
+
else
|
23
|
+
break # no people included on that page, thus no more people total
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Highrise
|
2
|
+
class Person < Subject
|
3
|
+
DATE_OCCASIONS = ["Birthday", "Anniversary", "First met", "Hired", "Fired"]
|
4
|
+
|
5
|
+
include Pagination
|
6
|
+
include Taggable
|
7
|
+
|
8
|
+
def self.search_by_name(name)
|
9
|
+
find(:first, :from => "/people/search.xml", :params => {:term => name})
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.find_all_by_tag(tag_name)
|
13
|
+
tag = Tag.find_by_name(tag_name)
|
14
|
+
find(:all, :from => "/tags/#{tag.id}.xml")
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.find_all_across_pages_since(time)
|
18
|
+
find_all_across_pages(:params => { :since => time.utc.to_s(:db).gsub(/[^\d]/, '') })
|
19
|
+
end
|
20
|
+
|
21
|
+
def company
|
22
|
+
Company.find(company_id) if company_id
|
23
|
+
end
|
24
|
+
|
25
|
+
def name
|
26
|
+
"#{first_name rescue ''} #{last_name rescue ''}".strip
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def add_date!(description, date, task_owner_id=nil)
|
31
|
+
description_key = DATE_OCCASIONS.include?(description) ? :description : :custom_description
|
32
|
+
custom_date = { description_key => description, :month => date.month, :day => date.day, :year => date.year }
|
33
|
+
if task_owner_id
|
34
|
+
custom_date[:assign_task] = true
|
35
|
+
custom_date[:task_owner_id] = task_owner_id
|
36
|
+
end
|
37
|
+
# TODO: use different way to add date (current throws 406 from Highrise, but works anyways)
|
38
|
+
self.post(:contact_dates, :contact_date => custom_date) rescue nil
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Highrise
|
2
|
+
class Subject < Base
|
3
|
+
def notes
|
4
|
+
Note.find_all_across_pages(:from => "/#{self.class.collection_name}/#{id}/notes.xml")
|
5
|
+
end
|
6
|
+
|
7
|
+
def emails
|
8
|
+
Email.find_all_across_pages(:from => "/#{self.class.collection_name}/#{id}/emails.xml")
|
9
|
+
end
|
10
|
+
|
11
|
+
def upcoming_tasks
|
12
|
+
Task.find(:all, :from => "/#{self.class.collection_name}/#{id}/tasks.xml")
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_note!(note_body)
|
16
|
+
return if note_body.blank?
|
17
|
+
note = Note.new(:body => note_body)
|
18
|
+
self.post(:notes, {}, note.to_xml)
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_deal!(name, category, status=nil, background=nil)
|
22
|
+
category_object = Deal::DealCategory.find_by_name(category)
|
23
|
+
category_id = category_object ? category_object.id : nil
|
24
|
+
deal = Deal.new(:name => name, :background => background, :status => status || "pending", :category_id => category_id)
|
25
|
+
deal.price_type = 'fixed'
|
26
|
+
deal.party_id = self.id
|
27
|
+
deal.save
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_task!(body, due_date, arguments = {})
|
31
|
+
category = Task::TaskCategory.find_by_name(arguments[:category])
|
32
|
+
category_id = category ? category.id : nil
|
33
|
+
|
34
|
+
task = Task.new(arguments.merge(:body => body, :category_id => category_id))
|
35
|
+
|
36
|
+
if Task::DUE_DATES.include?(due_date.to_s)
|
37
|
+
task.frame = due_date
|
38
|
+
else
|
39
|
+
task.frame = 'specific'
|
40
|
+
task.due_at = due_date
|
41
|
+
end
|
42
|
+
|
43
|
+
task.subject_type = self.instance_of?(Kase) ? 'Kase' : 'Party'
|
44
|
+
task.subject_id = self.id
|
45
|
+
task.save
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|