Ejemplo n.º 1
0
def is_overdelivered(campaign):
    if (campaign.cost_basis != promo.PROMOTE_COST_BASIS.fixed_cpm
            or campaign.is_house):
        return False

    billable_impressions = promote.get_billable_impressions(campaign)
    return billable_impressions >= campaign.impressions + ADZERK_IMPRESSION_BUMP
Ejemplo n.º 2
0
def is_overdelivered(campaign):
    if (campaign.cost_basis != promo.PROMOTE_COST_BASIS.fixed_cpm or
            campaign.is_house):
        return False

    billable_impressions = promote.get_billable_impressions(campaign)
    return billable_impressions >= campaign.impressions + ADZERK_IMPRESSION_BUMP
Ejemplo n.º 3
0
def finalize_completed_campaigns(daysago=1):
    # PromoCampaign.end_date is utc datetime with year, month, day only
    now = datetime.datetime.now(g.tz)
    date = now - datetime.timedelta(days=daysago)
    date = date.replace(hour=0, minute=0, second=0, microsecond=0)

    q = PromoCampaign._query(
        PromoCampaign.c.end_date == date,
        # exclude no transaction and freebies
        PromoCampaign.c.trans_id > 0,
        data=True)
    campaigns = list(q)

    # check that traffic is up to date
    earliest_campaign = min(campaigns, key=lambda camp: camp.start_date)
    start, end = promote.get_total_run(earliest_campaign)
    missing_traffic = get_missing_traffic(start.replace(tzinfo=None),
                                          date.replace(tzinfo=None))
    if missing_traffic:
        raise ValueError("Can't finalize campaigns finished on %s."
                         "Missing traffic from %s" % (date, missing_traffic))

    links = Link._byID([camp.link_id for link in links], data=True)

    for camp in campaigns:
        if hasattr(camp, 'refund_amount'):
            continue

        link = links[camp.link_id]
        billable_impressions = promote.get_billable_impressions(camp)
        billable_amount = promote.get_billable_amount(camp,
                                                      billable_impressions)

        if billable_amount >= camp.bid:
            text = ('%s completed with $%s billable (%s impressions @ $%s).' %
                    (camp, billable_amount, billable_impressions, camp.cpm))
            PromotionLog.add(link, text)
            refund_amount = 0.
        else:
            refund_amount = camp.bid - billable_amount
            user = Account._byID(link.author_id, data=True)
            try:
                success = authorize.refund_transaction(user, camp.trans_id,
                                                       camp._id, refund_amount)
            except authorize.AuthorizeNetException as e:
                text = ('%s $%s refund failed' % (camp, refund_amount))
                PromotionLog.add(link, text)
                g.log.debug(text + ' (response: %s)' % e)
                continue
            text = ('%s completed with $%s billable (%s impressions @ $%s).'
                    ' %s refunded.' %
                    (camp, billable_amount, billable_impressions, camp.cpm,
                     refund_amount))
            PromotionLog.add(link, text)

        camp.refund_amount = refund_amount
        camp._commit()
Ejemplo n.º 4
0
 def POST_refund_campaign(self, form, jquery, link, campaign):
     billable_impressions = promote.get_billable_impressions(campaign)
     billable_amount = promote.get_billable_amount(campaign, billable_impressions)
     refund_amount = campaign.bid - billable_amount
     if refund_amount > 0:
         promote.refund_campaign(link, campaign, billable_amount)
         form.set_html(".status", _("refund succeeded"))
     else:
         form.set_html(".status", _("refund not needed"))
def finalize_completed_campaigns(daysago=1):
    # PromoCampaign.end_date is utc datetime with year, month, day only
    now = datetime.datetime.now(g.tz)
    date = now - datetime.timedelta(days=daysago)
    date = date.replace(hour=0, minute=0, second=0, microsecond=0)

    q = PromoCampaign._query(PromoCampaign.c.end_date == date,
                             # exclude no transaction and freebies
                             PromoCampaign.c.trans_id > 0,
                             data=True)
    campaigns = list(q)

    # check that traffic is up to date
    earliest_campaign = min(campaigns, key=lambda camp: camp.start_date)
    start, end = promote.get_total_run(earliest_campaign)
    missing_traffic = get_missing_traffic(start.replace(tzinfo=None),
                                          date.replace(tzinfo=None))
    if missing_traffic:
        raise ValueError("Can't finalize campaigns finished on %s."
                         "Missing traffic from %s" % (date, missing_traffic))

    links = Link._byID([camp.link_id for link in links], data=True)

    for camp in campaigns:
        if hasattr(camp, 'refund_amount'):
            continue

        link = links[camp.link_id]
        billable_impressions = promote.get_billable_impressions(camp)
        billable_amount = promote.get_billable_amount(camp,
                                                      billable_impressions)

        if billable_amount >= camp.bid:
            text = ('%s completed with $%s billable (%s impressions @ $%s).'
                    % (camp, billable_amount, billable_impressions, camp.cpm))
            PromotionLog.add(link, text)
            refund_amount = 0.
        else:
            refund_amount = camp.bid - billable_amount
            user = Account._byID(link.author_id, data=True)
            try:
                success = authorize.refund_transaction(user, camp.trans_id,
                                                       camp._id, refund_amount)
            except authorize.AuthorizeNetException as e:
                text = ('%s $%s refund failed' % (camp, refund_amount))
                PromotionLog.add(link, text)
                g.log.debug(text + ' (response: %s)' % e)
                continue
            text = ('%s completed with $%s billable (%s impressions @ $%s).'
                    ' %s refunded.' % (camp, billable_amount,
                                       billable_impressions, camp.cpm,
                                       refund_amount))
            PromotionLog.add(link, text)

        camp.refund_amount = refund_amount
        camp._commit()
