diff --git a/adserver/auth/admin.py b/adserver/auth/admin.py index 1d37566e..928742e0 100644 --- a/adserver/auth/admin.py +++ b/adserver/auth/admin.py @@ -17,7 +17,14 @@ class UserAdmin(SimpleHistoryAdmin): (None, {"fields": ("email", "name", "password")}), ( _("Ad server details"), - {"fields": ("advertisers", "publishers", "notify_on_completed_flights")}, + { + "fields": ( + "advertisers", + "publishers", + "flight_notifications", + "notify_on_completed_flights", # DEPRECATED + ) + }, ), ( _("Permissions"), diff --git a/adserver/auth/migrations/0007_rename_flight_notifications.py b/adserver/auth/migrations/0007_rename_flight_notifications.py new file mode 100644 index 00000000..9b0fc2ac --- /dev/null +++ b/adserver/auth/migrations/0007_rename_flight_notifications.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.7 on 2024-08-16 22:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("adserver_auth", "0006_simple_history_upgrade"), + ] + + operations = [ + migrations.AddField( + model_name="historicaluser", + name="flight_notifications", + field=models.BooleanField( + default=True, help_text="Receive email notification about ad flights" + ), + ), + migrations.AddField( + model_name="user", + name="flight_notifications", + field=models.BooleanField( + default=True, help_text="Receive email notification about ad flights" + ), + ), + ] diff --git a/adserver/auth/migrations/0008_data_flight_notifications.py b/adserver/auth/migrations/0008_data_flight_notifications.py new file mode 100644 index 00000000..77d8fe8e --- /dev/null +++ b/adserver/auth/migrations/0008_data_flight_notifications.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.7 on 2024-08-16 22:01 + +from django.db import migrations + + +def forwards(apps, schema_editor): + """Update flight notifications field.""" + User = apps.get_model("adserver_auth", "User") + + for user in User.objects.all(): + user.flight_notifications = user.notify_on_completed_flights + user.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("adserver_auth", "0007_rename_flight_notifications"), + ] + + operations = [ + migrations.RunPython(forwards, reverse_code=migrations.RunPython.noop) + ] diff --git a/adserver/auth/models.py b/adserver/auth/models.py index 0ad83670..9d2d7889 100644 --- a/adserver/auth/models.py +++ b/adserver/auth/models.py @@ -83,6 +83,11 @@ class User(AbstractBaseUser, PermissionsMixin): publishers = models.ManyToManyField(Publisher, blank=True) # Notifications + flight_notifications = models.BooleanField( + default=True, + help_text=_("Receive email notification about ad flights"), + ) + # DEPRECATED and replaced by `flight_notifications` notify_on_completed_flights = models.BooleanField( default=True, help_text=_( diff --git a/adserver/forms.py b/adserver/forms.py index 877ec85a..bf7ee1af 100644 --- a/adserver/forms.py +++ b/adserver/forms.py @@ -1357,8 +1357,8 @@ def save(self, commit=True): user = super().save(commit) user.invite_user() - # Track who added this user - update_change_reason(user, "Invited via authorized users view") + # Track who added this user + update_change_reason(user, "Invited via authorized users view") # You will need to add the user to the publisher/advertiser in the view return user @@ -1387,7 +1387,7 @@ def __init__(self, *args, **kwargs): ), Fieldset( _("Notification settings"), - "notify_on_completed_flights", + "flight_notifications", css_class="my-3", ), Submit("submit", _("Update account")), @@ -1395,7 +1395,7 @@ def __init__(self, *args, **kwargs): class Meta: model = get_user_model() - fields = ("name", "notify_on_completed_flights") + fields = ("name", "flight_notifications") class SupportForm(forms.Form): diff --git a/adserver/tasks.py b/adserver/tasks.py index d1ca98b4..878cfed0 100644 --- a/adserver/tasks.py +++ b/adserver/tasks.py @@ -763,6 +763,61 @@ def notify_on_ad_image_change(advertisement_id): ) +@app.task() +def notify_of_first_flight_launched(): + """Notify when an advertiser's first ever flight launches.""" + start_date = get_ad_day().date() - datetime.timedelta(days=1) + site = get_current_site(request=None) + + # Get advertisers who launched today and + # exclude advertisers with flights launched before today + advertisers_launched_today = Flight.objects.filter( + live=True, + start_date=start_date, + ).values("campaign__advertiser") + advertisers_launched_before_today = Flight.objects.filter( + start_date__lt=start_date, + ).values("campaign__advertiser") + + for advertiser in Advertiser.objects.filter( + pk__in=advertisers_launched_today + ).exclude(pk__in=advertisers_launched_before_today): + log.debug( + "Advertiser with first flights launched today. advertiser=%s", advertiser + ) + + flights = Flight.objects.filter( + live=True, + start_date=start_date, + campaign__advertiser=advertiser, + ).select_related() + + if settings.FRONT_ENABLED: + to_addresses = [ + u.email for u in advertiser.user_set.all() if u.flight_notifications + ] + + context = { + "site": site, + "flights": flights, + "advertiser": advertiser, + } + + with mail.get_connection( + settings.FRONT_BACKEND, + sender_name=f"{site.name} Flight Tracker", + ) as connection: + message = mail.EmailMessage( + _("Advertising launched - %(name)s") % {"name": site.name}, + render_to_string("adserver/email/flights-launched.html", context), + from_email=settings.DEFAULT_FROM_EMAIL, # Front doesn't use this + to=to_addresses, + connection=connection, + ) + message.draft = True # Only create a draft for now + message.send() + + @app.task() def notify_of_autorenewing_flights(days_before=7): """Send a note to flights set to renew automatically.""" @@ -781,9 +836,7 @@ def notify_of_autorenewing_flights(days_before=7): site = get_current_site(request=None) to_addresses = [ - u.email - for u in advertiser.user_set.all() - if u.notify_on_completed_flights + u.email for u in advertiser.user_set.all() if u.flight_notifications ] context = { @@ -932,9 +985,7 @@ def notify_of_completed_flights(): advertiser = Advertiser.objects.get(slug=advertiser_slug) to_addresses = [ - u.email - for u in advertiser.user_set.all() - if u.notify_on_completed_flights + u.email for u in advertiser.user_set.all() if u.flight_notifications ] if not to_addresses: diff --git a/adserver/templates/adserver/email/advertiser-base.html b/adserver/templates/adserver/email/advertiser-base.html index 56a73f8f..85050b1b 100644 --- a/adserver/templates/adserver/email/advertiser-base.html +++ b/adserver/templates/adserver/email/advertiser-base.html @@ -9,6 +9,6 @@ {% url "account" as notification_settings_url %}

{% blocktrans with site_name=site.name %}You are receiving this email because you run advertising with {{ site_name }}.{% endblocktrans %} - {% blocktrans with site_domain=site.domain %}Adjust your notification settings{% endblocktrans %} in our dashboard. + {% blocktrans with site_domain=site.domain %}Adjust your notification settings{% endblocktrans %} in our dashboard.

{% endblock body %} diff --git a/adserver/templates/adserver/email/flights-launched.html b/adserver/templates/adserver/email/flights-launched.html new file mode 100644 index 00000000..887b9a44 --- /dev/null +++ b/adserver/templates/adserver/email/flights-launched.html @@ -0,0 +1,30 @@ +{% extends 'adserver/email/advertiser-base.html' %} +{% load i18n %} + + +{% block content %} +

{% blocktrans with advertiser_name=advertiser.name %}{{ advertiser_name }} team,{% endblocktrans %}

+ +

{% blocktrans with site_name=site.name %}Congrats on launching your first ad flight with {{ site_name }}!{% endblocktrans %}

+ +{% spaceless %} + +

{% blocktrans with total_flights=flights|length pluralized_flights=flights|length|pluralize %} + You have {{ total_flights }} flight{{ pluralized_flights }} that launched today. + Below are links to your flight{{ pluralized_flights }} and performance reports in our ad dashboard: +{% endblocktrans %}

+ + +

{% blocktrans %}Thanks for advertising with us and don't hesitate to let us know if there's anything we can do to help make advertising with us a success for you.{% endblocktrans %}

+ +{% endspaceless %} + +{% endblock content %} diff --git a/adserver/tests/test_tasks.py b/adserver/tests/test_tasks.py index 26d9d0b2..7b5b1ba5 100644 --- a/adserver/tests/test_tasks.py +++ b/adserver/tests/test_tasks.py @@ -33,6 +33,7 @@ from ..tasks import disable_inactive_publishers from ..tasks import notify_of_autorenewing_flights from ..tasks import notify_of_completed_flights +from ..tasks import notify_of_first_flight_launched from ..tasks import notify_of_publisher_changes from ..tasks import remove_old_client_ids from ..tasks import remove_old_report_data @@ -138,6 +139,31 @@ def test_calculate_ad_ctrs(self): self.assertAlmostEqual(self.ad1.sampled_ctr, 100 * (1 / 10)) self.assertAlmostEqual(self.ad2.sampled_ctr, 100 * (2 / 7)) + @override_settings( + # Use the memory email backend instead of front for testing + FRONT_BACKEND="django.core.mail.backends.locmem.EmailBackend", + FRONT_ENABLED=True, + ) + def test_notify_of_flights_launched(self): + # Ensure there's a recipient for a wrapup email + self.staff_user.advertisers.add(self.advertiser) + + notify_of_first_flight_launched() + + # Shouldn't be any flight launched messages + self.assertEqual(len(mail.outbox), 0) + + self.flight.start_date = get_ad_day().date() - datetime.timedelta(days=1) + self.flight.save() + + notify_of_first_flight_launched() + + # Should be one email for the flight that launched 'yesterday' now + self.assertEqual(len(mail.outbox), 1) + self.assertTrue( + mail.outbox[0].subject.startswith("Advertising launched") + ) + @override_settings( # Use the memory email backend instead of front for testing FRONT_BACKEND="django.core.mail.backends.locmem.EmailBackend",