facebook_ads 0.1.5 → 0.1.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.rubocop.yml +23 -0
- data/.travis.yml +1 -1
- data/Gemfile +4 -5
- data/Gemfile.lock +34 -82
- data/{README.markdown → README.md} +43 -10
- data/Rakefile +6 -0
- data/bin/console +15 -1
- data/facebook_ads.gemspec +11 -13
- data/lib/facebook_ads.rb +11 -7
- data/lib/facebook_ads/ad.rb +6 -8
- data/lib/facebook_ads/ad_account.rb +65 -54
- data/lib/facebook_ads/ad_audience.rb +22 -0
- data/lib/facebook_ads/ad_campaign.rb +21 -17
- data/lib/facebook_ads/ad_creative.rb +7 -9
- data/lib/facebook_ads/ad_image.rb +3 -5
- data/lib/facebook_ads/ad_insight.rb +4 -5
- data/lib/facebook_ads/ad_product.rb +6 -0
- data/lib/facebook_ads/ad_product_catalog.rb +60 -0
- data/lib/facebook_ads/ad_product_feed.rb +6 -0
- data/lib/facebook_ads/ad_product_set.rb +6 -0
- data/lib/facebook_ads/ad_set.rb +9 -11
- data/lib/facebook_ads/ad_targeting.rb +19 -21
- data/lib/facebook_ads/base.rb +107 -62
- data/test/ad_account_test.rb +27 -0
- data/test/ad_campaign_test.rb +32 -0
- data/test/ad_creative_test.rb +7 -0
- data/test/ad_image_test.rb +27 -0
- data/test/ad_insight_test.rb +7 -0
- data/test/ad_product_catalog_test.rb +28 -0
- data/test/ad_product_feed_test.rb +7 -0
- data/test/ad_product_set_test.rb +7 -0
- data/test/ad_product_test.rb +22 -0
- data/test/ad_set_test.rb +7 -0
- data/test/ad_targeting_test.rb +7 -0
- data/test/ad_test.rb +7 -0
- data/test/facebook_ads_test.rb +8 -0
- data/test/test_helper.rb +64 -0
- data/test/vcr_cassettes/AdAccountTest-test_all.yml +70 -0
- data/test/vcr_cassettes/AdAccountTest-test_applications.yml +130 -0
- data/test/vcr_cassettes/AdAccountTest-test_find_by.yml +71 -0
- data/test/vcr_cassettes/AdCampaignTest-test_create.yml +295 -0
- data/test/vcr_cassettes/AdCampaignTest-test_list.yml +133 -0
- data/test/vcr_cassettes/AdImageTest-test_create.yml +2963 -0
- data/test/vcr_cassettes/AdImageTest-test_list.yml +137 -0
- data/test/vcr_cassettes/AdProductCatalogTest-test_all.yml +60 -0
- data/test/vcr_cassettes/AdProductCatalogTest-test_create.yml +256 -0
- data/test/vcr_cassettes/AdProductTest-test_list.yml +130 -0
- metadata +44 -49
- data/spec/ad_account_spec.rb +0 -78
- data/spec/ad_campaign_spec.rb +0 -13
- data/spec/ad_creative_spec.rb +0 -14
- data/spec/ad_image_spec.rb +0 -11
- data/spec/ad_insight_spec.rb +0 -11
- data/spec/ad_set_spec.rb +0 -13
- data/spec/ad_spec.rb +0 -13
- data/spec/ad_targeting_spec.rb +0 -4
- data/spec/spec_helper.rb +0 -15
- data/spec/support/fixtures.sh +0 -17
- data/spec/support/fixtures/6057330925170.json +0 -1
- data/spec/support/fixtures/6057810634370.json +0 -1
- data/spec/support/fixtures/6057810946970.json +0 -1
- data/spec/support/fixtures/6057824295570.json +0 -1
- data/spec/support/fixtures/act_861827983860489.json +0 -1
- data/spec/support/fixtures/act_861827983860489/adcreatives.json +0 -1
- data/spec/support/fixtures/act_861827983860489/adimages.json +0 -1
- data/spec/support/fixtures/act_861827983860489/ads.json +0 -1
- data/spec/support/fixtures/act_861827983860489/adsets.json +0 -1
- data/spec/support/fixtures/act_861827983860489/campaigns.json +0 -1
- data/spec/support/fixtures/me/adaccounts.json +0 -1
- data/spec/support/rack_facebook.rb +0 -22
@@ -2,23 +2,22 @@ module FacebookAds
|
|
2
2
|
# Ad insights exist for ad accounts, ad campaigns, ad sets, and ads.
|
3
3
|
# A lot more can be done here.
|
4
4
|
# https://developers.facebook.com/docs/marketing-api/insights/overview
|
5
|
+
# https://developers.facebook.com/docs/marketing-api/insights/fields/v2.8
|
5
6
|
class AdInsight < Base
|
6
|
-
|
7
|
-
FIELDS = %w(ad_id objective impressions unique_actions cost_per_unique_action_type clicks cpc cpm ctr spend)
|
7
|
+
FIELDS = %w(account_id campaign_id adset_id ad_id objective impressions unique_actions cost_per_unique_action_type clicks cpc cpm cpp ctr spend reach relevance_score).freeze
|
8
8
|
|
9
9
|
class << self
|
10
|
-
def find(
|
10
|
+
def find(_id)
|
11
11
|
raise Exception, 'NOT IMPLEMENTED'
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
-
def update(
|
15
|
+
def update(_data)
|
16
16
|
raise Exception, 'NOT IMPLEMENTED'
|
17
17
|
end
|
18
18
|
|
19
19
|
def destroy
|
20
20
|
raise Exception, 'NOT IMPLEMENTED'
|
21
21
|
end
|
22
|
-
|
23
22
|
end
|
24
23
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module FacebookAds
|
2
|
+
# https://developers.facebook.com/docs/marketing-api/reference/product-catalog
|
3
|
+
class AdProductCatalog < Base
|
4
|
+
FIELDS = %w(id name vertical product_count feed_count).freeze
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def all
|
8
|
+
get("/#{FacebookAds.business_id}/product_catalogs", objectify: true)
|
9
|
+
end
|
10
|
+
|
11
|
+
def find_by(conditions)
|
12
|
+
all.detect do |object|
|
13
|
+
conditions.all? do |key, value|
|
14
|
+
object.send(key) == value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def create(name:)
|
20
|
+
catalog = post("/#{FacebookAds.business_id}/product_catalogs", query: { name: name }, objectify: true)
|
21
|
+
find(catalog.id)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# has_many ad_product_feeds
|
26
|
+
|
27
|
+
def ad_product_feeds
|
28
|
+
AdProductFeed.paginate("/#{id}/product_feeds")
|
29
|
+
end
|
30
|
+
|
31
|
+
# catalog.create_ad_product_feed(name: 'Test', schedule: { url: 'https://tophatter.com/admin/ad_automation/ad_product_feeds/1.csv', interval: 'HOURLY' })
|
32
|
+
def create_ad_product_feed(name:, schedule:)
|
33
|
+
feed = AdProductCatalog.post("/#{id}/product_feeds", query: { name: name, schedule: schedule }, objectify: true)
|
34
|
+
AdProductFeed.find(feed.id)
|
35
|
+
end
|
36
|
+
|
37
|
+
# has_many product_groups
|
38
|
+
|
39
|
+
# def ad_product_groups
|
40
|
+
# AdProductGroup.paginate("/#{id}/product_groups")
|
41
|
+
# end
|
42
|
+
|
43
|
+
# has_many product_sets
|
44
|
+
|
45
|
+
# def ad_product_sets
|
46
|
+
# AdProductSet.paginate("/#{id}/product_sets")
|
47
|
+
# end
|
48
|
+
|
49
|
+
# has_many ad_products
|
50
|
+
|
51
|
+
def ad_products
|
52
|
+
AdProduct.paginate("/#{id}/products")
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_ad_product(data)
|
56
|
+
product = AdProductCatalog.post("/#{id}/products", query: data, objectify: true)
|
57
|
+
AdProduct.find(product.id)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
module FacebookAds
|
2
|
+
# https://developers.facebook.com/docs/marketing-api/reference/product-feed
|
3
|
+
class AdProductFeed < Base
|
4
|
+
FIELDS = %w(id country created_time default_currency deletion_enabled delimiter encoding file_name latest_upload name product_count quoted_fields_mode schedule).freeze
|
5
|
+
end
|
6
|
+
end
|
data/lib/facebook_ads/ad_set.rb
CHANGED
@@ -2,34 +2,32 @@ module FacebookAds
|
|
2
2
|
# An ad set belongs to a campaign and has many ads.
|
3
3
|
# https://developers.facebook.com/docs/marketing-api/reference/ad-campaign
|
4
4
|
class AdSet < Base
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
OPTIMIZATION_GOALS = %w(APP_INSTALLS OFFSITE_CONVERSIONS) # %w(NONE BRAND_AWARENESS CLICKS ENGAGED_USERS EXTERNAL EVENT_RESPONSES IMPRESSIONS LEAD_GENERATION LINK_CLICKS OFFER_CLAIMS PAGE_ENGAGEMENT PAGE_LIKES POST_ENGAGEMENT REACH SOCIAL_IMPRESSIONS VIDEO_VIEWS)
|
5
|
+
FIELDS = %w(id account_id campaign_id adlabels adset_schedule bid_amount bid_info billing_event configured_status created_time creative_sequence effective_status end_time frequency_cap frequency_cap_reset_period frequency_control_specs is_autobid lifetime_frequency_cap lifetime_imps name optimization_goal promoted_object rf_prediction_id rtb_flag start_time targeting updated_time use_new_app_click pacing_type budget_remaining daily_budget lifetime_budget).freeze
|
6
|
+
STATUSES = %w(ACTIVE PAUSED DELETED PENDING_REVIEW DISAPPROVED PREAPPROVED PENDING_BILLING_INFO CAMPAIGN_PAUSED ARCHIVED ADSET_PAUSED).freeze
|
7
|
+
BILLING_EVENTS = %w(APP_INSTALLS IMPRESSIONS).freeze
|
8
|
+
OPTIMIZATION_GOALS = %w(APP_INSTALLS OFFSITE_CONVERSIONS).freeze
|
10
9
|
|
11
10
|
# belongs_to ad_account
|
12
11
|
|
13
12
|
def ad_account
|
14
|
-
@
|
13
|
+
@ad_account ||= AdAccount.find("act_#{account_id}")
|
15
14
|
end
|
16
15
|
|
17
16
|
# belongs_to ad_campaign
|
18
17
|
|
19
18
|
def ad_campaign
|
20
|
-
@campaign ||=
|
19
|
+
@campaign ||= AdCampaign.find(campaign_id)
|
21
20
|
end
|
22
21
|
|
23
22
|
# has_many ads
|
24
23
|
|
25
24
|
def ads(effective_status: ['ACTIVE'], limit: 100)
|
26
|
-
|
25
|
+
Ad.paginate("/#{id}/ads", query: { effective_status: effective_status, limit: limit })
|
27
26
|
end
|
28
27
|
|
29
28
|
def create_ad(name:, creative_id:)
|
30
|
-
ad =
|
31
|
-
|
29
|
+
ad = Ad.post("/act_#{account_id}/ads", query: { name: name, adset_id: id, creative: { creative_id: creative_id }.to_json }, objectify: true) # Returns an Ad instance.
|
30
|
+
Ad.find(ad.id)
|
32
31
|
end
|
33
|
-
|
34
32
|
end
|
35
33
|
end
|
@@ -1,35 +1,34 @@
|
|
1
|
-
# https://developers.facebook.com/docs/marketing-api/targeting-specs
|
2
1
|
module FacebookAds
|
2
|
+
# https://developers.facebook.com/docs/marketing-api/targeting-specs
|
3
3
|
class AdTargeting
|
4
|
-
|
5
4
|
MEN = 1
|
6
5
|
WOMEN = 2
|
7
|
-
GENDERS = [MEN, WOMEN]
|
8
|
-
ANDROID_OS = 'Android'
|
9
|
-
APPLE_OS = 'iOS'
|
10
|
-
OSES = [ANDROID_OS, APPLE_OS]
|
11
|
-
ANDROID_DEVICES = %w(Android_Smartphone Android_Tablet)
|
12
|
-
APPLE_DEVICES = %w(iPhone iPad iPod)
|
6
|
+
GENDERS = [MEN, WOMEN].freeze
|
7
|
+
ANDROID_OS = 'Android'.freeze
|
8
|
+
APPLE_OS = 'iOS'.freeze
|
9
|
+
OSES = [ANDROID_OS, APPLE_OS].freeze
|
10
|
+
ANDROID_DEVICES = %w(Android_Smartphone Android_Tablet).freeze
|
11
|
+
APPLE_DEVICES = %w(iPhone iPad iPod).freeze
|
13
12
|
DEVICES = ANDROID_DEVICES + APPLE_DEVICES
|
14
|
-
INSTALLED = 'installed'
|
15
|
-
NOT_INSTALLED = 'not_installed'
|
16
|
-
APP_INSTALL_STATES = [INSTALLED, NOT_INSTALLED]
|
13
|
+
INSTALLED = 'installed'.freeze
|
14
|
+
NOT_INSTALLED = 'not_installed'.freeze
|
15
|
+
APP_INSTALL_STATES = [INSTALLED, NOT_INSTALLED].freeze
|
17
16
|
|
18
17
|
attr_accessor :genders, :age_min, :age_max, :countries, :user_os, :user_device, :app_install_state
|
19
18
|
|
20
19
|
def initialize
|
21
|
-
# self.genders
|
22
|
-
# self.age_min
|
23
|
-
# self.age_max
|
24
|
-
self.countries
|
25
|
-
# self.user_os
|
26
|
-
# self.user_device
|
20
|
+
# self.genders = [WOMEN] # If nil, defaults to all genders.
|
21
|
+
# self.age_min = 18 # If nil, defaults to 18.
|
22
|
+
# self.age_max = 65 # If nil, defaults to 65+.
|
23
|
+
self.countries = ['US']
|
24
|
+
# self.user_os = [ANDROID_OS]
|
25
|
+
# self.user_device = ANDROID_DEVICES
|
27
26
|
# self.app_install_state = NOT_INSTALLED
|
28
27
|
end
|
29
28
|
|
30
29
|
def validate!
|
31
30
|
{ gender: genders, countries: countries, user_os: user_os, user_device: user_device }.each_pair do |key, array|
|
32
|
-
if array.
|
31
|
+
if !array.nil? && !array.is_a?(Array)
|
33
32
|
raise Exception, "#{self.class.name}: #{key} must be an array"
|
34
33
|
end
|
35
34
|
end
|
@@ -37,8 +36,8 @@ module FacebookAds
|
|
37
36
|
{ genders: [genders, GENDERS], user_os: [user_os, OSES], user_device: [user_device, DEVICES] }.each_pair do |key, provided_and_acceptable|
|
38
37
|
provided, acceptable = provided_and_acceptable
|
39
38
|
|
40
|
-
if provided.
|
41
|
-
raise Exception, "#{self.class.name}: #{
|
39
|
+
if !provided.nil? && !(invalid = provided.detect { |value| !acceptable.include?(value) }).nil?
|
40
|
+
raise Exception, "#{self.class.name}: #{invalid} is an invalid #{key}"
|
42
41
|
end
|
43
42
|
end
|
44
43
|
|
@@ -56,6 +55,5 @@ module FacebookAds
|
|
56
55
|
app_install_state: app_install_state
|
57
56
|
}.compact
|
58
57
|
end
|
59
|
-
|
60
58
|
end
|
61
59
|
end
|
data/lib/facebook_ads/base.rb
CHANGED
@@ -1,47 +1,70 @@
|
|
1
1
|
module FacebookAds
|
2
|
+
# The base class for all ads objects.
|
2
3
|
class Base < Hashie::Mash
|
3
|
-
|
4
4
|
class << self
|
5
|
-
|
6
5
|
def find(id)
|
7
6
|
get("/#{id}", objectify: true)
|
8
7
|
end
|
9
8
|
|
10
9
|
def get(path, query: {}, objectify:)
|
11
10
|
query = pack(query, objectify) # Adds access token, fields, etc.
|
12
|
-
|
13
|
-
|
14
|
-
response =
|
11
|
+
uri = "#{FacebookAds.base_uri}#{path}?" + build_nested_query(query)
|
12
|
+
FacebookAds.logger.debug "GET #{uri}"
|
13
|
+
response = begin
|
14
|
+
RestClient.get(uri)
|
15
|
+
rescue RestClient::Exception => e
|
16
|
+
exception(:get, path, e)
|
17
|
+
end
|
18
|
+
unpack(response, objectify: objectify)
|
15
19
|
end
|
16
20
|
|
17
21
|
def post(path, query: {}, objectify:)
|
18
22
|
query = pack(query, objectify)
|
19
|
-
|
20
|
-
|
21
|
-
response =
|
23
|
+
uri = "#{FacebookAds.base_uri}#{path}"
|
24
|
+
FacebookAds.logger.debug "POST #{uri} #{query}"
|
25
|
+
response = begin
|
26
|
+
RestClient.post(uri, query)
|
27
|
+
rescue RestClient::Exception => e
|
28
|
+
exception(:post, path, e)
|
29
|
+
end
|
30
|
+
unpack(response, objectify: objectify)
|
22
31
|
end
|
23
32
|
|
24
33
|
def delete(path, query: {})
|
25
34
|
query = pack(query, false)
|
26
|
-
|
27
|
-
|
28
|
-
response =
|
35
|
+
uri = "#{FacebookAds.base_uri}#{path}?" + build_nested_query(query)
|
36
|
+
FacebookAds.logger.debug "DELETE #{uri}"
|
37
|
+
response = begin
|
38
|
+
RestClient.delete(uri)
|
39
|
+
rescue RestClient::Exception => e
|
40
|
+
exception(:delete, path, e)
|
41
|
+
end
|
42
|
+
unpack(response, objectify: false)
|
29
43
|
end
|
30
44
|
|
31
45
|
def paginate(path, query: {})
|
46
|
+
query[:limit] ||= 100
|
47
|
+
limit = query[:limit]
|
32
48
|
response = get(path, query: query.merge(fields: self::FIELDS.join(',')), objectify: false)
|
33
|
-
data = response['data'].
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
49
|
+
data = response['data'].nil? ? [] : response['data']
|
50
|
+
|
51
|
+
if data.length == limit
|
52
|
+
while !(paging = response['paging']).nil? && !(url = paging['next']).nil?
|
53
|
+
FacebookAds.logger.debug "GET #{url}"
|
54
|
+
response = begin
|
55
|
+
RestClient.get(url)
|
56
|
+
rescue RestClient::Exception => e
|
57
|
+
exception(:get, url, e)
|
58
|
+
end
|
59
|
+
response = unpack(response, objectify: false)
|
60
|
+
data += response['data'] unless response['data'].nil?
|
61
|
+
end
|
39
62
|
end
|
40
63
|
|
41
|
-
if data.
|
42
|
-
data.map { |hash| instantiate(hash) }
|
43
|
-
else
|
64
|
+
if data.nil?
|
44
65
|
[]
|
66
|
+
else
|
67
|
+
data.map { |hash| instantiate(hash) }
|
45
68
|
end
|
46
69
|
end
|
47
70
|
|
@@ -61,75 +84,98 @@ module FacebookAds
|
|
61
84
|
def pack(hash, objectify)
|
62
85
|
hash = hash.merge(access_token: FacebookAds.access_token)
|
63
86
|
hash = hash.merge(fields: self::FIELDS.join(',')) if objectify
|
64
|
-
hash.
|
87
|
+
hash.delete_if { |_k, v| v.nil? }
|
65
88
|
end
|
66
89
|
|
67
90
|
def unpack(response, objectify:)
|
68
|
-
|
69
|
-
|
91
|
+
raise Exception, 'Invalid nil response' if response.nil?
|
92
|
+
response = response.body if response.is_a?(RestClient::Response)
|
93
|
+
|
94
|
+
if response.is_a?(String)
|
95
|
+
response = begin
|
96
|
+
JSON.parse(response)
|
97
|
+
rescue JSON::ParserError
|
98
|
+
raise Exception, "Invalid JSON response: #{response.inspect}"
|
99
|
+
end
|
70
100
|
end
|
71
101
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
end
|
102
|
+
raise Exception, "Invalid response: #{response.inspect}" unless response.is_a?(Hash)
|
103
|
+
raise Exception, "[#{response['error']['code']}] #{response['error']['message']} - raw response: #{response.inspect}" unless response['error'].nil?
|
104
|
+
return response unless objectify
|
76
105
|
|
77
|
-
if
|
78
|
-
|
79
|
-
data.map { |hash| instantiate(hash) }
|
80
|
-
else
|
81
|
-
instantiate(response)
|
82
|
-
end
|
106
|
+
if response.key?('data') && (data = response['data']).is_a?(Array)
|
107
|
+
data.map { |hash| instantiate(hash) }
|
83
108
|
else
|
84
|
-
response
|
109
|
+
instantiate(response)
|
85
110
|
end
|
86
111
|
end
|
87
112
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
if data.present?
|
92
|
-
response = self.class.post("/#{id}", query: data, objectify: false)
|
113
|
+
def escape(s)
|
114
|
+
URI.encode_www_form_component(s)
|
115
|
+
end
|
93
116
|
|
94
|
-
|
95
|
-
|
117
|
+
# https://github.com/rack/rack/blob/master/lib/rack/utils.rb
|
118
|
+
def build_nested_query(value, prefix = nil)
|
119
|
+
case value
|
120
|
+
when Array
|
121
|
+
value.map { |v| build_nested_query(v, "#{prefix}[]") }.join('&')
|
122
|
+
when Hash
|
123
|
+
value.map { |k, v| build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k)) }.reject(&:empty?).join('&')
|
124
|
+
when nil
|
125
|
+
prefix
|
96
126
|
else
|
97
|
-
raise
|
127
|
+
raise ArgumentError, 'value must be a Hash' if prefix.nil?
|
128
|
+
"#{prefix}=#{escape(value)}"
|
98
129
|
end
|
99
|
-
else
|
100
|
-
false
|
101
130
|
end
|
102
|
-
end
|
103
131
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
132
|
+
def exception(verb, path, e)
|
133
|
+
if e.response.is_a?(String)
|
134
|
+
begin
|
135
|
+
hash = JSON.parse(e.response)
|
136
|
+
error = hash['error']
|
137
|
+
message = error.nil? ? hash.inspect : "#{error['type']} code=#{error['code']} message=#{error['message']}"
|
138
|
+
rescue JSON::ParserError
|
139
|
+
message = e.response.first(100)
|
140
|
+
end
|
111
141
|
else
|
112
|
-
|
142
|
+
message = e.response.inspect
|
113
143
|
end
|
114
|
-
|
115
|
-
|
144
|
+
|
145
|
+
FacebookAds.logger.error "#{verb.upcase} #{path} [#{e.message}] #{message}"
|
146
|
+
raise e
|
116
147
|
end
|
117
148
|
end
|
118
149
|
|
150
|
+
def update(data)
|
151
|
+
return false if data.nil?
|
152
|
+
response = self.class.post("/#{id}", query: data, objectify: false)
|
153
|
+
raise Exception, "Invalid response from update: #{response.inspect}" unless @response.is_a?(Hash) && response.key?('success')
|
154
|
+
response['success']
|
155
|
+
end
|
156
|
+
|
157
|
+
def save
|
158
|
+
return nil if changes.nil? || changes.length.zero?
|
159
|
+
data = {}
|
160
|
+
changes.keys.each { |key| data[key] = self[key] }
|
161
|
+
return nil unless update(data)
|
162
|
+
self.class.find(id)
|
163
|
+
end
|
164
|
+
|
119
165
|
def destroy(path: nil, query: {})
|
120
166
|
response = self.class.delete(path || "/#{id}", query: query)
|
121
|
-
|
122
|
-
|
123
|
-
response['success']
|
124
|
-
else
|
125
|
-
raise Exception, "Invalid response from destroy: #{response.inspect}"
|
126
|
-
end
|
167
|
+
raise Exception, "Invalid response from destroy: #{response.inspect}" unless response.key?('success')
|
168
|
+
response['success']
|
127
169
|
end
|
128
170
|
|
129
171
|
protected
|
130
172
|
|
131
173
|
attr_accessor :changes
|
132
174
|
|
175
|
+
def persisted?
|
176
|
+
!id.nil?
|
177
|
+
end
|
178
|
+
|
133
179
|
private
|
134
180
|
|
135
181
|
def []=(key, value)
|
@@ -146,6 +192,5 @@ module FacebookAds
|
|
146
192
|
|
147
193
|
new_values
|
148
194
|
end
|
149
|
-
|
150
195
|
end
|
151
196
|
end
|