Ejemplo n.º 6
0
 def POST_refund_campaign(self, form, jquery, link, campaign):
     billable_impressions = promote.get_billable_impressions(campaign)
     billable_amount = promote.get_billable_amount(campaign,
                                                   billable_impressions)
     refund_amount = campaign.bid - billable_amount
     if refund_amount > 0:
         promote.refund_campaign(link, campaign, billable_amount)
         form.set_html('.status', _('refund succeeded'))
     else:
         form.set_html('.status', _('refund not needed'))
Ejemplo n.º 7
0
 def POST_refund_campaign(self, form, jquery, link, campaign):
     billable_impressions = promote.get_billable_impressions(campaign)
     billable_amount = promote.get_billable_amount(campaign,
                                                   billable_impressions)
     refund_amount = promote.get_refund_amount(campaign, billable_amount)
     if refund_amount > 0:
         promote.refund_campaign(link, campaign, billable_amount,
                                 billable_impressions)
         form.set_html('.status', _('refund succeeded'))
     else:
         form.set_html('.status', _('refund not needed'))
Ejemplo n.º 8
0
    def POST_refund_campaign(self, form, jquery, link, campaign):
        if not link or not campaign or link._id != campaign.link_id:
            return abort(404, "not found")

        billable_impressions = promote.get_billable_impressions(campaign)
        billable_amount = promote.get_billable_amount(campaign, billable_impressions)
        refund_amount = promote.get_refund_amount(campaign, billable_amount)
        if refund_amount > 0:
            promote.refund_campaign(link, campaign, billable_amount, billable_impressions)
            form.set_html(".status", _("refund succeeded"))
        else:
            form.set_html(".status", _("refund not needed"))
Ejemplo n.º 9
0
    def POST_refund_campaign(self, form, jquery, link, campaign):
        if not link or not campaign or link._id != campaign.link_id:
            return abort(404, 'not found')

        billable_impressions = promote.get_billable_impressions(campaign)
        billable_amount = promote.get_billable_amount(campaign,
                                                      billable_impressions)
        refund_amount = promote.get_refund_amount(campaign, billable_amount)
        if refund_amount > 0:
            promote.refund_campaign(link, campaign, billable_amount,
                                    billable_impressions)
            form.set_text('.status', _('refund succeeded'))
        else:
            form.set_text('.status', _('refund not needed'))
def is_overdelivered(campaign):
    if not hasattr(campaign, 'cpm') or not campaign.priority.cpm:
        return False

    billable_impressions = promote.get_billable_impressions(campaign)
    return billable_impressions >= campaign.impressions + ADZERK_IMPRESSION_BUMP
Ejemplo n.º 11
0
def update_flight(link, campaign, az_campaign):
    """Add/update a reddit campaign as an Adzerk Flight"""
    if getattr(campaign, 'adzerk_flight_id', None) is not None:
        az_flight = adzerk_api.Flight.get(campaign.adzerk_flight_id)
    else:
        az_flight = None

    campaign_overdelivered = is_overdelivered(campaign)
    delayed_start = campaign.start_date + datetime.timedelta(minutes=15)
    if delayed_start >= campaign.end_date:
        # start time must be before end time
        delayed_start = campaign.start_date

    d = {
        'StartDate':
        date_to_adzerk(delayed_start),
        'EndDate':
        date_to_adzerk(campaign.end_date),
        'OptionType':
        1,  # 1: CPM, 2: Remainder
        'IsUnlimited':
        False,
        'IsFullSpeed':
        False,
        'Keywords':
        '\n'.join(campaign.target.subreddit_names),
        'CampaignId':
        az_campaign.Id,
        'PriorityId':
        g.az_selfserve_priorities[campaign.priority_name],
        'IsDeleted':
        False,
        'IsActive': (promote.charged_or_not_needed(campaign)
                     and not (campaign._deleted or campaign_overdelivered)),
        'IsFreqCap':
        None,
    }

    is_cpm = hasattr(campaign, 'cpm') and campaign.priority.cpm
    if is_cpm:
        d.update({
            'Price': campaign.cpm / 100.,  # convert from cents to dollars
            'Impressions': campaign.impressions + ADZERK_IMPRESSION_BUMP,
            'GoalType': 1,  # 1: Impressions
            'RateType': 2,  # 2: CPM
        })
    else:
        d.update({
            'Price': campaign.bid,
            'Impressions': 100,
            'GoalType': 2,  # 2: Percentage
            'RateType': 1,  # 1: Flat
        })

    if campaign.mobile_os:
        deviceQueries = [
            '($device.os contains "%s")' % os for os in campaign.mobile_os
        ]

        if campaign.platform == "all":
            deviceQueries.append('($device.formFactor contains "desktop")')

        customTargeting = ' or '.join(deviceQueries)
        d.update({
            'CustomTargeting': customTargeting,
        })
    else:
        d.update({
            'CustomTargeting': '',
        })

    if campaign.platform != 'all':
        siteZones = []
        if campaign.platform == 'desktop':
            siteZones.append({
                'SiteId': g.az_selfserve_site_id,
                'IsExclude': False,
            })
        elif campaign.platform == 'mobile':
            siteZones.append({
                'SiteId': g.az_selfserve_mobile_web_site_id,
                'IsExclude': False,
            })

        if len(siteZones):
            d.update({'SiteZoneTargeting': siteZones})

    # special handling for location conversions between reddit and adzerk
    if campaign.location:
        campaign_country = campaign.location.country
        campaign_region = campaign.location.region
        if campaign.location.metro:
            campaign_metro = int(campaign.location.metro)
        else:
            campaign_metro = None

    if az_flight and az_flight.GeoTargeting:
        # special handling for geotargeting of existing flights
        # can't update geotargeting through the Flight endpoint, do it manually
        existing = az_flight.GeoTargeting[0]
        az_geotarget = adzerk_api.GeoTargeting._from_item(existing)

        if (campaign.location and (campaign_country != az_geotarget.CountryCode
                                   or campaign_region != az_geotarget.Region
                                   or campaign_metro != az_geotarget.MetroCode
                                   or az_geotarget.IsExclude)):
            # existing geotargeting doesn't match current location
            az_geotarget.CountryCode = campaign_country
            az_geotarget.Region = campaign_region
            az_geotarget.MetroCode = campaign_metro
            az_geotarget.IsExclude = False
            az_geotarget._send(az_flight.Id)
            log_text = 'updated geotargeting to %s' % campaign.location
            PromotionLog.add(link, log_text)
        elif not campaign.location:
            # flight should no longer be geotargeted
            az_geotarget._delete(az_flight.Id)
            log_text = 'deleted geotargeting'
            PromotionLog.add(link, log_text)

        # only allow one geotarget per flight
        for existing in az_flight.GeoTargeting[1:]:
            az_geotarget = adzerk_api.GeoTargeting._from_item(existing)
            az_geotarget._delete(az_flight.Id)

        # NOTE: need to unset GeoTargeting otherwise it will be added to the
        # flight again when we _send updates
        az_flight.GeoTargeting = None

    elif campaign.location:
        # flight endpoint works when a new flight is being created or an
        # existing one that didn't have geotargeting is being updated
        d.update({
            'GeoTargeting': [{
                'CountryCode': campaign_country,
                'Region': campaign_region,
                'MetroCode': campaign_metro,
                'IsExclude': False,
            }],
        })
    else:
        # no geotargeting, either a new flight is being created or an existing
        # flight is being updated that wasn't geotargeted
        d.update({
            'GeoTargeting': [],
        })

    if az_flight:
        changed = update_changed(az_flight, **d)
        change_strs = make_change_strings(changed)

        if campaign_overdelivered:
            billable = promote.get_billable_impressions(campaign)
            over_str = 'overdelivered %s/%s' % (billable, campaign.impressions)
            change_strs.append(over_str)

        if change_strs:
            log_text = 'updated %s: ' % az_flight + ', '.join(change_strs)
        else:
            log_text = None
    else:
        d.update({'Name': campaign._fullname})
        az_flight = adzerk_api.Flight.create(**d)
        campaign.adzerk_flight_id = az_flight.Id
        campaign._commit()
        log_text = 'created %s' % az_flight

    if log_text:
        PromotionLog.add(link, log_text)
        g.log.info(log_text)

    if campaign_overdelivered:
        campaign.adzerk_flight_overdelivered = True
        campaign._commit()

    return az_flight
