Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.9 on 2026-01-05 21:39

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('experiments', '0307_nimbusexperiment_next_steps_and_more'),
]

operations = [
migrations.AddField(
model_name='nimbusexperiment',
name='enable_review_slack_notifications',
field=models.BooleanField(default=True, verbose_name='Enable Review Slack Notifications'),
),
]
4 changes: 4 additions & 0 deletions experimenter/experimenter/experiments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,10 @@ class NimbusExperiment(NimbusConstants, TargetingConstants, FilterMixin, models.
blank=True,
verbose_name="Subscribers",
)
enable_review_slack_notifications = models.BooleanField(
"Enable Review Slack Notifications",
default=True,
)
use_group_id = models.BooleanField(default=True)
objects = NimbusExperimentManager()
is_firefox_labs_opt_in = models.BooleanField(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def test_outputs_expected_schema_for_empty_experiment(self):
"channels": [],
"conclusion_recommendations": [],
"countries": [],
"enable_review_slack_notifications": True,
"equal_branch_ratio": experiment.equal_branch_ratio,
"excluded_experiments": [],
"exclude_countries": experiment.exclude_countries,
Expand Down Expand Up @@ -191,6 +192,7 @@ def test_outputs_expected_schema_for_complete_experiment(self):
"channel": experiment.channel,
"channels": experiment.channels,
"conclusion_recommendations": [],
"enable_review_slack_notifications": True,
"equal_branch_ratio": experiment.equal_branch_ratio,
"excluded_experiments": [],
"exclude_countries": experiment.exclude_countries,
Expand Down
27 changes: 21 additions & 6 deletions experimenter/experimenter/nimbus_ui/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -1329,19 +1329,34 @@ def save(self, commit=True):
return self.instance


class ToggleReviewSlackNotificationsForm(NimbusChangeLogFormMixin, forms.ModelForm):
class Meta:
model = NimbusExperiment
fields = ["enable_review_slack_notifications"]

def get_changelog_message(self):
status = (
"enabled"
if self.cleaned_data.get("enable_review_slack_notifications")
else "disabled"
)
return f"{self.request.user} {status} review Slack notifications"


class SlackNotificationMixin:
slack_action = None

@transaction.atomic
def save(self, commit=True):
experiment = super().save(commit=commit)
if self.slack_action:
nimbus_send_slack_notification.delay(
experiment_id=experiment.id,
email_addresses=experiment.notification_emails,
action_text=NimbusConstants.SLACK_FORM_ACTIONS[self.slack_action],
requesting_user_email=self.request.user.email,
)
if experiment.enable_review_slack_notifications:
nimbus_send_slack_notification.delay(
experiment_id=experiment.id,
email_addresses=experiment.notification_emails,
action_text=NimbusConstants.SLACK_FORM_ACTIONS[self.slack_action],
requesting_user_email=self.request.user.email,
)
return experiment


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,31 @@ <h4 class="mb-0">{{ experiment.name }}</h4>
<i class="fa-solid fa-copy"></i>
</button>
</div>
<div class="d-flex align-items-center mb-3">
<form method="post"
action="{% url 'nimbus-ui-toggle-review-slack-notifications' slug=experiment.slug %}"
class="d-flex align-items-center">
{% csrf_token %}
<input type="hidden" name="enable_review_slack_notifications" value="false">
<div class="form-check form-switch">
<input type="checkbox"
id="slack-notifications-toggle"
name="enable_review_slack_notifications"
value="true"
{% if experiment.enable_review_slack_notifications %}checked{% endif %}
onchange="this.form.submit()"
class="form-check-input"
role="switch"
style="cursor: pointer">
<label for="slack-notifications-toggle"
class="form-check-label text-secondary small"
style="cursor: pointer">
<i class="fa-solid fa-bell me-1"></i>
<span>Enable review Slack notifications</span>
</label>
</div>
</form>
</div>
{% if experiment.parent %}
<p class="text-secondary small">
Cloned from
Expand Down
143 changes: 143 additions & 0 deletions experimenter/experimenter/nimbus_ui/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
TagFormSet,
TakeawaysForm,
ToggleArchiveForm,
ToggleReviewSlackNotificationsForm,
UnsubscribeForm,
)
from experimenter.openidc.tests.factories import UserFactory
Expand Down Expand Up @@ -404,6 +405,54 @@ def test_toggle_unarchive(self):
)


class TestToggleReviewSlackNotificationsForm(RequestFormTestCase):
@parameterized.expand(
[
(
"enable",
False,
True,
"enabled",
),
(
"disable",
True,
False,
"disabled",
),
]
)
def test_toggle_slack_notifications(
self, _name, initial_value, new_value, expected_status
):
experiment = NimbusExperiment.objects.create(
owner=self.user,
name="Test Experiment",
slug="test-experiment",
enable_review_slack_notifications=initial_value,
)

data = {
"enable_review_slack_notifications": new_value,
}

form = ToggleReviewSlackNotificationsForm(
data, instance=experiment, request=self.request
)
self.assertTrue(form.is_valid())

updated_experiment = form.save()

self.assertEqual(updated_experiment.enable_review_slack_notifications, new_value)

changelog_message = form.get_changelog_message()

self.assertEqual(
changelog_message,
f"{self.user} {expected_status} review Slack notifications",
)


