pipeline_dealers 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. data/.gitignore +17 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +4 -0
  4. data/Guardfile +5 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +64 -0
  7. data/Rakefile +1 -0
  8. data/examples/companies_create_and_destroy.rb +6 -0
  9. data/examples/companies_list_all.rb +9 -0
  10. data/examples/company_details.rb +19 -0
  11. data/examples/company_notes.rb +17 -0
  12. data/examples/company_people.rb +15 -0
  13. data/examples/config.rb +54 -0
  14. data/examples/find_person.rb +9 -0
  15. data/examples/list_custom_fields.rb +19 -0
  16. data/examples/people_create_and_destroy.rb +6 -0
  17. data/examples/people_list_all.rb +9 -0
  18. data/lib/pipeline_dealers/backend/base/backend.rb +13 -0
  19. data/lib/pipeline_dealers/backend/base/collection.rb +111 -0
  20. data/lib/pipeline_dealers/backend/base.rb +2 -0
  21. data/lib/pipeline_dealers/backend/http/backend.rb +64 -0
  22. data/lib/pipeline_dealers/backend/http/collection.rb +32 -0
  23. data/lib/pipeline_dealers/backend/http/connection.rb +69 -0
  24. data/lib/pipeline_dealers/backend/http/fetcher.rb +78 -0
  25. data/lib/pipeline_dealers/backend/http.rb +5 -0
  26. data/lib/pipeline_dealers/backend/test/backend.rb +34 -0
  27. data/lib/pipeline_dealers/backend/test/collection.rb +34 -0
  28. data/lib/pipeline_dealers/backend/test.rb +3 -0
  29. data/lib/pipeline_dealers/client.rb +23 -0
  30. data/lib/pipeline_dealers/delegator.rb +61 -0
  31. data/lib/pipeline_dealers/error/connection.rb +17 -0
  32. data/lib/pipeline_dealers/error/custom_field.rb +12 -0
  33. data/lib/pipeline_dealers/error/invalid_attribute.rb +29 -0
  34. data/lib/pipeline_dealers/error.rb +12 -0
  35. data/lib/pipeline_dealers/limits.rb +7 -0
  36. data/lib/pipeline_dealers/model/company/custom_field.rb +9 -0
  37. data/lib/pipeline_dealers/model/company.rb +46 -0
  38. data/lib/pipeline_dealers/model/custom_field.rb +94 -0
  39. data/lib/pipeline_dealers/model/has_custom_fields.rb +62 -0
  40. data/lib/pipeline_dealers/model/note.rb +37 -0
  41. data/lib/pipeline_dealers/model/person/custom_field.rb +9 -0
  42. data/lib/pipeline_dealers/model/person.rb +63 -0
  43. data/lib/pipeline_dealers/model.rb +158 -0
  44. data/lib/pipeline_dealers/test.rb +10 -0
  45. data/lib/pipeline_dealers/version.rb +3 -0
  46. data/lib/pipeline_dealers.rb +24 -0
  47. data/pipeline_dealers.gemspec +39 -0
  48. data/spec/acceptance/companies/creation_spec.rb +30 -0
  49. data/spec/acceptance/companies/custom_fields_spec.rb +8 -0
  50. data/spec/acceptance/companies/updating_spec.rb +36 -0
  51. data/spec/acceptance/people/creation_spec.rb +20 -0
  52. data/spec/pipeline_dealers/backend/http/backend_spec.rb +48 -0
  53. data/spec/pipeline_dealers/backend/http/collection_spec.rb +158 -0
  54. data/spec/pipeline_dealers/model/custom_field_spec.rb +129 -0
  55. data/spec/pipeline_dealers/model/has_custom_fields_spec.rb +115 -0
  56. data/spec/pipeline_dealers/model_spec.rb +33 -0
  57. data/spec/pipeline_dealers/test_client_spec.rb +80 -0
  58. data/spec/support/test_model.rb +6 -0
  59. data/todo.md +13 -0
  60. metadata +291 -0