def update_flight(link, campaign, az_campaign):
    """Add/update a reddit campaign as an Adzerk Flight"""
    if getattr(campaign, 'adzerk_flight_id', None) is not None:
        az_flight = adzerk_api.Flight.get(campaign.adzerk_flight_id)
    else:
        az_flight = None

    campaign_overdelivered = is_overdelivered(campaign)
    delayed_start = campaign.start_date + datetime.timedelta(minutes=15)
    if delayed_start >= campaign.end_date:
        # start time must be before end time
        delayed_start = campaign.start_date

    d = {
        'StartDate': date_to_adzerk(delayed_start),
        'EndDate': date_to_adzerk(campaign.end_date),
        'OptionType': 1, # 1: CPM, 2: Remainder
        'IsUnlimited': False,
        'IsFullSpeed': False,
        'Keywords': '\n'.join(campaign.target.subreddit_names),
        'CampaignId': az_campaign.Id,
        'PriorityId': g.az_selfserve_priorities[campaign.priority_name],
        'IsDeleted': False,
        'IsActive': (promote.charged_or_not_needed(campaign) and
                     not (campaign._deleted or campaign_overdelivered)),
        'IsFreqCap': None,
    }

    is_cpm = hasattr(campaign, 'cpm') and campaign.priority.cpm
    if is_cpm:
        d.update({
            'Price': campaign.cpm / 100.,   # convert from cents to dollars
            'Impressions': campaign.impressions + ADZERK_IMPRESSION_BUMP,
            'GoalType': 1, # 1: Impressions
            'RateType': 2, # 2: CPM
        })
    else:
        d.update({
            'Price': campaign.bid,
            'Impressions': 100,
            'GoalType': 2, # 2: Percentage
            'RateType': 1, # 1: Flat
        })

    if campaign.mobile_os:
        deviceQueries = ['($device.os contains "%s")' % os
                         for os in campaign.mobile_os]

        if campaign.platform == "all":
            deviceQueries.append('($device.formFactor contains "desktop")')
        
        customTargeting = ' or '.join(deviceQueries)
        d.update({
            'CustomTargeting': customTargeting,
        })
    else:
        d.update({
            'CustomTargeting': '',
        })

    if campaign.platform != 'all':
        siteZones = []
        if campaign.platform == 'desktop':
            siteZones.append({
                'SiteId': g.az_selfserve_site_id,
                'IsExclude': False,
            })
        elif campaign.platform == 'mobile':
            siteZones.append({
                'SiteId': g.az_selfserve_mobile_web_site_id,
                'IsExclude': False,
            })

        if len(siteZones):
            d.update({
                'SiteZoneTargeting': siteZones
            })

    # special handling for location conversions between reddit and adzerk
    if campaign.location:
        campaign_country = campaign.location.country
        campaign_region = campaign.location.region
        if campaign.location.metro:
            campaign_metro = int(campaign.location.metro)
        else:
            campaign_metro = None

    if az_flight and az_flight.GeoTargeting:
        # special handling for geotargeting of existing flights
        # can't update geotargeting through the Flight endpoint, do it manually
        existing = az_flight.GeoTargeting[0]
        az_geotarget = adzerk_api.GeoTargeting._from_item(existing)

        if (campaign.location and
            (campaign_country != az_geotarget.CountryCode or
             campaign_region != az_geotarget.Region or
             campaign_metro != az_geotarget.MetroCode or
             az_geotarget.IsExclude)):
            # existing geotargeting doesn't match current location
            az_geotarget.CountryCode = campaign_country
            az_geotarget.Region = campaign_region
            az_geotarget.MetroCode = campaign_metro
            az_geotarget.IsExclude = False
            az_geotarget._send(az_flight.Id)
            log_text = 'updated geotargeting to %s' % campaign.location
            PromotionLog.add(link, log_text)
        elif not campaign.location:
            # flight should no longer be geotargeted
            az_geotarget._delete(az_flight.Id)
            log_text = 'deleted geotargeting'
            PromotionLog.add(link, log_text)

        # only allow one geotarget per flight
        for existing in az_flight.GeoTargeting[1:]:
            az_geotarget = adzerk_api.GeoTargeting._from_item(existing)
            az_geotarget._delete(az_flight.Id)

        # NOTE: need to unset GeoTargeting otherwise it will be added to the
        # flight again when we _send updates
        az_flight.GeoTargeting = None

    elif campaign.location:
        # flight endpoint works when a new flight is being created or an
        # existing one that didn't have geotargeting is being updated
        d.update({
            'GeoTargeting': [{
                'CountryCode': campaign_country,
                'Region': campaign_region,
                'MetroCode': campaign_metro,
                'IsExclude': False,
            }],
        })
    else:
        # no geotargeting, either a new flight is being created or an existing
        # flight is being updated that wasn't geotargeted
        d.update({
            'GeoTargeting': [],
        })

    if az_flight:
        changed = update_changed(az_flight, **d)
        change_strs = make_change_strings(changed)

        if campaign_overdelivered:
            billable = promote.get_billable_impressions(campaign)
            over_str = 'overdelivered %s/%s' % (billable, campaign.impressions)
            change_strs.append(over_str)

        if change_strs:
            log_text = 'updated %s: ' % az_flight + ', '.join(change_strs)
        else:
            log_text = None
    else:
        d.update({'Name': campaign._fullname})
        az_flight = adzerk_api.Flight.create(**d)
        campaign.adzerk_flight_id = az_flight.Id
        campaign._commit()
        log_text = 'created %s' % az_flight

    if log_text:
        PromotionLog.add(link, log_text)
        g.log.info(log_text)

    if campaign_overdelivered:
        campaign.adzerk_flight_overdelivered = True
        campaign._commit()

    return az_flight
