pipeline_dealers 0.0.1

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.
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