From a6c1d2e81f4be042aa9087e0d2e06de6dcff13ed Mon Sep 17 00:00:00 2001 From: Brian Simpson Date: Wed, 5 Nov 2014 02:59:28 -0500 Subject: [PATCH] Allow custom pricing for location targets. --- r2/r2/models/promo.py | 132 +++++++++++++++++++++------- r2/r2/public/static/js/sponsored.js | 39 +++++--- 2 files changed, 124 insertions(+), 47 deletions(-) diff --git a/r2/r2/models/promo.py b/r2/r2/models/promo.py index ad79f3c2d..ddf2d1097 100644 --- a/r2/r2/models/promo.py +++ b/r2/r2/models/promo.py @@ -575,13 +575,15 @@ class PromotedLinkRoadblock(tdb_cassandra.View): class PromotionPrices(tdb_cassandra.View): """ - Price rules: - * Location targeting trumps all: Anything targeting a metro gets the global - metro price. - * Any collection or subreddit with a specified price gets that price - * Any collection or subreddit without a specified price gets the global - collection or subreddit price - * Frontpage gets the global collection price. + Check all the following potentially specially priced conditions: + * metro level targeting + * country level targeting (but not if the metro targeting is used) + * collection targeting + * frontpage targeting + * subreddit targeting + + The price is the maximum price for all matching conditions. If no special + conditions are met use the global price. """ @@ -595,10 +597,14 @@ class PromotionPrices(tdb_cassandra.View): "default_validation_class": tdb_cassandra.INT_TYPE, } + COLLECTION_DEFAULT = g.cpm_selfserve_collection.pennies + SUBREDDIT_DEFAULT = g.cpm_selfserve.pennies + COUNTRY_DEFAULT = g.cpm_selfserve_collection.pennies + METRO_DEFAULT = g.cpm_selfserve_geotarget_metro.pennies + @classmethod - def _get_components(cls, target, cpm): + def _rowkey_and_column_from_target(cls, target): rowkey = column_name = None - column_value = cpm if isinstance(target, Target): if target.is_collection: @@ -611,50 +617,94 @@ class PromotionPrices(tdb_cassandra.View): if not rowkey or not column_name: raise ValueError("target must be Target") - return rowkey, column_name, column_value + return rowkey, column_name @classmethod - def set_price(cls, target, cpm): - rowkey, column_name, column_value = cls._get_components(target, cpm) - cls._cf.insert(rowkey, {column_name: column_value}) + def _rowkey_and_column_from_location(cls, location): + if not isinstance(location, Location): + raise ValueError("location must be Location") + + if location.metro: + rowkey = "METRO" + # NOTE: the column_name will also be the key used in the frontend + # to determine pricing + column_name = ''.join(map(str, (location.country, location.metro))) + else: + rowkey = "COUNTRY" + column_name = location.country + return rowkey, column_name @classmethod - def get_price(cls, target, location): - if location and location.metro: - return g.cpm_selfserve_geotarget_metro.pennies + def set_target_price(cls, target, cpm): + rowkey, column_name = cls._rowkey_and_column_from_target(target) + cls._cf.insert(rowkey, {column_name: cpm}) - # check for Frontpage - if (isinstance(target, Target) and - not target.is_collection and - target.subreddit_name == Frontpage.name): - return g.cpm_selfserve_collection.pennies + @classmethod + def set_location_price(cls, location, cpm): + rowkey, column_name = cls._rowkey_and_column_from_location(location) + cls._cf.insert(rowkey, {column_name: cpm}) - # check for target specific override price - rowkey, column_name, _ = cls._get_components(target, None) + @classmethod + def lookup_target_price(cls, target, default): + rowkey, column_name = cls._rowkey_and_column_from_target(target) + target_price = cls._lookup_price(rowkey, column_name) + return target_price or default + + @classmethod + def lookup_location_price(cls, location, default): + rowkey, column_name = cls._rowkey_and_column_from_location(location) + location_price = cls._lookup_price(rowkey, column_name) + return location_price or default + + @classmethod + def _lookup_price(cls, rowkey, column_name): try: columns = cls._cf.get(rowkey, columns=[column_name]) except tdb_cassandra.NotFoundException: columns = {} - if column_name in columns: - return columns[column_name] - # use global price - if isinstance(target, Target): - if target.is_collection: - return g.cpm_selfserve_collection.pennies - else: - return g.cpm_selfserve.pennies + return columns.get(column_name) - raise ValueError("target must be Target") + @classmethod + def get_price(cls, target, location): + prices = [] + + # set location specific prices or use defaults + if location and location.metro: + metro_price = cls.lookup_location_price(location, cls.METRO_DEFAULT) + prices.append(metro_price) + elif location: + country_price = cls.lookup_location_price( + location, cls.COUNTRY_DEFAULT) + prices.append(country_price) + + # set target specific prices or use default + if (not target.is_collection and + target.subreddit_name == Frontpage.name): + # Frontpage is priced as a collection + prices.append(cls.COLLECTION_DEFAULT) + elif target.is_collection: + collection_price = cls.lookup_target_price( + target, cls.COLLECTION_DEFAULT) + prices.append(collection_price) + else: + subreddit_price = cls.lookup_target_price( + target, cls.SUBREDDIT_DEFAULT) + prices.append(subreddit_price) + + return max(prices) @classmethod def get_price_dict(cls): r = { "COLLECTION": {}, "SUBREDDIT": {}, - "METRO": g.cpm_selfserve_geotarget_metro.pennies, + "COUNTRY": {}, + "METRO": {}, "COLLECTION_DEFAULT": g.cpm_selfserve_collection.pennies, "SUBREDDIT_DEFAULT": g.cpm_selfserve.pennies, + "COUNTRY_DEFAULT": g.cpm_selfserve_collection.pennies, + "METRO_DEFAULT": g.cpm_selfserve_geotarget_metro.pennies, } try: @@ -667,10 +717,26 @@ class PromotionPrices(tdb_cassandra.View): except tdb_cassandra.NotFoundException: subreddits = {} + try: + countries = cls._cf.get("COUNTRY") + except tdb_cassandra.NotFoundException: + countries = {} + + try: + metros = cls._cf.get("METRO") + except tdb_cassandra.NotFoundException: + metros = {} + for name, cpm in collections.iteritems(): r["COLLECTION"][name] = cpm for name, cpm in subreddits.iteritems(): r["SUBREDDIT"][name] = cpm + for name, cpm in countries.iteritems(): + r["COUNTRY"][name] = cpm + + for name, cpm in metros.iteritems(): + r["METRO"][name] = cpm + return r diff --git a/r2/r2/public/static/js/sponsored.js b/r2/r2/public/static/js/sponsored.js index 2f1bab3c2..406f3c44d 100644 --- a/r2/r2/public/static/js/sponsored.js +++ b/r2/r2/public/static/js/sponsored.js @@ -820,23 +820,34 @@ var exports = r.sponsored = { }, get_cpm: function($form) { - var isMetroGeotarget = $('#metro').val() !== null && !$('#metro').is(':disabled'), - isSubreddit = $form.find('input[name="targeting"][value="one"]').is(':checked'), - collectionVal = $form.find('input[name="collection"]:checked').val(), - isFrontpage = !isSubreddit && collectionVal === 'none', - isCollection = !isSubreddit && !isFrontpage, - sr = isSubreddit ? $form.find('*[name="sr"]').val() : '', - collection = isCollection ? collectionVal : null + var isMetroGeotarget = $('#metro').val() !== null && !$('#metro').is(':disabled'); + var metro = $('#metro').val(); + var country = $('#country').val(); + var isGeotarget = country !== '' && !$('#country').is(':disabled'); + var isSubreddit = $form.find('input[name="targeting"][value="one"]').is(':checked'); + var collectionVal = $form.find('input[name="collection"]:checked').val(); + var isFrontpage = !isSubreddit && collectionVal === 'none'; + var isCollection = !isSubreddit && !isFrontpage; + var sr = isSubreddit ? $form.find('*[name="sr"]').val() : ''; + var collection = isCollection ? collectionVal : null; + var prices = []; if (isMetroGeotarget) { - return this.priceDict.METRO - } else if (isFrontpage) { - return this.priceDict.COLLECTION_DEFAULT - } else if (isCollection) { - return this.priceDict.COLLECTION[collection] || this.priceDict.COLLECTION_DEFAULT - } else { - return this.priceDict.SUBREDDIT[sr] || this.priceDict.SUBREDDIT_DEFAULT + var metroKey = metro + country; + prices.push(this.priceDict.METRO[metro] || this.priceDict.METRO_DEFAULT); + } else if (isGeotarget) { + prices.push(this.priceDict.COUNTRY[country] || this.priceDict.COUNTRY_DEFAULT); } + + if (isFrontpage) { + prices.push(this.priceDict.COLLECTION_DEFAULT); + } else if (isCollection) { + prices.push(this.priceDict.COLLECTION[collectionVal] || this.priceDict.COLLECTION_DEFAULT); + } else { + prices.push(this.priceDict.SUBREDDIT[sr] || this.priceDict.SUBREDDIT_DEFAULT); + } + + return _.max(prices); }, get_targeting: function($form) {