Ejemplo n.º 13
0
def is_overdelivered(campaign):
    if not hasattr(campaign, 'cpm') or not campaign.priority.cpm:
        return False

    billable_impressions = promote.get_billable_impressions(campaign)
    return billable_impressions >= campaign.impressions + ADZERK_IMPRESSION_BUMP
Ejemplo n.º 14
0
def update_flight(link, campaign, az_campaign):
    """Add/update a reddit campaign as an Adzerk Flight"""
    if getattr(campaign, 'external_flight_id', None) is not None:
        az_flight = adzerk_api.Flight.get(campaign.external_flight_id)
    else:
        az_flight = None

    # backwards compatability
    if campaign.platform == "mobile":
        campaign.platform = "mobile_web"
        campaign._commit()

    campaign_overdelivered = is_overdelivered(campaign)
    delayed_start = campaign.start_date + datetime.timedelta(minutes=15)
    if delayed_start >= campaign.end_date:
        # start time must be before end time
        delayed_start = campaign.start_date

    keywords = campaign.target.subreddit_names

    # Don't allow nsfw links to show up on the frontpage
    if link.over_18:
        keywords.append('!' + Frontpage.name)

    d = {
        'StartDate': date_to_adzerk(delayed_start),
        'EndDate': date_to_adzerk(campaign.end_date),
        'OptionType': 1, # 1: CPM, 2: Remainder
        'IsUnlimited': False,
        'IsFullSpeed': False,
        'Keywords': '\n'.join(keywords),
        'CampaignId': az_campaign.Id,
        'PriorityId': g.az_selfserve_priorities[campaign.priority_name],
        'IsDeleted': False,
        'IsActive': (not campaign.paused and
                     promote.charged_or_not_needed(campaign) and
                     not (campaign._deleted or campaign_overdelivered)),
    }

    if campaign.frequency_cap:
        d.update({'IsFreqCap': True,
                  'FreqCap': campaign.frequency_cap,
                  'FreqCapDuration': FREQUENCY_CAP_DURATION_HOURS,
                  'FreqCapType': FREQ_CAP_TYPE.hour})
    else:
        d['IsFreqCap'] = None

    if campaign.is_house:
        # house campaigns are flat rate (free) 100% impression goal
        d.update({
            'Price': 0, # free/0 revenue
            'Impressions': 100, # 100% impressions/rotates with other house campaigns.
            'GoalType': 2, # percentage
            'RateType': 1, # flat
        })
    else:
        # price is bid if the rate type is cpm/cpc
        d.update({
            'Price': campaign.bid_pennies / 100., # convert cents to dollars
            'GoalType': GOAL_TYPE_BY_COST_BASIS[campaign.cost_basis], # goal should be based on billing type
            'RateType': RATE_TYPE_BY_COST_BASIS[campaign.cost_basis], 
        })

        if campaign.cost_basis == promo.PROMOTE_COST_BASIS.fixed_cpm:
            d['Impressions'] = campaign.impressions + ADZERK_IMPRESSION_BUMP
        else:
            total_budget_dollars = campaign.total_budget_pennies / 100.
            # budget caps must be whole dollar amounts. round things up to prevent
            # things from underdelivering.
            d.update({
                'CapType': 4,
                'DailyCapAmount': int(math.ceil(total_budget_dollars / campaign.ndays)),
                'LifetimeCapAmount': int(math.ceil(total_budget_dollars)),
            })

    # Zerkel queries here
    if campaign.mobile_os:
        queries_list = []

        if 'iOS' in campaign.mobile_os:
            ios_targets = get_mobile_targeting_query(os_str='iOS',
                                                     lookup_str='modelName',
                                                     devices=campaign.ios_devices,
                                                     versions=campaign.ios_version_range)
            queries_list.append(ios_targets)

        if 'Android' in campaign.mobile_os:
            android_targets = get_mobile_targeting_query(os_str='Android',
                                                         lookup_str='formFactor',
                                                         devices=campaign.android_devices,
                                                         versions=campaign.android_version_range)
            queries_list.append(android_targets)

        if campaign.platform == 'all':
            queries_list.append('($device.formFactor CONTAINS "desktop")')

        mobile_targeting_query = ' OR '.join(queries_list)
        
        d.update({
            'CustomTargeting': mobile_targeting_query,
        })
    else:
        d.update({
            'CustomTargeting': '',
        })

    if campaign.platform != 'all':
        d.update({
            'SiteZoneTargeting': [{
                'SiteId': g.az_selfserve_site_ids[campaign.platform],
                'IsExclude': False,
            }],
        })

    # special handling for location conversions between reddit and adzerk
    if campaign.location:
        campaign_country = campaign.location.country
        campaign_region = campaign.location.region
        if campaign.location.metro:
            campaign_metro = int(campaign.location.metro)
        else:
            campaign_metro = None

    if az_flight and az_flight.GeoTargeting:
        # special handling for geotargeting of existing flights
        # can't update geotargeting through the Flight endpoint, do it manually
        existing = az_flight.GeoTargeting[0]
        az_geotarget = adzerk_api.GeoTargeting._from_item(existing)

        if (campaign.location and
            (campaign_country != az_geotarget.CountryCode or
             campaign_region != az_geotarget.Region or
             campaign_metro != az_geotarget.MetroCode or
             az_geotarget.IsExclude)):
            # existing geotargeting doesn't match current location
            az_geotarget.CountryCode = campaign_country
            az_geotarget.Region = campaign_region
            az_geotarget.MetroCode = campaign_metro
            az_geotarget.IsExclude = False
            az_geotarget._send(az_flight.Id)
            log_text = 'updated geotargeting to %s' % campaign.location
            PromotionLog.add(link, log_text)
        elif not campaign.location:
            # flight should no longer be geotargeted
            az_geotarget._delete(az_flight.Id)
            log_text = 'deleted geotargeting'
            PromotionLog.add(link, log_text)

        # only allow one geotarget per flight
        for existing in az_flight.GeoTargeting[1:]:
            az_geotarget = adzerk_api.GeoTargeting._from_item(existing)
            az_geotarget._delete(az_flight.Id)

        # NOTE: need to unset GeoTargeting otherwise it will be added to the
        # flight again when we _send updates
        az_flight.GeoTargeting = None

    elif campaign.location:
        # flight endpoint works when a new flight is being created or an
        # existing one that didn't have geotargeting is being updated
        d.update({
            'GeoTargeting': [{
                'CountryCode': campaign_country,
                'Region': campaign_region,
                'MetroCode': campaign_metro,
                'IsExclude': False,
            }],
        })
    else:
        # no geotargeting, either a new flight is being created or an existing
        # flight is being updated that wasn't geotargeted
        d.update({
            'GeoTargeting': [],
        })

    if az_flight:
        changed = update_changed(az_flight, **d)
        change_strs = make_change_strings(changed)

        if campaign_overdelivered:
            billable = promote.get_billable_impressions(campaign)
            over_str = 'overdelivered %s/%s' % (billable, campaign.impressions)
            change_strs.append(over_str)

        if change_strs:
            log_text = 'updated %s: ' % az_flight + ', '.join(change_strs)
        else:
            log_text = None
    else:
        d.update({'Name': campaign._fullname})
        az_flight = adzerk_api.Flight.create(**d)
        campaign.external_flight_id = az_flight.Id
        campaign._commit()

        PromoCampaignByFlightIdCache.add(campaign)
        log_text = 'created %s' % az_flight

    if log_text:
        PromotionLog.add(link, log_text)
        g.log.info(log_text)

    if campaign_overdelivered:
        campaign.external_flight_overdelivered = True
        campaign._commit()

    return az_flight