class TestQAStatusForm(RequestFormTestCase):
def test_form_updates_qa_fields_and_creates_changelog(self):
experiment = NimbusExperimentFactory.create_with_lifecycle(
Expand Down Expand Up @@ -1382,6 +1431,100 @@ def test_approve_update_rollout_form(self):
self.mock_preview_task.assert_called_once_with(countdown=5)
self.mock_allocate_bucket_range.assert_called_once()

@parameterized.expand(
[
(
"draft_to_review_skips_slack_when_disabled",
DraftToReviewForm,
NimbusExperiment.Status.DRAFT,
{},
False,
False,
),
(
"draft_to_review_sends_slack_when_enabled",
DraftToReviewForm,
NimbusExperiment.Status.DRAFT,
{},
True,
True,
),
(
"live_to_update_rollout_skips_slack_when_disabled",
LiveToUpdateRolloutForm,
NimbusExperiment.Status.LIVE,
{"is_rollout": True},
False,
False,
),
(
"live_to_update_rollout_sends_slack_when_enabled",
LiveToUpdateRolloutForm,
NimbusExperiment.Status.LIVE,
{"is_rollout": True},
True,
True,
),
(
"live_to_end_enrollment_skips_slack_when_disabled",
LiveToEndEnrollmentForm,
NimbusExperiment.Status.LIVE,
{},
False,
False,
),
(
"live_to_end_enrollment_sends_slack_when_enabled",
LiveToEndEnrollmentForm,
NimbusExperiment.Status.LIVE,
{},
True,
True,
),
(
"live_to_complete_skips_slack_when_disabled",
LiveToCompleteForm,
NimbusExperiment.Status.LIVE,
{},
False,
False,
),
(
"live_to_complete_sends_slack_when_enabled",
LiveToCompleteForm,
NimbusExperiment.Status.LIVE,
{},
True,
True,
),
]
)
def test_slack_notification_behavior(
self,
_name,
form_class,
status,
extra_kwargs,
enable_slack,
should_call_slack,
):
experiment = NimbusExperimentFactory.create(
status=status,
status_next=None,
publish_status=NimbusExperiment.PublishStatus.IDLE,
enable_review_slack_notifications=enable_slack,
**extra_kwargs,
)
form = form_class(data={}, instance=experiment, request=self.request)
self.assertTrue(form.is_valid(), form.errors)

form.save()

if should_call_slack:
self.mock_slack_task.assert_called_once()
else:
self.mock_slack_task.assert_not_called()


class TestOverviewForm(RequestFormTestCase):
def test_valid_form_saves(self):
Expand Down
37 changes: 37 additions & 0 deletions experimenter/experimenter/nimbus_ui/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1626,6 +1626,43 @@ def test_toggle_archive_status_to_unarchive(self):
self.assertFalse(updated_experiment.is_archived)


class TestToggleReviewSlackNotificationsView(AuthTestCase):
def setUp(self):
super().setUp()
self.experiment = NimbusExperiment.objects.create(
slug="test-experiment",
name="Test Experiment",
owner=self.user,
enable_review_slack_notifications=True,
)

@parameterized.expand(
[
("disable", True, False),
("enable", False, True),
]
)
def test_toggle_slack_notifications(self, _name, initial_value, new_value):
self.experiment.enable_review_slack_notifications = initial_value
self.experiment.save()

response = self.client.post(
reverse(
"nimbus-ui-toggle-review-slack-notifications",
kwargs={"slug": self.experiment.slug},
),
{"enable_review_slack_notifications": new_value},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(
response.url,
reverse("nimbus-ui-detail", kwargs={"slug": self.experiment.slug}),
)

updated_experiment = NimbusExperiment.objects.get(slug=self.experiment.slug)
self.assertEqual(updated_experiment.enable_review_slack_notifications, new_value)


class TestOverviewUpdateView(AuthTestCase):
def test_get_renders_for_draft_experiment(self):
experiment = NimbusExperimentFactory.create_with_lifecycle(
Expand Down
6 changes: 6 additions & 0 deletions experimenter/experimenter/nimbus_ui/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
TagsManageView,
TakeawaysUpdateView,
ToggleArchiveView,
ToggleReviewSlackNotificationsView,
UnsubscribeView,
)

Expand Down Expand Up @@ -200,6 +201,11 @@
FeatureUnsubscribeView.as_view(),
name="nimbus-ui-feature-unsubscribe",
),
re_path(
r"^(?P<slug>[\w-]+)/toggle_review_slack_notifications/",
ToggleReviewSlackNotificationsView.as_view(),
name="nimbus-ui-toggle-review-slack-notifications",
),
re_path(
r"^(?P<slug>[\w-]+)/update_collaborators/",
CollaboratorsUpdateView.as_view(),
Expand Down
12 changes: 12 additions & 0 deletions experimenter/experimenter/nimbus_ui/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
TagFormSet,
TakeawaysForm,
ToggleArchiveForm,
ToggleReviewSlackNotificationsForm,
UnsubscribeForm,
)

Expand Down Expand Up @@ -591,6 +592,17 @@ class UnsubscribeView(
form_class = UnsubscribeForm


class ToggleReviewSlackNotificationsView(
NimbusExperimentViewMixin,
RequestFormMixin,
UpdateView,
):
form_class = ToggleReviewSlackNotificationsForm

def get_success_url(self):
return reverse("nimbus-ui-detail", kwargs={"slug": self.object.slug})


class FeatureSubscribeView(FeatureSubscriberViewMixin):
form_class = FeatureSubscribeForm
url_name = "nimbus-ui-feature-subscribe"
Expand Down