@@ -0,0 +1,34 @@
1
+ require_relative "collection"
2
+
3
+ module PipelineDealers
4
+ module Backend
5
+ class Test
6
+ attr_reader :items
7
+
8
+ def cache key
9
+ yield
10
+ end
11
+
12
+ def initialize(client)
13
+ @client = client
14
+ @items = Hash.new([])
15
+ @last_id = 41
16
+ end
17
+
18
+ def collection(options)
19
+ Collection.new(self, options)
20
+ end
21
+
22
+ def save(collection, model)
23
+ @items[model.class] << model
24
+ if model.id.nil?
25
+ model.send(:instance_variable_set, :@id, @last_id += 1)
26
+ end
27
+ end
28
+
29
+ def destroy(collection, to_remove)
30
+ @items[to_remove.class].reject! { |model| model == to_remove }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ module PipelineDealers
2
+ module Backend
3
+ class Test
4
+ class Collection < Backend::Base::Collection
5
+ extend Delegator
6
+
7
+ delegate :count, :first, to: :items
8
+
9
+ def initialize(backend, options)
10
+ super(backend, options)
11
+ end
12
+
13
+ def find id
14
+ items.select { |model| model.id == id }.first
15
+ end
16
+
17
+ def each &operation
18
+ items.each &operation
19
+ end
20
+
21
+ def collect &operation
22
+ items.collect &operation
23
+ end
24
+
25
+ protected
26
+
27
+ def items
28
+ @backend.items[@model_klass]
29
+ @backend.items[@model_klass]
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ require "pipeline_dealers/backend/base"
2
+ require "pipeline_dealers/backend/test/backend"
3
+ require "pipeline_dealers/backend/test/collection"
@@ -0,0 +1,23 @@
1
+ module PipelineDealers
2
+ # This class models the API client.
3
+ class Client
4
+ attr_reader :api_key
5
+ attr_reader :backend
6
+
7
+ def initialize(options = {})
8
+ @backend = (options.delete(:backend) || Backend::Http).new(options)
9
+ end
10
+
11
+ def companies
12
+ @companies ||= @backend.collection(client: self, model_klass: Model::Company, custom_fields: { model_klass: Model::Company::CustomField, cached: true, cache_key: "custom_company_fields" })
13
+ end
14
+
15
+ def people
16
+ @people ||= @backend.collection(client: self, model_klass: Model::Person, custom_fields: {model_klass: Model::Person::CustomField, cached: true, cache_key: "custom_person_fields"})
17
+ end
18
+
19
+ def notes
20
+ @notes ||= @backend.collection(client: self, model_klass: Model::Note)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,61 @@
1
+ # Copied from activesupport/lib/active_support/core_ext/module/delegation.rb
2
+
3
+ module PipelineDealers
4
+ module Delegator
5
+ def delegate(*methods)
6
+ options = methods.pop
7
+ unless options.is_a?(Hash) && to = options[:to]
8
+ raise ArgumentError, 'Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter).'
9
+ end
10
+
11
+ prefix, allow_nil = options.values_at(:prefix, :allow_nil)
12
+
13
+ if prefix == true && to =~ /^[^a-z_]/
14
+ raise ArgumentError, 'Can only automatically set the delegation prefix when delegating to a method.'
15
+ end
16
+
17
+ method_prefix = \
18
+ if prefix
19
+ "#{prefix == true ? to : prefix}_"
20
+ else
21
+ ''
22
+ end
23
+
24
+ file, line = caller.first.split(':', 2)
25
+ line = line.to_i
26
+
27
+ to = to.to_s
28
+ to = 'self.class' if to == 'class'
29
+
30
+ methods.each do |method|
31
+ # Attribute writer methods only accept one argument. Makes sure []=
32
+ # methods still accept two arguments.
33
+ definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block'
34
+
35
+ if allow_nil
36
+ module_eval(<<-EOS, file, line - 2)
37
+ def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
38
+ if #{to} || #{to}.respond_to?(:#{method}) # if client || client.respond_to?(:name)
39
+ #{to}.#{method}(#{definition}) # client.name(*args, &block)
40
+ end # end
41
+ end # end
42
+ EOS
43
+ else
44
+ exception = %(raise "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
45
+
46
+ module_eval(<<-EOS, file, line - 1)
47
+ def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
48
+ #{to}.#{method}(#{definition}) # client.name(*args, &block)
49
+ rescue NoMethodError # rescue NoMethodError
50
+ if #{to}.nil? # if client.nil?
51
+ #{exception} # # add helpful message to the exception
52
+ else # else
53
+ raise # raise
54
+ end # end
55
+ end # end
56
+ EOS
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,17 @@
1
+ module PipelineDealers
2
+ class Error
3
+ class Connection < Error
4
+ end
5
+
6
+ class Connection
7
+ class Unprocessable < Error::Connection
8
+ attr_reader :errors
9
+
10
+ def initialize(errors)
11
+ @errors = errors
12
+ @message = "Couldn't save to the server: #{errors.inspect}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ module PipelineDealers
2
+ class Error
3
+ class NoSuchCustomField < Error
4
+ attr_reader :name
5
+
6
+ def initialize(name)
7
+ @name = name
8
+ @message = "Could not find a custom field with the name #{name.inspect}"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,29 @@
1
+ module PipelineDealers
2
+ class Error
3
+ class InvalidAttribute < Error
4
+ attr_reader :attribute_name
5
+
6
+ def initialize(klass, attribute_name)
7
+ @klass = klass
8
+ @attribute_name = attribute_name
9
+ end
10
+ end
11
+
12
+ class InvalidAttributeName < InvalidAttribute
13
+ def initialize(klass, attribute_name)
14
+ super
15
+ @message = "The attribute #{attribute_name.inspect} is not known by #{klass.inspect}!"
16
+ end
17
+ end
18
+
19
+ class InvalidAttributeValue < InvalidAttribute
20
+ attr_reader :value
21
+
22
+ def initialize(klass, attribute_name, value)
23
+ super(klass, attribute_name)
24
+ @value = value
25
+ @message = "The attribute #{attribute_name.inspect} does not accept the value #{value.inspect} (on model #{klass.inspect})"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,12 @@
1
+ module PipelineDealers
2
+ class Error < Exception
3
+ attr_reader :message
4
+ def to_s
5
+ @message
6
+ end
7
+ end
8
+ end
9
+
10
+ require "pipeline_dealers/error/connection"
11
+ require "pipeline_dealers/error/custom_field"
12
+ require "pipeline_dealers/error/invalid_attribute"
@@ -0,0 +1,7 @@
1
+ module PipelineDealers
2
+ # There are the limits of the API
3
+ module Limits
4
+ MAX_RESULTS_PER_PAGE = 200
5
+ MAX_REQUESTS_PER_SECOND = 1
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module PipelineDealers
2
+ class Model
3
+ class Company
4
+ class CustomField < Model::CustomField
5
+ self.collection_url = "admin/company_custom_field_labels"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,46 @@
1
+ module PipelineDealers
2
+ class Model
3
+ class Company < Model
4
+ include HasCustomFields
5
+ self.collection_url = "companies"
6
+ self.attribute_name = "company"
7
+
8
+ attrs :name,
9
+ :description,
10
+ :email,
11
+ :web,
12
+ :fax,
13
+ :address_1,
14
+ :address_2,
15
+ :city,
16
+ :postal_code,
17
+ :country,
18
+ :phone1,
19
+ :phone2,
20
+ :phone3,
21
+ :phone4,
22
+ :phone1_desc,
23
+ :phone2_desc, :phone3_desc,
24
+ :phone4_desc,
25
+ :created_at,
26
+ :import_id
27
+
28
+
29
+ # Read only
30
+ attrs :won_deals_total,
31
+ :image_thumb_url,
32
+ :updated_at,
33
+ :total_pipeline,
34
+ :state,
35
+ read_only: true
36
+
37
+ def people
38
+ @client.people.where(company_id: self.id)
39
+ end
40
+
41
+ def notes
42
+ @client.notes.where(company_id: self.id)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,94 @@
1
+ module PipelineDealers
2
+ class Model
3
+ class CustomField < Model
4
+ attr_reader :dropdown_entries
5
+
6
+ attrs :name,
7
+ :field_type,
8
+ :is_required,
9
+ :custom_field_label_dropdown_entries,
10
+ readonly: true
11
+
12
+ def process_attributes
13
+ @dropdown_entries = @attributes.delete(:custom_field_label_dropdown_entries)
14
+ end
15
+
16
+ def decode(model, value)
17
+ coder_for(model).decode(value)
18
+ end
19
+
20
+ def encode(model, value)
21
+ coder_for(model).encode(value)
22
+ end
23
+
24
+ protected
25
+
26
+ def coder_for(model)
27
+ coder_klass = Coder::Types[field_type.to_sym]
28
+ if coder_klass.nil?
29
+ raise "Unkown custom field type '#{field_type.inspect}'"
30
+ else
31
+ return coder_klass.new(model, self)
32
+ end
33
+ end
34
+
35
+ class Coder
36
+ def initialize(model, field)
37
+ @model, @field = model, field
38
+ end
39
+
40
+ class Identity < Coder
41
+ def encode(value) value end
42
+ def decode(value) value end
43
+ end
44
+
45
+ class Dropdown < Coder
46
+ def encode value
47
+ find :value, of: value, and_return: :id
48
+ end
49
+
50
+ def decode id
51
+ find :id, of: id, and_return: :value
52
+ end
53
+
54
+ protected
55
+
56
+ def find key, options
57
+ key = key.to_s
58
+ value = options[:of]
59
+ and_return = options[:and_return].to_s
60
+
61
+ entry = @field.dropdown_entries.find { |entry| entry[key] == value }
62
+
63
+ if entry
64
+ return entry[and_return]
65
+ else
66
+ raise Error::InvalidAttributeValue.new(@model, @field.name, value)
67
+ end
68
+ end
69
+ end
70
+
71
+ class MultiSelect < Coder
72
+ def encode values
73
+ coder = Dropdown.new(@model, @field)
74
+ values.collect { |value| coder.encode(value) }
75
+ end
76
+
77
+ def decode ids
78
+ coder = Dropdown.new(@model, @field)
79
+ ids.collect { |value| coder.decode(value) }
80
+ end
81
+ end
82
+
83
+ Types = {
84
+ numeric: Identity,
85
+ text: Identity,
86
+ currency: Identity,
87
+ dropdown: Dropdown,
88
+ multi_select: MultiSelect,
89
+ multi_association: Identity # Does not work yet
90
+ }
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,62 @@
1
+ require "active_support/concern"
2
+
3
+ module PipelineDealers
4
+ class Model
5
+ module HasCustomFields
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attr_reader :custom_fields
10
+ end
11
+
12
+ CUSTOM_FIELD_PREFIX = "custom_label_"
13
+ CUSTOM_FIELD_REGEX = /#{CUSTOM_FIELD_PREFIX}(\d+)/
14
+
15
+ protected
16
+
17
+ def custom_field_collection
18
+ if @collection.respond_to?(:custom_fields)
19
+ @collection.custom_fields.all
20
+ else
21
+ raise "Expected collection of model '#{@collection.model_klass}' to have the custom field collection (custom_fields method)!"
22
+ end
23
+ end
24
+
25
+ def process_attributes
26
+ @custom_fields = pld2human_fields(@attributes.delete(:custom_fields))
27
+ end
28
+
29
+ def attributes_for_saving attributes
30
+ attributes.merge("custom_fields" => human2pld_fields(@custom_fields))
31
+ end
32
+
33
+ def pld2human_fields attributes
34
+ attributes ||= {}
35
+ result = {}
36
+
37
+ attributes.each do |key, value|
38
+ field_id = CUSTOM_FIELD_REGEX.match(key)[1].to_i
39
+ field = custom_field_collection.select { |field| field.id == field_id }.first
40
+
41
+ raise Error::InvalidAttributeName.new(self, key) if field.nil?
42
+ result[field.name] = field.decode(self, value)
43
+ end
44
+
45
+ result
46
+ end
47
+
48
+ def human2pld_fields attributes
49
+ attributes ||= {}
50
+ result = HashWithIndifferentAccess.new
51
+
52
+ attributes.each do |name, value|
53
+ field = custom_field_collection.select { |field| field.name == name}.first
54
+ raise Error::InvalidAttributeName.new(self, name) if field.nil?
55
+ result[CUSTOM_FIELD_PREFIX + field.id.to_s] = field.encode(self, value)
56
+ end
57
+
58
+ result
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,37 @@
1
+ module PipelineDealers
2
+ class Model
3
+ class Note < Model
4
+ self.collection_url = "notes"
5
+ self.attribute_name = "note"
6
+
7
+ attrs :deal_id,
8
+ :person_id,
9
+ :company_id,
10
+ :title,
11
+ :created_by_user_id,
12
+ :created_by_user,
13
+ :note_category_id,
14
+ :content,
15
+ :note_category
16
+
17
+ # Read only
18
+ attrs :created_at,
19
+ :updated_at,
20
+ :deal,
21
+ :person,
22
+ :company,
23
+ read_only: true
24
+
25
+ alias_method :person_cache, :person
26
+ alias_method :company_cache, :company
27
+
28
+ def person
29
+ @client.people.find(self.person_id)
30
+ end
31
+
32
+ def company
33
+ @client.companies.find(self.company_id)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,9 @@
1
+ module PipelineDealers
2
+ class Model
3
+ class Person
4
+ class CustomField < Model::CustomField
5
+ self.collection_url = "admin/person_custom_field_labels"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,63 @@
1
+ module PipelineDealers
2
+ class Model
3
+ class Person < Model
4
+ include HasCustomFields
5
+ self.collection_url = "people"
6
+ self.attribute_name = "person"
7
+
8
+ attrs :first_name,
9
+ :last_name,
10
+ :phone,
11
+ :home_phone,
12
+ :mobile,
13
+ :position,
14
+ :website,
15
+ :email,
16
+ :email2,
17
+ :home_email,
18
+ :company_id,
19
+ :user_id,
20
+ :type,
21
+ :lead_status_id,
22
+ :lead_source_id,
23
+ :predefined_contacts_tag_ids,
24
+ :deal_ids,
25
+ :work_address_1,
26
+ :work_address_2,
27
+ :work_city,
28
+ :work_state,
29
+ :work_country,
30
+ :work_postal_code,
31
+ :home_address_1,
32
+ :home_address_2,
33
+ :home_city,
34
+ :home_state,
35
+ :home_country,
36
+ :home_postal_code,
37
+ :fax,
38
+ :facebook_url,
39
+ :linked_in_url,
40
+ :twitter,
41
+ :instant_message,
42
+ :created_at
43
+
44
+
45
+ # Read only
46
+ attrs :predefined_contacts_tags,
47
+ :won_deals_total,
48
+ :full_name,
49
+ :company_name,
50
+ :user,
51
+ :lead_status,
52
+ :lead_source,
53
+ :image_thumb_url,
54
+ :predefined_contacts_tags,
55
+ :updated_at,
56
+ :deals,
57
+ :company,
58
+ :viewed_at,
59
+ :total_pipeline,
60
+ read_only: true
61
+ end
62
+ end
63
+ end