def is_overdelivered(campaign):
    if not hasattr(campaign, 'cpm'):
        return False

    billable_impressions = promote.get_billable_impressions(campaign)
    return billable_impressions >= campaign.impressions
Ejemplo n.º 16
0
def update_flight(link, campaign, triggered_by=None):
    """Add/update a reddit campaign as an Adzerk Flight"""
    if getattr(campaign, 'external_flight_id', None) is not None:
        az_flight = adzerk_api.Flight.get(campaign.external_flight_id)
    else:
        az_flight = None

    # backwards compatability
    if campaign.platform == "mobile":
        campaign.platform = "mobile_web"
        campaign._commit()

    campaign_overdelivered = is_overdelivered(campaign)
    delayed_start = campaign.start_date + datetime.timedelta(minutes=15)
    if delayed_start >= campaign.end_date:
        # start time must be before end time
        delayed_start = campaign.start_date

    keywords = set(campaign.target.subreddit_names)

    targets_frontpage = Frontpage.name in keywords

    if targets_frontpage:
        keywords.remove(Frontpage.name)

    # Don't allow nsfw links to show up on the frontpage
    if link.over_18:
        keywords.add("!s.frontpage")
    elif targets_frontpage:
        keywords.add("s.frontpage")

    campaign_needs_approval = (not campaign.is_approved and
                               promote.campaign_needs_review(campaign, link))
    campaign_is_paused = campaign.paused
    campaign_needs_payment = not promote.charged_or_not_needed(campaign)
    campaign_is_terminated = campaign.is_terminated

    is_active = flight_is_active(
        needs_approval=campaign_needs_approval,
        is_paused=campaign_is_paused,
        needs_payment=campaign_needs_payment,
        is_terminated=campaign_is_terminated,
        is_deleted=campaign._deleted,
        is_overdelivered=campaign_overdelivered,
    )

    d = {
        'StartDate': date_to_adzerk(delayed_start),
        'EndDate': date_to_adzerk(campaign.end_date),
        'OptionType': 1, # 1: CPM, 2: Remainder
        'IsUnlimited': False,
        'IsFullSpeed': False,
        'Keywords': '\n'.join(list(keywords)),
        'CampaignId': link.external_campaign_id,
        'PriorityId': g.az_selfserve_priorities[campaign.priority_name],
        'IsDeleted': False,
        'IsActive': is_active,
    }

    if campaign.frequency_cap:
        d.update({'IsFreqCap': True,
                  'FreqCap': campaign.frequency_cap,
                  'FreqCapDuration': FREQUENCY_CAP_DURATION_HOURS,
                  'FreqCapType': FREQ_CAP_TYPE.hour})
    else:
        d['IsFreqCap'] = None

    if campaign.is_house:
        # house campaigns are flat rate (free) 100% impression goal
        d.update({
            'Price': 0, # free/0 revenue
            'Impressions': 100, # 100% impressions/rotates with other house campaigns.
            'GoalType': 2, # percentage
            'RateType': 1, # flat
        })
    else:
        # price is bid if the rate type is cpm/cpc
        d.update({
            'Price': campaign.bid_pennies / 100., # convert cents to dollars
            'GoalType': GOAL_TYPE_BY_COST_BASIS[campaign.cost_basis], # goal should be based on billing type
            'RateType': RATE_TYPE_BY_COST_BASIS[campaign.cost_basis], 
        })

        stop_showing_onclick = campaign.cost_basis == promo.PROMOTE_COST_BASIS.cpc

        d.update({
            "BehavioralTargeting": {
                "onClick": {
                    "stopShowingAdsFromFlight": stop_showing_onclick,
                },
            },
        })

        if campaign.cost_basis == promo.PROMOTE_COST_BASIS.fixed_cpm:
            d['Impressions'] = campaign.impressions + ADZERK_IMPRESSION_BUMP
        else:
            total_budget_dollars = campaign.total_budget_pennies / 100.
            daily_cap_dollars = total_budget_dollars / max(campaign.ndays, 1)
            ndays = campaign.ndays
            padding = 1 + (1. / (ndays + 1))
            lifetime_cap = int(math.ceil(total_budget_dollars))
            # budget caps must be whole dollar amounts. round things up to prevent
            # things from underdelivering.
            d.update({
                'CapType': 4,
                'LifetimeCapAmount': lifetime_cap,
            })

            if not campaign.use_daily_budget_cap:
                # set the daily cap to the same as the lifetime cap since 
                # `update_changed` doesn't handle unsetting attributes.
                d['DailyCapAmount'] = lifetime_cap
            else:
                d['DailyCapAmount'] = int(math.ceil(daily_cap_dollars * padding))

    # Zerkel queries here
    if campaign.mobile_os:
        queries_list = []

        if 'iOS' in campaign.mobile_os:
            ios_targets = get_mobile_targeting_query(os_str='iOS',
                                                     lookup_str='modelName',
                                                     devices=campaign.ios_devices,
                                                     versions=campaign.ios_version_range)
            queries_list.append(ios_targets)

        if 'Android' in campaign.mobile_os:
            android_targets = get_mobile_targeting_query(os_str='Android',
                                                         lookup_str='formFactor',
                                                         devices=campaign.android_devices,
                                                         versions=campaign.android_version_range)
            queries_list.append(android_targets)

        if campaign.platform == 'all':
            queries_list.append('($device.formFactor CONTAINS "desktop")')

        mobile_targeting_query = ' OR '.join(queries_list)
        
        d.update({
            'CustomTargeting': mobile_targeting_query,
        })
    else:
        d.update({
            'CustomTargeting': '',
        })

    if campaign.platform != 'all':
        site_targeting = [{
            'SiteId': g.az_selfserve_site_ids[campaign.platform],
            'IsExclude': False,
            'ZoneId': None,
        }]

        if az_flight:
            site_targeting[0]['FlightId'] = az_flight.Id

        # Check equality more specifically to reduce spam in the PromotionLog.
        update_site_targeting = True
        if az_flight and az_flight.SiteZoneTargeting:
            # Loop through the existing site targeting and remove the `Id` param.
            old_site_targeting = map(
                lambda (index, value): {
                    key: value[key]
                    for key in value.keys() if key != "Id"},
                enumerate(az_flight.SiteZoneTargeting),
            )
            update_site_targeting = old_site_targeting != site_targeting

        if update_site_targeting:
            d.update({
                'SiteZoneTargeting': site_targeting,
            })
    elif az_flight and az_flight.SiteZoneTargeting:
        d.update({
            'SiteZoneTargeting': [],
        })

    # special handling for location conversions between reddit and adzerk
    if campaign.location:
        campaign_country = campaign.location.country
        campaign_region = campaign.location.region
        if campaign.location.metro:
            campaign_metro = int(campaign.location.metro)
        else:
            campaign_metro = None

    if az_flight and az_flight.GeoTargeting:
        # special handling for geotargeting of existing flights
        # can't update geotargeting through the Flight endpoint, do it manually
        existing = az_flight.GeoTargeting[0]
        az_geotarget = adzerk_api.GeoTargeting._from_item(existing)

        if (campaign.location and
            (campaign_country != az_geotarget.CountryCode or
             campaign_region != az_geotarget.Region or
             campaign_metro != az_geotarget.MetroCode or
             az_geotarget.IsExclude)):
            # existing geotargeting doesn't match current location
            az_geotarget.CountryCode = campaign_country
            az_geotarget.Region = campaign_region
            az_geotarget.MetroCode = campaign_metro
            az_geotarget.IsExclude = False
            az_geotarget._send(az_flight.Id)
            log_text = 'updated geotargeting to %s' % campaign.location
            PromotionLog.add(link, log_text)
        elif not campaign.location:
            # flight should no longer be geotargeted
            az_geotarget._delete(az_flight.Id)
            log_text = 'deleted geotargeting'
            PromotionLog.add(link, log_text)

        # only allow one geotarget per flight
        for existing in az_flight.GeoTargeting[1:]:
            az_geotarget = adzerk_api.GeoTargeting._from_item(existing)
            az_geotarget._delete(az_flight.Id)

        # NOTE: need to unset GeoTargeting otherwise it will be added to the
        # flight again when we _send updates
        az_flight.GeoTargeting = None

    elif campaign.location:
        # flight endpoint works when a new flight is being created or an
        # existing one that didn't have geotargeting is being updated
        d.update({
            'GeoTargeting': [{
                'CountryCode': campaign_country,
                'Region': campaign_region,
                'MetroCode': campaign_metro,
                'IsExclude': False,
            }],
        })
    else:
        # no geotargeting, either a new flight is being created or an existing
        # flight is being updated that wasn't geotargeted
        d.update({
            'GeoTargeting': [],
        })

    request_error = None
    additional_data = dict(
        requires_approval=campaign_needs_approval,
        requires_payment=campaign_needs_payment,
        is_overdelivered=campaign_overdelivered,
        is_paused=campaign_is_paused,
        is_terminated=campaign_is_terminated,
    )

    if az_flight:
        try:
            changed = update_changed(az_flight, **d)
        except adzerk_api.AdzerkError as e:
            request_error = e
        finally:
            g.ad_events.adzerk_api_request(
                request_type="update_flight",
                thing=campaign,
                request_body=d,
                triggered_by=triggered_by,
                additional_data=additional_data,
                request_error=request_error,
            )

            # re-raise after sending event to requeue item.
            if request_error:
                raise request_error

        change_strs = make_change_strings(changed)

        if campaign_overdelivered:
            billable = promote.get_billable_impressions(campaign)
            over_str = 'overdelivered %s/%s' % (billable, campaign.impressions)
            change_strs.append(over_str)

        if change_strs:
            log_text = 'updated %s: ' % az_flight + ', '.join(change_strs)
        else:
            log_text = None
    else:
        d.update({'Name': campaign._fullname})

        try:
            az_flight = adzerk_api.Flight.create(**d)
        except adzerk_api.AdzerkError as e:
            request_error = e
        finally:
            g.ad_events.adzerk_api_request(
                request_type="create_flight",
                thing=campaign,
                request_body=d,
                triggered_by=triggered_by,
                additional_data=additional_data,
                request_error=request_error,
            )

            # re-raise after sending event to requeue item.
            if request_error:
                raise request_error

        campaign.external_flight_id = az_flight.Id
        campaign._commit()

        PromoCampaignByFlightIdCache.add(campaign)
        log_text = 'created %s' % az_flight

    if log_text:
        PromotionLog.add(link, log_text)
        g.log.info(log_text)

    if campaign_overdelivered:
        campaign.external_flight_overdelivered = True
        campaign._commit()

    return az_flight
