Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flight hard stop #772

Merged
merged 4 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions adserver/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class Meta:
"campaign",
"start_date",
"end_date",
"hard_stop_date",
"live",
"priority_multiplier",
"pacing_interval",
Expand Down
47 changes: 47 additions & 0 deletions adserver/migrations/0085_flight_hard_stop_date.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 3.2.20 on 2023-07-18 15:55
import datetime

from django.db import migrations
from django.db import models

import adserver.models


class Migration(migrations.Migration):

dependencies = [
('adserver', '0084_publisher_traffic_shaping'),
]

operations = [
migrations.AddField(
model_name='flight',
name='hard_stop_date',
field=models.DateField(blank=True, default=None, help_text='The flight will be stopped on this date even if not completely fulfilled', null=True, verbose_name='Hard Stop Date'),
),
migrations.AddField(
model_name='historicalflight',
name='hard_stop_date',
field=models.DateField(blank=True, default=None, help_text='The flight will be stopped on this date even if not completely fulfilled', null=True, verbose_name='Hard Stop Date'),
),
migrations.AlterField(
model_name='flight',
name='end_date',
field=models.DateField(default=adserver.models.default_flight_end_date, help_text='The target end date for the flight (it may go after this date)', verbose_name='End Date'),
),
migrations.AlterField(
model_name='flight',
name='start_date',
field=models.DateField(db_index=True, default=datetime.date.today, help_text='This flight will not be shown before this date', verbose_name='Start Date'),
),
migrations.AlterField(
model_name='historicalflight',
name='end_date',
field=models.DateField(default=adserver.models.default_flight_end_date, help_text='The target end date for the flight (it may go after this date)', verbose_name='End Date'),
),
migrations.AlterField(
model_name='historicalflight',
name='start_date',
field=models.DateField(db_index=True, default=datetime.date.today, help_text='This flight will not be shown before this date', verbose_name='Start Date'),
),
]
13 changes: 11 additions & 2 deletions adserver/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -720,12 +720,21 @@ class Flight(TimeStampedModel, IndestructibleModel):
_("Start Date"),
default=datetime.date.today,
db_index=True,
help_text=_("This ad will not be shown before this date"),
help_text=_("This flight will not be shown before this date"),
)
end_date = models.DateField(
_("End Date"),
default=default_flight_end_date,
help_text=_("The target end date for the ad (it may go after this date)"),
help_text=_("The target end date for the flight (it may go after this date)"),
davidfischer marked this conversation as resolved.
Show resolved Hide resolved
)
hard_stop_date = models.DateField(
_("Hard Stop Date"),
default=None,
blank=True,
null=True,
help_text=_(
"The flight will be stopped on this date even if not completely fulfilled"
),
)
live = models.BooleanField(_("Live"), default=False)
priority_multiplier = models.IntegerField(
Expand Down
42 changes: 42 additions & 0 deletions adserver/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
from django_slack import slack_message
from simple_history.utils import update_change_reason

from .constants import FLIGHT_STATE_CURRENT
from .constants import FLIGHT_STATE_UPCOMING
Expand Down Expand Up @@ -1024,6 +1025,47 @@ def update_flight_traffic_fill():
log.info("Completed updating flight traffic fill")


@app.task()
def daily_flight_hard_stop():
"""
Set flight with a hard stop date to completed.

This works by setting the sold amount equal to the current fulfilled amount
and sending a slack notification about the remaining credit.

This should be called before `notify_of_completed_flights()`
so that the regular wrapup emails can be sent to advertisers.
"""
today = get_ad_day().date()

for flight in Flight.objects.filter(live=True, hard_stop_date__lte=today):
if flight.clicks_remaining() <= 0 and flight.views_remaining() <= 0:
continue
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could probably use a comment. Are we skipping this because it finished properly?

Suggested change
if flight.clicks_remaining() <= 0 and flight.views_remaining() <= 0:
continue
# Don't execute for flights that properly finished
if flight.clicks_remaining() <= 0 and flight.views_remaining() <= 0:
continue


flight.sold_clicks = flight.total_clicks
flight.sold_impressions = flight.total_views
davidfischer marked this conversation as resolved.
Show resolved Hide resolved
flight.save()

value_remaining = round(flight.value_remaining(), 2)
davidfischer marked this conversation as resolved.
Show resolved Hide resolved

log.info("Hard stopped flight %s", flight)

# Store the change reason in the history
update_change_reason(
flight, f"Hard stopped with ${value_remaining} value remaining."
)

flight_url = generate_absolute_url(flight.get_absolute_url())

# Send an internal notification about this flight being hard stopped.
slack_message(
"adserver/slack/generic-message.slack",
{
"text": f"Flight {flight.name} was hard stopped. There was ${value_remaining} value remaining. {flight_url}"
},
)


@app.task()
def run_publisher_importers():
"""
Expand Down
4 changes: 4 additions & 0 deletions adserver/templates/adserver/includes/flight-metadata.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
<dt title="{% trans 'Note: your campaign may run beyond this date due to availability.' %}" data-toggle="tooltip" data-placement="left">{% trans 'Estimated end date' %}</dt>
<dd>{{ flight.end_date }}</dd>
{% endif %}
{% if flight.hard_stop_date %}
<dt title="{% trans 'The flight will be stopped on this date even if not completely fulfilled. The balance will be credited.' %}" data-toggle="tooltip" data-placement="left">{% trans 'Hard stop date' %} <span class="fa fa-info-circle fa-fw mr-2 text-muted" aria-hidden="true"></span></dt>
<dd>{{ flight.hard_stop_date }}</dd>
{% endif %}

<dt title="{% trans 'Determines which ad is chosen when a flight has multiple ads.' %}" data-toggle="tooltip" data-placement="left">{% trans 'Ad selection' %} <span class="fa fa-info-circle fa-fw mr-2 text-muted" aria-hidden="true"></span></dt>
<dd>
Expand Down
19 changes: 19 additions & 0 deletions adserver/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from ..models import UpliftImpression
from ..tasks import calculate_ad_ctrs
from ..tasks import calculate_publisher_ctrs
from ..tasks import daily_flight_hard_stop
from ..tasks import daily_update_advertisers
from ..tasks import daily_update_geos
from ..tasks import daily_update_impressions
Expand Down Expand Up @@ -290,6 +291,24 @@ def test_disable_inactive_publishers(self):
self.assertEqual(len(messages), 1)
self.assertEqual(len(mail.outbox), 1)

def test_flight_hard_stop(self):
daily_flight_hard_stop()
self.flight.refresh_from_db()

self.assertTrue(self.flight.clicks_remaining() > 0)

# Set flight to hard stop today
self.flight.start_date = timezone.now() - datetime.timedelta(days=30)
self.flight.end_date = timezone.now()
self.flight.hard_stop_date = timezone.now()
self.flight.save()

# This should hard stop the flight
daily_flight_hard_stop()
self.flight.refresh_from_db()

self.assertEqual(self.flight.clicks_remaining(), 0)


class AggregationTaskTests(BaseAdModelsTestCase):
def setUp(self):
Expand Down
4 changes: 4 additions & 0 deletions config/settings/production.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@
"task": "adserver.tasks.run_publisher_importers",
"schedule": crontab(hour="1", minute="0"),
},
"every-day-flight-hard-stop": {
"task": "adserver.tasks.daily_flight_hard_stop",
"schedule": crontab(hour="23", minute="55"),
},
}

# Tasks which should only be run if the analyzer is installed
Expand Down