neuron-client 0.2.6 → 0.3.0
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/README.md +11 -8
- data/lib/neuron-client/api.rb +5 -0
- data/lib/neuron-client/model/ad.rb +142 -0
- data/lib/neuron-client/model/ad_calculations.rb +325 -0
- data/lib/neuron-client/model/ad_zone.rb +45 -0
- data/lib/neuron-client/model/base.rb +233 -18
- data/lib/neuron-client/model/blocked_referer.rb +20 -0
- data/lib/neuron-client/model/blocked_user_agent.rb +21 -0
- data/lib/neuron-client/model/geo_target.rb +36 -0
- data/lib/neuron-client/model/pixel.rb +22 -0
- data/lib/neuron-client/model/report.rb +35 -0
- data/lib/neuron-client/model/s3_file.rb +30 -0
- data/lib/neuron-client/model/zone.rb +64 -0
- data/lib/neuron-client/model/zone_calculations.rb +37 -0
- data/lib/neuron-client/schema/ad.rb +406 -0
- data/lib/neuron-client/schema/ad_zone.rb +49 -0
- data/lib/neuron-client/schema/blocked_referer.rb +52 -0
- data/lib/neuron-client/schema/blocked_user_agent.rb +64 -0
- data/lib/neuron-client/schema/common.rb +220 -0
- data/lib/neuron-client/schema/event.rb +17 -0
- data/lib/neuron-client/schema/geo_target.rb +33 -0
- data/lib/neuron-client/schema/pixel.rb +39 -0
- data/lib/neuron-client/schema/report.rb +59 -0
- data/lib/neuron-client/schema/s3_file.rb +87 -0
- data/lib/neuron-client/schema/zone.rb +214 -0
- data/lib/neuron-client/version.rb +1 -1
- data/lib/neuron-client.rb +24 -59
- data/neuron-client.gemspec +3 -0
- data/spec/lib/admin_connection_spec.rb +234 -0
- data/spec/lib/api_spec.rb +41 -63
- data/spec/lib/model/ad_calculations_spec.rb +1146 -0
- data/spec/lib/model/ad_spec.rb +253 -0
- data/spec/lib/model/ad_zone_spec.rb +15 -0
- data/spec/lib/model/base_spec.rb +5 -83
- data/spec/lib/model/blocked_referer_spec.rb +36 -0
- data/spec/lib/model/blocked_user_agent_spec.rb +36 -0
- data/spec/lib/model/geo_target_spec.rb +28 -0
- data/spec/lib/model/pixel_spec.rb +36 -0
- data/spec/lib/model/report_spec.rb +17 -0
- data/spec/lib/{s3_file_spec.rb → model/s3_file_spec.rb} +6 -5
- data/spec/lib/model/zone_calculations_spec.rb +49 -0
- data/spec/lib/model/zone_spec.rb +155 -0
- data/spec/lib/schema/ad_spec.rb +515 -0
- data/spec/lib/schema/ad_zone_spec.rb +149 -0
- data/spec/lib/schema/blocked_referer_spec.rb +136 -0
- data/spec/lib/schema/blocked_user_agent_spec.rb +147 -0
- data/spec/lib/schema/geo_target_spec.rb +92 -0
- data/spec/lib/schema/pixel_spec.rb +125 -0
- data/spec/lib/schema/report_spec.rb +129 -0
- data/spec/lib/schema/s3_file_spec.rb +164 -0
- data/spec/lib/schema/zone_spec.rb +243 -0
- data/spec/spec_helper.rb +2 -1
- metadata +141 -121
- data/lib/neuron-client/model/admin/ad.rb +0 -22
- data/lib/neuron-client/model/admin/ad_zone.rb +0 -15
- data/lib/neuron-client/model/admin/base.rb +0 -91
- data/lib/neuron-client/model/admin/blocked_referer.rb +0 -12
- data/lib/neuron-client/model/admin/blocked_user_agent.rb +0 -12
- data/lib/neuron-client/model/admin/geo_target.rb +0 -16
- data/lib/neuron-client/model/admin/pixel.rb +0 -12
- data/lib/neuron-client/model/admin/report.rb +0 -15
- data/lib/neuron-client/model/admin/s3_file.rb +0 -12
- data/lib/neuron-client/model/admin/zone.rb +0 -15
- data/lib/neuron-client/model/common/ad.rb +0 -42
- data/lib/neuron-client/model/common/ad_calculations.rb +0 -329
- data/lib/neuron-client/model/common/ad_zone.rb +0 -17
- data/lib/neuron-client/model/common/base.rb +0 -67
- data/lib/neuron-client/model/common/blocked_referer.rb +0 -16
- data/lib/neuron-client/model/common/blocked_user_agent.rb +0 -16
- data/lib/neuron-client/model/common/geo_target.rb +0 -16
- data/lib/neuron-client/model/common/pixel.rb +0 -18
- data/lib/neuron-client/model/common/report.rb +0 -21
- data/lib/neuron-client/model/common/s3_file.rb +0 -16
- data/lib/neuron-client/model/common/zone.rb +0 -22
- data/lib/neuron-client/model/common/zone_calculations.rb +0 -41
- data/lib/neuron-client/model/membase/ad.rb +0 -49
- data/lib/neuron-client/model/membase/ad_zone.rb +0 -11
- data/lib/neuron-client/model/membase/blocked_referer.rb +0 -20
- data/lib/neuron-client/model/membase/blocked_user_agent.rb +0 -20
- data/lib/neuron-client/model/membase/geo_target.rb +0 -11
- data/lib/neuron-client/model/membase/pixel.rb +0 -22
- data/lib/neuron-client/model/membase/report.rb +0 -11
- data/lib/neuron-client/model/membase/s3_file.rb +0 -11
- data/lib/neuron-client/model/membase/zone.rb +0 -30
- data/lib/neuron-client/model/models.rb +0 -15
- data/spec/lib/model/admin/ad_spec.rb +0 -34
- data/spec/lib/model/admin/ad_zone_spec.rb +0 -19
- data/spec/lib/model/admin/base_spec.rb +0 -11
- data/spec/lib/model/admin/blocked_referer_spec.rb +0 -11
- data/spec/lib/model/admin/blocked_user_agent_spec.rb +0 -11
- data/spec/lib/model/admin/geo_target_spec.rb +0 -30
- data/spec/lib/model/admin/report_spec.rb +0 -21
- data/spec/lib/model/admin/s3_spec.rb +0 -11
- data/spec/lib/model/admin/zone_spec.rb +0 -21
- data/spec/lib/model/common/ad_calculations_spec.rb +0 -1151
- data/spec/lib/model/common/ad_spec.rb +0 -11
- data/spec/lib/model/common/ad_zone_spec.rb +0 -11
- data/spec/lib/model/common/base_spec.rb +0 -11
- data/spec/lib/model/common/blocked_referer_spec.rb +0 -11
- data/spec/lib/model/common/blocked_user_agent_spec.rb +0 -11
- data/spec/lib/model/common/geo_target_spec.rb +0 -11
- data/spec/lib/model/common/report_spec.rb +0 -11
- data/spec/lib/model/common/s3_spec.rb +0 -11
- data/spec/lib/model/common/zone_calculations_spec.rb +0 -54
- data/spec/lib/model/common/zone_spec.rb +0 -11
- data/spec/lib/model/membase/ad_spec.rb +0 -54
- data/spec/lib/model/membase/ad_zone_spec.rb +0 -11
- data/spec/lib/model/membase/base_spec.rb +0 -11
- data/spec/lib/model/membase/blocked_referer_spec.rb +0 -34
- data/spec/lib/model/membase/blocked_user_agent_spec.rb +0 -34
- data/spec/lib/model/membase/geo_target_spec.rb +0 -11
- data/spec/lib/model/membase/pixel_spec.rb +0 -34
- data/spec/lib/model/membase/report_spec.rb +0 -11
- data/spec/lib/model/membase/s3_spec.rb +0 -11
- data/spec/lib/model/membase/zone_spec.rb +0 -32
- data/spec/lib/old_spec.rb +0 -437
data/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Connect to the admin server for read/write access to exposed models:
|
|
|
11
11
|
config.connection_type = :admin
|
|
12
12
|
config.admin_url = "https://example.com"
|
|
13
13
|
config.admin_key = "secret"
|
|
14
|
+
config.validate = (ENV['RAILS_ENV'] != 'production')
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
Short form to copy and paste into console:
|
|
@@ -32,8 +33,11 @@ Connect to the Membase (or Memcached) Server for limited read access to some exp
|
|
|
32
33
|
Create a new API, configure and use it for one specific model:
|
|
33
34
|
|
|
34
35
|
api = Neuron::Client::API.new
|
|
35
|
-
api.configure
|
|
36
|
-
|
|
36
|
+
api.configure do |config|
|
|
37
|
+
config.connection_type = :membase
|
|
38
|
+
config.membase_servers = '127.0.0.1:11211'
|
|
39
|
+
end
|
|
40
|
+
Neuron::Client::Ad.api = api
|
|
37
41
|
|
|
38
42
|
Zones
|
|
39
43
|
=====
|
|
@@ -42,23 +46,22 @@ Zones
|
|
|
42
46
|
|
|
43
47
|
Create a zone:
|
|
44
48
|
|
|
45
|
-
zone = Neuron::Client::
|
|
49
|
+
zone = Neuron::Client::Zone.new(:name => 'test', :response_type => 'Redirect', :redirect_url => 'http://example.com')
|
|
46
50
|
zone.save
|
|
47
51
|
|
|
48
52
|
... or simply:
|
|
49
53
|
|
|
50
|
-
Neuron::Client::
|
|
54
|
+
Neuron::Client::Zone.create(:name => 'test', :response_type => 'Redirect', :redirect_url => 'http://example.com')
|
|
51
55
|
|
|
52
56
|
List all zones:
|
|
53
57
|
|
|
54
|
-
Neuron::Client::
|
|
58
|
+
Neuron::Client::Zone.all # => Array of Zone objects (with limited attributes)
|
|
55
59
|
|
|
56
60
|
Find a zone by ID:
|
|
57
61
|
|
|
58
|
-
Neuron::Client::
|
|
62
|
+
Neuron::Client::Zone.find(zone_id)
|
|
59
63
|
|
|
60
64
|
Update a zone:
|
|
61
65
|
|
|
62
|
-
zone.update_attributes(:
|
|
66
|
+
zone.update_attributes(:redirect_url => 'http://example.com/store')
|
|
63
67
|
|
|
64
|
-
TODO: Finish and finalize the API
|
data/lib/neuron-client/api.rb
CHANGED
|
@@ -10,6 +10,7 @@ module Neuron
|
|
|
10
10
|
|
|
11
11
|
configure_admin_connection if config.connection_type == :admin
|
|
12
12
|
configure_membase_connection if config.connection_type == :membase
|
|
13
|
+
@validate = (config.validate != false)
|
|
13
14
|
|
|
14
15
|
self
|
|
15
16
|
end
|
|
@@ -18,6 +19,10 @@ module Neuron
|
|
|
18
19
|
@config.connection_type
|
|
19
20
|
end
|
|
20
21
|
|
|
22
|
+
def validate?
|
|
23
|
+
@validate != false
|
|
24
|
+
end
|
|
25
|
+
|
|
21
26
|
private
|
|
22
27
|
|
|
23
28
|
def config
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
module Neuron
|
|
2
|
+
module Client
|
|
3
|
+
class Ad
|
|
4
|
+
include Base
|
|
5
|
+
include AdCalculations
|
|
6
|
+
|
|
7
|
+
ACTIVE_TTL = 60 #seconds
|
|
8
|
+
PRESSURE_TTL = 60 #seconds
|
|
9
|
+
|
|
10
|
+
REDIRECT = 'Redirect'
|
|
11
|
+
VIDEO_AD = 'VideoAd'
|
|
12
|
+
VAST_NETWORK = 'VastNetwork'
|
|
13
|
+
ACUDEO_NETWORK = 'AcudeoNetwork'
|
|
14
|
+
RESPONSE_TYPES = [REDIRECT, VIDEO_AD, VAST_NETWORK, ACUDEO_NETWORK]
|
|
15
|
+
|
|
16
|
+
SOCIAL_TYPES = %w(facebook googleplus twitter youtube)
|
|
17
|
+
TIME_ZONES = ActiveSupport::TimeZone.all.collect{|tz| tz.name }
|
|
18
|
+
FREQUENCY_CAP_WINDOWS = %w(Day Hour)
|
|
19
|
+
VAST_TRACKER_TYPES = %w(impression clickTracking firstQuartile midpoint thirdQuartile complete)
|
|
20
|
+
|
|
21
|
+
ATTRIBUTES = [
|
|
22
|
+
# Basics
|
|
23
|
+
:id, # integer
|
|
24
|
+
:approved, # "Yes" or "No"
|
|
25
|
+
:daily_cap, # nil, or integer > 1
|
|
26
|
+
:day_partitions, # nil, or 168 characters of "T" and "F", Sunday first
|
|
27
|
+
:end_datetime, # nil, or a datetime, in UTC
|
|
28
|
+
:frequency_cap, # nil, or {"limit" => 1..10, "window" => in Ad::FREQUENCY_CAP_WINDOWS}
|
|
29
|
+
:geo_target_netacuity_ids, # hash, where keys are in GeoTarget::TYPES, and values are arrays of integer IDs (from NetAcuity)
|
|
30
|
+
:ideal_impressions_per_hour, # nil, or number > 0
|
|
31
|
+
:name, # nil, or string with 255 chars or less
|
|
32
|
+
:overall_cap, # nil, or integer >= 1
|
|
33
|
+
:pixel_ids, # array of integers
|
|
34
|
+
:response_type, # a string, in Ad::RESPONSE_TYPES
|
|
35
|
+
:start_datetime, # a datetime, in UTC
|
|
36
|
+
:time_zone, # a string, in Ad::TIME_ZONES.
|
|
37
|
+
:zone_links, # a hash, with zone_ids as keys, and {"priority" => integer, "weight" => number} hashes as values.
|
|
38
|
+
|
|
39
|
+
# Timestamps
|
|
40
|
+
:created_at, :updated_at, # string, datetime in UTC
|
|
41
|
+
|
|
42
|
+
# Counts
|
|
43
|
+
:today_impressed, # nil, or integer >= 0
|
|
44
|
+
:total_impressed, # nil, or integer >= 0
|
|
45
|
+
|
|
46
|
+
# "Redirect" advertisements must have:
|
|
47
|
+
:redirect_url, # a URI string
|
|
48
|
+
|
|
49
|
+
# "VideoAd" advertisements must have:
|
|
50
|
+
:video_flv_url, # a URI string (no macros)
|
|
51
|
+
:clickthru_url, # a URI string, perhaps with macros
|
|
52
|
+
:companion_ad_html, # nil, or a string (raw html, perhaps with macros)
|
|
53
|
+
:social_urls, # a hash, where keys are in Ad::SOCIAL_TYPES and values are URI strings
|
|
54
|
+
:vast_tracker_urls, # a hash, where keys are in Ad::VAST_TRACKER_TYPES and values are arrays of URI strings
|
|
55
|
+
|
|
56
|
+
# "VastNetwork" advertisements must have:
|
|
57
|
+
:vast_url, # a URI string, perhaps with macros
|
|
58
|
+
|
|
59
|
+
# "AcudeoNetwork" advertisements must have:
|
|
60
|
+
:acudeo_program_id, # a slug
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
attr_accessor *ATTRIBUTES
|
|
64
|
+
|
|
65
|
+
def attributes
|
|
66
|
+
ATTRIBUTES
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
STATISTIC_TYPES = %w(selections undeliveries impressions redirects clicks)
|
|
70
|
+
|
|
71
|
+
def recent(statistic, by=nil)
|
|
72
|
+
connected_to_admin!
|
|
73
|
+
if validate?
|
|
74
|
+
unless STATISTIC_TYPES.include?(statistic.to_s)
|
|
75
|
+
raise "Unsupported statistic: #{statistic}"
|
|
76
|
+
end
|
|
77
|
+
unless by.nil? || by.to_s == 'zone'
|
|
78
|
+
raise "Unsupported by: #{by}"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
parameters = by.nil? ? {} : {'by' => by.to_s}
|
|
82
|
+
connection.get("ads/#{id}/recent/#{statistic}", parameters)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def unlink(zone_id)
|
|
86
|
+
connected_to_admin!
|
|
87
|
+
validate_uuid!(zone_id)
|
|
88
|
+
connection.delete("ads/#{id}/zones/#{zone_id}")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def total_impressed
|
|
92
|
+
if connected_to_membase?
|
|
93
|
+
key = "count_delivery_ad_#{id}"
|
|
94
|
+
connection.get(key,1).to_f
|
|
95
|
+
else
|
|
96
|
+
@total_impressed || 0
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def today_impressed
|
|
101
|
+
if connected_to_membase?
|
|
102
|
+
now_adjusted_for_ad_time_zone = Time.now.in_time_zone(self.time_zone)
|
|
103
|
+
formatted_date = now_adjusted_for_ad_time_zone.strftime('%Y%m%d') # format to YYYYMMDD
|
|
104
|
+
key = "count_delivery_#{formatted_date}_ad_#{id}"
|
|
105
|
+
connection.get(key,1).to_f
|
|
106
|
+
else
|
|
107
|
+
@today_impressed || 0
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def active?
|
|
112
|
+
if connected_to_membase?
|
|
113
|
+
connection.fetch("Ad:#{id}:active", ACTIVE_TTL) do
|
|
114
|
+
calculate_active?(Time.now, total_impressed, today_impressed)
|
|
115
|
+
end
|
|
116
|
+
else
|
|
117
|
+
calculate_active?(Time.now, total_impressed, today_impressed)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def pressure
|
|
122
|
+
if connected_to_membase?
|
|
123
|
+
connection.fetch("Ad:#{id}:pressure", PRESSURE_TTL) do
|
|
124
|
+
calculate_pressure(Time.now, total_impressed, today_impressed)
|
|
125
|
+
end
|
|
126
|
+
else
|
|
127
|
+
calculate_pressure(Time.now, total_impressed, today_impressed)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
protected
|
|
132
|
+
|
|
133
|
+
def to_update_hash(*except)
|
|
134
|
+
super(:total_impressed, :today_impressed)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def to_create_hash(*except)
|
|
138
|
+
super(:total_impressed, :today_impressed)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
module Neuron
|
|
2
|
+
module Client
|
|
3
|
+
module AdCalculations
|
|
4
|
+
# This module expects the following methods to be defined:
|
|
5
|
+
# start_datetime (Time, String, or nil)
|
|
6
|
+
# end_datetime (Time, String, or nil)
|
|
7
|
+
# time_zone (String)
|
|
8
|
+
# day_partitions (String, or nil, length = 7*24, matches /^[TF]+$/)
|
|
9
|
+
# daily_cap (FixNum, or nil)
|
|
10
|
+
# overall_cap (FixNum, or nil)
|
|
11
|
+
# ideal_impressions_per_hour (Number, or nil)
|
|
12
|
+
# total_impressed (Integer, >= 0)
|
|
13
|
+
# today_impressed (Integer, >= 0)
|
|
14
|
+
|
|
15
|
+
def active?
|
|
16
|
+
calculate_active?(Time.now, total_impressed, today_impressed)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def pressure
|
|
20
|
+
calculate_pressure(Time.now, total_impressed, today_impressed)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def calculate_active?(time, total, today)
|
|
24
|
+
time = time.in_time_zone(time_zone)
|
|
25
|
+
return false unless within_date_range?(time)
|
|
26
|
+
return false if partitioned? && !partitioned_hour?(time)
|
|
27
|
+
!cap_met?(total, today)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Set the optional "active" parameter to true or false if you already know
|
|
31
|
+
def calculate_pressure(time, total, today, active=nil)
|
|
32
|
+
return nil if active == false
|
|
33
|
+
return nil unless active || calculate_active?(time,total,today)
|
|
34
|
+
time = time.in_time_zone(time_zone)
|
|
35
|
+
if daily_capped?
|
|
36
|
+
if daily_cap_precludes_overall_cap?(time,total,today)
|
|
37
|
+
calculate_overall_pressure(time, total)
|
|
38
|
+
else
|
|
39
|
+
calculate_today_pressure(time, today)
|
|
40
|
+
end
|
|
41
|
+
elsif overall_pressure_exists?
|
|
42
|
+
calculate_overall_pressure(time, total)
|
|
43
|
+
else
|
|
44
|
+
(ideal_impressions_per_hour || 1.0).to_f
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def in_ad_time_zone(time)
|
|
51
|
+
if time.present?
|
|
52
|
+
(time.is_a?(String) ? Time.parse(time) : time).in_time_zone(time_zone)
|
|
53
|
+
end
|
|
54
|
+
rescue
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def start_in_time_zone
|
|
59
|
+
in_ad_time_zone(start_datetime)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def end_in_time_zone
|
|
63
|
+
in_ad_time_zone(end_datetime)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def daily_capped?
|
|
67
|
+
!daily_cap.nil? && daily_cap >= 0
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def overall_capped?
|
|
71
|
+
!overall_cap.nil? && overall_cap >= 0
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def overall_pressure_exists?
|
|
75
|
+
overall_capped? && end_datetime.present?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def partitioned?
|
|
79
|
+
!day_partitions.nil? && day_partitions.length == 24*7 && day_partitions != "T"*24*7
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def within_date_range?(time)
|
|
83
|
+
return false if start_datetime.present? && time < start_in_time_zone
|
|
84
|
+
return false if end_datetime.present? && time >= end_in_time_zone
|
|
85
|
+
true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Assume time is in the time zone of the ad.
|
|
89
|
+
# Doesn't worry about whether or not the time is in the ad's date range.
|
|
90
|
+
# Assumes ad is partitioned.
|
|
91
|
+
def partitioned_hour?(time)
|
|
92
|
+
day_partitions.at((time.wday * 24) + time.hour) == 'T'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Assume time is in the time zone of the ad.
|
|
96
|
+
# Doesn't worry about whether or not the time is in the ad's date range.
|
|
97
|
+
# Assumes ad is partitioned.
|
|
98
|
+
# Returns true if there is any active partition the day of the given time.
|
|
99
|
+
def partitioned_day?(time)
|
|
100
|
+
day_partitions[(time.wday) * 24, 24].include?('T')
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Assumes time is in the time zone of the ad.
|
|
104
|
+
# Doesn't worry about date range or day partitions.
|
|
105
|
+
def cap_met?(total_impressed, today_impressed)
|
|
106
|
+
return true if daily_capped? && (daily_cap <= today_impressed)
|
|
107
|
+
return true if overall_capped? && (overall_cap <= total_impressed)
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Assumes time is in time_zone of the ad.
|
|
112
|
+
# Returns true if there's no way we can hit the overall cap
|
|
113
|
+
# without busting the daily cap.
|
|
114
|
+
def daily_cap_precludes_overall_cap?(time,total_impressed,today_impressed)
|
|
115
|
+
return false unless daily_capped? && overall_capped?
|
|
116
|
+
return false unless self.end_datetime.present?
|
|
117
|
+
a = remaining_impressions_via_daily_cap(time, today_impressed)
|
|
118
|
+
b = remaining_impressions_via_overall_cap(total_impressed)
|
|
119
|
+
a < b
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def remaining_impressions_via_overall_cap(total_impressed)
|
|
123
|
+
[overall_cap - total_impressed, 0].max
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def remaining_impressions_today(today_impressed)
|
|
127
|
+
[daily_cap - today_impressed, 0].max
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def remaining_impressions_via_daily_cap(time, today_impressed)
|
|
131
|
+
days = remaining_days(time) - 1 # exclude today
|
|
132
|
+
daily_cap * days + remaining_impressions_today(today_impressed)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Assumes time is in time_zone of the ad.
|
|
136
|
+
# Assume daily_cap is present.
|
|
137
|
+
def calculate_today_pressure(time, today_impressed)
|
|
138
|
+
hours = remaining_hours_today(time)
|
|
139
|
+
if hours <= 0
|
|
140
|
+
0.0
|
|
141
|
+
else
|
|
142
|
+
impressions = remaining_impressions_today(today_impressed)
|
|
143
|
+
2 * impressions / hours.to_f
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Assumes time is in time_zone of the ad.
|
|
148
|
+
# Assume overall_cap is present.
|
|
149
|
+
# Assume end_datetime is present.
|
|
150
|
+
def calculate_overall_pressure(time, total_impressed)
|
|
151
|
+
hours = remaining_hours(time)
|
|
152
|
+
if hours <= 0
|
|
153
|
+
0.0
|
|
154
|
+
else
|
|
155
|
+
impressions = remaining_impressions_via_overall_cap(total_impressed)
|
|
156
|
+
impressions / hours.to_f
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# remaining_days : the total integer number of days that an ad will run
|
|
161
|
+
# (even for part of the day) between now and the
|
|
162
|
+
# end_datetime.
|
|
163
|
+
# Assume all dates/times are in the time_zone of the ad.
|
|
164
|
+
# Assume end_datetime is present.
|
|
165
|
+
def remaining_days(time)
|
|
166
|
+
ending = end_in_time_zone
|
|
167
|
+
beginning = [time, start_in_time_zone].compact.max
|
|
168
|
+
return 0 unless beginning < ending
|
|
169
|
+
if partitioned?
|
|
170
|
+
end_of_beginning_week = beginning_of_week(beginning) + 7.days
|
|
171
|
+
beginning_of_end_week = beginning_of_week(ending)
|
|
172
|
+
|
|
173
|
+
if end_of_beginning_week < beginning_of_end_week
|
|
174
|
+
head_days = partitioned_days(beginning, end_of_beginning_week)
|
|
175
|
+
tail_days = partitioned_days(beginning_of_end_week, ending)
|
|
176
|
+
whole_weeks = (beginning_of_end_week-end_of_beginning_week) / 7.days
|
|
177
|
+
head_days + (whole_weeks * days_per_week) + tail_days
|
|
178
|
+
else
|
|
179
|
+
partitioned_days(beginning, ending)
|
|
180
|
+
end
|
|
181
|
+
else
|
|
182
|
+
(ending.beginning_of_day.to_datetime -
|
|
183
|
+
beginning.beginning_of_day.to_datetime) + 1
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# remaining_hours : the total number of hours that an ad will run from now
|
|
188
|
+
# to the end_datetime, taking day parting into account.
|
|
189
|
+
# Assume all dates/times are in the time_zone of the ad.
|
|
190
|
+
def remaining_hours(time)
|
|
191
|
+
ending = end_in_time_zone
|
|
192
|
+
beginning = [time, start_in_time_zone].compact.max
|
|
193
|
+
return 0 unless beginning < ending
|
|
194
|
+
if partitioned?
|
|
195
|
+
|
|
196
|
+
end_of_beginning_week = beginning_of_week(beginning) + 7.days
|
|
197
|
+
beginning_of_end_week = beginning_of_week(ending)
|
|
198
|
+
|
|
199
|
+
if end_of_beginning_week < beginning_of_end_week
|
|
200
|
+
head_hours = partitioned_hours(beginning, end_of_beginning_week)
|
|
201
|
+
tail_hours = partitioned_hours(beginning_of_end_week, ending)
|
|
202
|
+
whole_weeks = (beginning_of_end_week-end_of_beginning_week) / 7.days
|
|
203
|
+
head_hours + (whole_weeks * hours_per_week) + tail_hours
|
|
204
|
+
else
|
|
205
|
+
partitioned_hours(beginning, ending)
|
|
206
|
+
end
|
|
207
|
+
else
|
|
208
|
+
actual_hours(beginning, ending)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# remaining_hours_today : the total number of hours that an ad will run
|
|
213
|
+
# between now and the end of the day, taking day
|
|
214
|
+
# parting into account.
|
|
215
|
+
# Assume all dates/times are in the time_zone of the ad.
|
|
216
|
+
def remaining_hours_today(time)
|
|
217
|
+
beginning = [time, start_in_time_zone].compact.max
|
|
218
|
+
ending = [time.beginning_of_day + 1.day, end_in_time_zone].compact.min
|
|
219
|
+
return 0 unless beginning < ending
|
|
220
|
+
if partitioned?
|
|
221
|
+
partitioned_hours(beginning, ending)
|
|
222
|
+
else
|
|
223
|
+
actual_hours(beginning, ending)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Assume time is in the time_zone of the ad.
|
|
228
|
+
# Assume a week begins on Sunday.
|
|
229
|
+
def beginning_of_week(time)
|
|
230
|
+
recent_monday = time.monday
|
|
231
|
+
sunday_before_recent_monday = recent_monday - 1.day
|
|
232
|
+
if (time - sunday_before_recent_monday) < 7.days
|
|
233
|
+
sunday_before_recent_monday
|
|
234
|
+
else
|
|
235
|
+
sunday_before_recent_monday + 7.days
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def beginning_of_hour(time)
|
|
240
|
+
time.change(:min => 0, :sec => 0, :usec => 0)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Assume beginning and ending are in the ad's time_zone.
|
|
244
|
+
# Assume beginning and ending are within the ad's date range.
|
|
245
|
+
# Assume ad is partitioned.
|
|
246
|
+
def partitioned_hours(beginning, ending)
|
|
247
|
+
return 0 unless beginning < ending
|
|
248
|
+
total = 0.0
|
|
249
|
+
this_hour = beginning_of_hour(beginning)
|
|
250
|
+
last_hour = beginning_of_hour(ending)
|
|
251
|
+
if this_hour.to_i == last_hour.to_i
|
|
252
|
+
if partitioned_hour?(this_hour)
|
|
253
|
+
total = actual_hours(beginning, ending)
|
|
254
|
+
end
|
|
255
|
+
else
|
|
256
|
+
next_hour = this_hour + 1.hour
|
|
257
|
+
if partitioned_hour?(this_hour)
|
|
258
|
+
total += actual_hours(beginning, next_hour)
|
|
259
|
+
end
|
|
260
|
+
each_hour(next_hour, last_hour) do |hour|
|
|
261
|
+
total += 1 if partitioned_hour?(hour)
|
|
262
|
+
end
|
|
263
|
+
if partitioned_hour?(last_hour)
|
|
264
|
+
total += actual_hours(last_hour, ending)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
total
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Assume beginning and ending are in the ad's time_zone.
|
|
271
|
+
# Assume beginning and ending are within the ad's date range.
|
|
272
|
+
# Assume ad is partitioned.
|
|
273
|
+
# Assume beginning and ending are within a week of each other.
|
|
274
|
+
# Computes integer number of days where any part of the day is active.
|
|
275
|
+
def partitioned_days(beginning, ending)
|
|
276
|
+
return 0 unless beginning < ending
|
|
277
|
+
|
|
278
|
+
end_of_beginning_day = beginning.beginning_of_day + 1.day
|
|
279
|
+
beginning_of_end_day = ending.beginning_of_day
|
|
280
|
+
|
|
281
|
+
if end_of_beginning_day < beginning_of_end_day
|
|
282
|
+
days = 0
|
|
283
|
+
days += 1 if partitioned_hours(beginning, end_of_beginning_day) > 0
|
|
284
|
+
days += 1 if partitioned_hours(beginning_of_end_day, ending) > 0
|
|
285
|
+
each_day(end_of_beginning_day, beginning_of_end_day) do |day|
|
|
286
|
+
days += 1 if partitioned_day?(day)
|
|
287
|
+
end
|
|
288
|
+
days
|
|
289
|
+
else
|
|
290
|
+
(partitioned_hours(beginning,ending) > 0) ? 1 : 0
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def each_day(beginning, ending)
|
|
295
|
+
time = beginning.clone
|
|
296
|
+
while (time < ending)
|
|
297
|
+
yield time
|
|
298
|
+
time += 1.day
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def each_hour(beginning, ending)
|
|
303
|
+
time = beginning.clone
|
|
304
|
+
while(time < ending)
|
|
305
|
+
yield time
|
|
306
|
+
time += 1.hour
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def actual_hours(beginning, ending)
|
|
311
|
+
(ending.to_f - beginning.to_f) / 3600
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Assumes ad is partitioned.
|
|
315
|
+
def days_per_week
|
|
316
|
+
(0..6).map{|d| day_partitions[d*24,24].include?('T') ? 1 : 0 }.sum
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Assumes ad is partitioned.
|
|
320
|
+
def hours_per_week
|
|
321
|
+
day_partitions.count("T")
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Neuron
|
|
2
|
+
module Client
|
|
3
|
+
class AdZone
|
|
4
|
+
include Base
|
|
5
|
+
|
|
6
|
+
ATTRIBUTES = [
|
|
7
|
+
:ad_id, # integer
|
|
8
|
+
:zone_id, # string, UUID
|
|
9
|
+
:weight, # number
|
|
10
|
+
:priority, # integer, 1..10
|
|
11
|
+
|
|
12
|
+
:created_at, #string, datetime in UTC
|
|
13
|
+
:updated_at, #string, datetime in UTC
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
attr_accessor *ATTRIBUTES
|
|
17
|
+
|
|
18
|
+
def attributes
|
|
19
|
+
ATTRIBUTES
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def new_record?
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def id
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def destroy
|
|
31
|
+
connected_to_admin!
|
|
32
|
+
validate_id!(ad_id)
|
|
33
|
+
validate_uuid!(zone_id)
|
|
34
|
+
connection.delete("zones/#{zone_id}/ads/#{ad_id}")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.unlink(ad_id, zone_id)
|
|
38
|
+
connected_to_admin!
|
|
39
|
+
validate_id!(ad_id)
|
|
40
|
+
validate_uuid!(zone_id)
|
|
41
|
+
connection.delete("zones/#{zone_id}/ads/#{ad_id}")
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|