Ejemplo n.º 17
0
def update_flight(link, campaign, az_campaign):
    """Add/update a reddit campaign as an Adzerk Flight"""
    if getattr(campaign, 'external_flight_id', None) is not None:
        az_flight = adzerk_api.Flight.get(campaign.external_flight_id)
    else:
        az_flight = None

    # backwards compatability
    if campaign.platform == "mobile":
        campaign.platform = "mobile_web"
        campaign._commit()

    campaign_overdelivered = is_overdelivered(campaign)
    delayed_start = campaign.start_date + datetime.timedelta(minutes=15)
    if delayed_start >= campaign.end_date:
        # start time must be before end time
        delayed_start = campaign.start_date

    keywords = campaign.target.subreddit_names

    # Don't allow nsfw links to show up on the frontpage
    if link.over_18:
        keywords.append('!' + Frontpage.name)

    d = {
        'StartDate':
        date_to_adzerk(delayed_start),
        'EndDate':
        date_to_adzerk(campaign.end_date),
        'OptionType':
        1,  # 1: CPM, 2: Remainder
        'IsUnlimited':
        False,
        'IsFullSpeed':
        False,
        'Keywords':
        '\n'.join(keywords),
        'CampaignId':
        az_campaign.Id,
        'PriorityId':
        g.az_selfserve_priorities[campaign.priority_name],
        'IsDeleted':
        False,
        'IsActive':
        (not campaign.paused and promote.charged_or_not_needed(campaign)
         and not (campaign._deleted or campaign_overdelivered)),
    }

    if campaign.frequency_cap:
        d.update({
            'IsFreqCap': True,
            'FreqCap': campaign.frequency_cap,
            'FreqCapDuration': FREQUENCY_CAP_DURATION_HOURS,
            'FreqCapType': FREQ_CAP_TYPE.hour
        })
    else:
        d['IsFreqCap'] = None

    if campaign.is_house:
        # house campaigns are flat rate (free) 100% impression goal
        d.update({
            'Price': 0,  # free/0 revenue
            'Impressions':
            100,  # 100% impressions/rotates with other house campaigns.
            'GoalType': 2,  # percentage
            'RateType': 1,  # flat
        })
    else:
        # price is bid if the rate type is cpm/cpc
        d.update({
            'Price': campaign.bid_pennies / 100.,  # convert cents to dollars
            'GoalType': GOAL_TYPE_BY_COST_BASIS[
                campaign.cost_basis],  # goal should be based on billing type
            'RateType': RATE_TYPE_BY_COST_BASIS[campaign.cost_basis],
        })

        if campaign.cost_basis == promo.PROMOTE_COST_BASIS.fixed_cpm:
            d['Impressions'] = campaign.impressions + ADZERK_IMPRESSION_BUMP
        else:
            total_budget_dollars = campaign.total_budget_pennies / 100.
            # budget caps must be whole dollar amounts. round things up to prevent
            # things from underdelivering.
            d.update({
                'CapType':
                4,
                'DailyCapAmount':
                int(math.ceil(total_budget_dollars / campaign.ndays)),
                'LifetimeCapAmount':
                int(math.ceil(total_budget_dollars)),
            })

    # Zerkel queries here
    if campaign.mobile_os:
        queries_list = []

        if 'iOS' in campaign.mobile_os:
            ios_targets = get_mobile_targeting_query(
                os_str='iOS',
                lookup_str='modelName',
                devices=campaign.ios_devices,
                versions=campaign.ios_version_range)
            queries_list.append(ios_targets)

        if 'Android' in campaign.mobile_os:
            android_targets = get_mobile_targeting_query(
                os_str='Android',
                lookup_str='formFactor',
                devices=campaign.android_devices,
                versions=campaign.android_version_range)
            queries_list.append(android_targets)

        if campaign.platform == 'all':
            queries_list.append('($device.formFactor CONTAINS "desktop")')

        mobile_targeting_query = ' OR '.join(queries_list)

        d.update({
            'CustomTargeting': mobile_targeting_query,
        })
    else:
        d.update({
            'CustomTargeting': '',
        })

    if campaign.platform != 'all':
        d.update({
            'SiteZoneTargeting': [{
                'SiteId':
                g.az_selfserve_site_ids[campaign.platform],
                'IsExclude':
                False,
            }],
        })

    # special handling for location conversions between reddit and adzerk
    if campaign.location:
        campaign_country = campaign.location.country
        campaign_region = campaign.location.region
        if campaign.location.metro:
            campaign_metro = int(campaign.location.metro)
        else:
            campaign_metro = None

    if az_flight and az_flight.GeoTargeting:
        # special handling for geotargeting of existing flights
        # can't update geotargeting through the Flight endpoint, do it manually
        existing = az_flight.GeoTargeting[0]
        az_geotarget = adzerk_api.GeoTargeting._from_item(existing)

        if (campaign.location and (campaign_country != az_geotarget.CountryCode
                                   or campaign_region != az_geotarget.Region
                                   or campaign_metro != az_geotarget.MetroCode
                                   or az_geotarget.IsExclude)):
            # existing geotargeting doesn't match current location
            az_geotarget.CountryCode = campaign_country
            az_geotarget.Region = campaign_region
            az_geotarget.MetroCode = campaign_metro
            az_geotarget.IsExclude = False
            az_geotarget._send(az_flight.Id)
            log_text = 'updated geotargeting to %s' % campaign.location
            PromotionLog.add(link, log_text)
        elif not campaign.location:
            # flight should no longer be geotargeted
            az_geotarget._delete(az_flight.Id)
            log_text = 'deleted geotargeting'
            PromotionLog.add(link, log_text)

        # only allow one geotarget per flight
        for existing in az_flight.GeoTargeting[1:]:
            az_geotarget = adzerk_api.GeoTargeting._from_item(existing)
            az_geotarget._delete(az_flight.Id)

        # NOTE: need to unset GeoTargeting otherwise it will be added to the
        # flight again when we _send updates
        az_flight.GeoTargeting = None

    elif campaign.location:
        # flight endpoint works when a new flight is being created or an
        # existing one that didn't have geotargeting is being updated
        d.update({
            'GeoTargeting': [{
                'CountryCode': campaign_country,
                'Region': campaign_region,
                'MetroCode': campaign_metro,
                'IsExclude': False,
            }],
        })
    else:
        # no geotargeting, either a new flight is being created or an existing
        # flight is being updated that wasn't geotargeted
        d.update({
            'GeoTargeting': [],
        })

    if az_flight:
        changed = update_changed(az_flight, **d)
        change_strs = make_change_strings(changed)

        if campaign_overdelivered:
            billable = promote.get_billable_impressions(campaign)
            over_str = 'overdelivered %s/%s' % (billable, campaign.impressions)
            change_strs.append(over_str)

        if change_strs:
            log_text = 'updated %s: ' % az_flight + ', '.join(change_strs)
        else:
            log_text = None
    else:
        d.update({'Name': campaign._fullname})
        az_flight = adzerk_api.Flight.create(**d)
        campaign.external_flight_id = az_flight.Id
        campaign._commit()

        PromoCampaignByFlightIdCache.add(campaign)
        log_text = 'created %s' % az_flight

    if log_text:
        PromotionLog.add(link, log_text)
        g.log.info(log_text)

    if campaign_overdelivered:
        campaign.external_flight_overdelivered = True
        campaign._commit()

    return az_flight