From 2ab1754066cfca7a2725f7d2f49abd0fc19c3468 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Thu, 21 Nov 2024 14:52:04 -0800 Subject: [PATCH 01/19] feat(nimbus): Summary page actions --- .../experimenter/experiments/models.py | 7 +- .../experimenter/nimbus_ui_new/forms.py | 90 ++++++++++ .../templates/nimbus_experiments/detail.html | 17 +- .../nimbus_experiments/launch_controls.html | 28 +++ .../nimbus_experiments/preview_controls.html | 56 ++++++ .../experimenter/nimbus_ui_new/urls.py | 25 +++ .../experimenter/nimbus_ui_new/views.py | 163 ++++++++++++++++++ 7 files changed, 381 insertions(+), 5 deletions(-) create mode 100644 experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html create mode 100644 experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/preview_controls.html diff --git a/experimenter/experimenter/experiments/models.py b/experimenter/experimenter/experiments/models.py index a5f3bb7e3c..c4f3d411b0 100644 --- a/experimenter/experimenter/experiments/models.py +++ b/experimenter/experimenter/experiments/models.py @@ -628,11 +628,14 @@ def treatment_branches(self): @property def is_draft(self): - return self.status == self.Status.DRAFT + return self.status == self.Status.DRAFT and self.publish_status == self.PublishStatus.IDLE @property def is_review(self): - return self.is_draft and self.publish_status == self.PublishStatus.REVIEW + return ( + self.status == self.Status.DRAFT + and self.publish_status == self.PublishStatus.REVIEW + ) @property def is_preview(self): diff --git a/experimenter/experimenter/nimbus_ui_new/forms.py b/experimenter/experimenter/nimbus_ui_new/forms.py index b30e792ab7..20c3fee218 100644 --- a/experimenter/experimenter/nimbus_ui_new/forms.py +++ b/experimenter/experimenter/nimbus_ui_new/forms.py @@ -201,6 +201,10 @@ class Meta: def save(self, commit=True): experiment = super().save(commit=commit) experiment.subscribers.add(self.request.user) + # Generate the changelog after all updates + generate_nimbus_changelog( + experiment, self.request.user, self.get_changelog_message() + ) return experiment def get_changelog_message(self): @@ -215,7 +219,93 @@ class Meta: def save(self, commit=True): experiment = super().save(commit=commit) experiment.subscribers.remove(self.request.user) + # Generate the changelog after all updates + generate_nimbus_changelog( + experiment, self.request.user, self.get_changelog_message() + ) return experiment def get_changelog_message(self): return f"{self.request.user} removed subscriber" + + +class LaunchToPreviewForm(NimbusChangeLogFormMixin, forms.ModelForm): + class Meta: + model = NimbusExperiment + fields = [] + + def save(self, commit=True): + experiment = super().save(commit=commit) + # Update experiment status or other relevant fields + experiment.status = NimbusExperiment.Status.PREVIEW + experiment.save() + # Generate the changelog after all updates + generate_nimbus_changelog( + experiment, self.request.user, self.get_changelog_message() + ) + return experiment + + def get_changelog_message(self): + return f"{self.request.user} launched experiment to Preview" + + +class LaunchWithoutPreviewForm(NimbusChangeLogFormMixin, forms.ModelForm): + class Meta: + model = NimbusExperiment + fields = [] + + def save(self, commit=True): + experiment = super().save(commit=commit) + # Update experiment status or other relevant fields + experiment.publish_status = NimbusExperiment.PublishStatus.REVIEW + experiment.status = NimbusExperiment.Status.DRAFT + experiment.status_next = NimbusExperiment.Status.LIVE + experiment.save() + # Generate the changelog after all updates + generate_nimbus_changelog( + experiment, self.request.user, self.get_changelog_message() + ) + return experiment + + def get_changelog_message(self): + return f"{self.request.user} requested launch without Preview" + + +class LaunchPreviewToReviewForm(NimbusChangeLogFormMixin, forms.ModelForm): + class Meta: + model = NimbusExperiment + fields = [] + + def save(self, commit=True): + experiment = super().save(commit=commit) + experiment.publish_status = NimbusExperiment.PublishStatus.REVIEW + experiment.status_next = NimbusExperiment.Status.LIVE + experiment.save() + # Generate the changelog after all updates + generate_nimbus_changelog( + experiment, self.request.user, self.get_changelog_message() + ) + return experiment + + def get_changelog_message(self): + return f"{self.request.user} requested launch from Preview" + + +class LaunchPreviewToDraftForm(NimbusChangeLogFormMixin, forms.ModelForm): + class Meta: + model = NimbusExperiment + fields = [] + + def save(self, commit=True): + experiment = super().save(commit=commit) + experiment.status = NimbusExperiment.Status.DRAFT + experiment.status_next = None + experiment.save() + # Generate the changelog after all updates + generate_nimbus_changelog( + experiment, self.request.user, self.get_changelog_message() + ) + return experiment + + def get_changelog_message(self): + return f"{self.request.user} moved the experiment back to Draft" diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html index 32f30e22ab..9b835f4c8d 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html @@ -16,9 +16,20 @@

{{ experiment.name }}

{{ experiment.slug }}

- {% include "nimbus_experiments/timeline.html" %} - - +
+
+ {% include "nimbus_experiments/timeline.html" %} +
+
+ {% if experiment.is_draft %} + {% include "nimbus_experiments/launch_controls.html" %} + {% elif experiment.is_preview %} + {% include "nimbus_experiments/preview_controls.html" %} + {% endif %} +
+
+ {% include "nimbus_experiments/takeaways_card.html" %} diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html new file mode 100644 index 0000000000..2040505922 --- /dev/null +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html @@ -0,0 +1,28 @@ +{# templates/nimbus_experiments/launch_controls.html #} +
+

Do you want to test this experiment before launching to production? + Learn more + +

+ +
+
+ {% csrf_token %} + +
+ +
+ {% csrf_token %} + +
+
+
+ diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/preview_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/preview_controls.html new file mode 100644 index 0000000000..a2df29209c --- /dev/null +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/preview_controls.html @@ -0,0 +1,56 @@ +{# templates/nimbus_experiments/preview_success.html #} +
+

+ + + + All set! Your experiment is in Preview mode and you can test it now. +

+
+ +
+
+ {% csrf_token %} +

+ This experiment is currently live for testing, but you will need to let QA know in your + PI request. When you have received a sign-off, click “Request Launch” to launch the experiment. + Note: It can take up to an hour before clients receive a preview experiment. +

+ + +
+ + +
+
+ + +
+ + +
+ + +
+
+
+ + diff --git a/experimenter/experimenter/nimbus_ui_new/urls.py b/experimenter/experimenter/nimbus_ui_new/urls.py index aa1647bdd4..68151dac23 100644 --- a/experimenter/experimenter/nimbus_ui_new/urls.py +++ b/experimenter/experimenter/nimbus_ui_new/urls.py @@ -1,6 +1,10 @@ from django.urls import re_path from experimenter.nimbus_ui_new.views import ( + LaunchPreviewToDraftView, + LaunchPreviewToReviewView, + LaunchToPreviewView, + LaunchWithoutPreviewView, MetricsUpdateView, NimbusChangeLogsView, NimbusExperimentDetailView, @@ -64,4 +68,25 @@ UnsubscribeView.as_view(), name="nimbus-new-unsubscribe", ), + re_path( + r"^(?P[\w-]+)/launch-to-preview/$", + LaunchToPreviewView.as_view(), + name="launch-to-preview", + ), + re_path( + r"^(?P[\w-]+)/launch-without-preview/$", + LaunchWithoutPreviewView.as_view(), + name="launch-without-preview", + ), + re_path( + r"^(?P[\w-]+)/launch-preview-to-review/$", + LaunchPreviewToReviewView.as_view(), + name="launch-preview-to-review", + ), + re_path( + r"^(?P[\w-]+)/launch-preview-to-draft/$", + LaunchPreviewToDraftView.as_view(), + name="launch-preview-to-draft", + ), + ] diff --git a/experimenter/experimenter/nimbus_ui_new/views.py b/experimenter/experimenter/nimbus_ui_new/views.py index 5043e46193..be4e7a15ec 100644 --- a/experimenter/experimenter/nimbus_ui_new/views.py +++ b/experimenter/experimenter/nimbus_ui_new/views.py @@ -1,9 +1,13 @@ from django.conf import settings from django.http import HttpResponse from django.urls import reverse +from django.shortcuts import render + from django.views.generic import CreateView, DetailView from django.views.generic.edit import UpdateView from django_filters.views import FilterView +from django.template.loader import render_to_string +from django.http import JsonResponse from experimenter.experiments.constants import RISK_QUESTIONS from experimenter.experiments.models import NimbusExperiment @@ -14,6 +18,10 @@ StatusChoices, ) from experimenter.nimbus_ui_new.forms import ( + LaunchPreviewToDraftForm, + LaunchPreviewToReviewForm, + LaunchToPreviewForm, + LaunchWithoutPreviewForm, MetricsForm, NimbusExperimentCreateForm, QAStatusForm, @@ -235,3 +243,158 @@ class UnsubscribeView(NimbusExperimentViewMixin, RequestFormMixin, UpdateView): def form_valid(self, form): super().form_valid(form) return self.render_to_response(self.get_context_data(form=form)) + + +# class LaunchToPreviewView(NimbusExperimentViewMixin, RequestFormMixin, UpdateView): +# form_class = LaunchToPreviewForm +# template_name = "nimbus_experiments/launch_controls.html" + +# def render_combined_response(self, experiment): +# # Render the timeline +# timeline_html = render_to_string( +# "nimbus_experiments/timeline.html", +# {"experiment": experiment}, +# request=self.request, +# ) +# # Render appropriate controls based on state +# controls_template = ( +# "nimbus_experiments/preview_controls.html" +# if experiment.is_preview +# else "nimbus_experiments/launch_controls.html" +# ) +# controls_html = render_to_string( +# controls_template, +# {"experiment": experiment}, +# request=self.request, +# ) +# return HttpResponse( +# f""" +#
{timeline_html}
+#
{controls_html}
+# """ +# ) + +# def form_valid(self, form): +# super().form_valid(form) +# return self.render_combined_response(self.object) + + +# class LaunchWithoutPreviewView(NimbusExperimentViewMixin, RequestFormMixin, UpdateView): +# form_class = LaunchWithoutPreviewForm +# template_name = "nimbus_experiments/launch_controls.html" + +# def form_valid(self, form): +# super().form_valid(form) +# return self.render_to_response(self.get_context_data(form=form)) + + +# class LaunchPreviewToReviewView( +# NimbusExperimentViewMixin, RequestFormMixin, UpdateView +# ): +# form_class = LaunchPreviewToReviewForm +# template_name = "nimbus_experiments/preview_controls.html" + +# def form_valid(self, form): +# form.save() +# return self.render_to_response(self.get_context_data(form=form)) + + +# class LaunchPreviewToDraftView(NimbusExperimentViewMixin, RequestFormMixin, UpdateView): +# form_class = LaunchPreviewToDraftForm +# template_name = "nimbus_experiments/preview_controls.html" + +# def form_valid(self, form): +# super().form_valid(form) +# return render( +# self.request, +# "nimbus_experiments/launch_controls.html", +# self.get_context_data(form=form), +# ) + + +class TimelineAndControlsMixin: + """ + Mixin to handle rendering of the timeline and controls for a given experiment. + """ + + def render_response_with_timeline_and_controls(self, experiment, controls_template): + """ + Renders the timeline and controls together as a combined response. + """ + # Render timeline + timeline_html = render_to_string( + "nimbus_experiments/timeline.html", + {"experiment": experiment}, + request=self.request, + ) + + # Render controls + controls_html = render_to_string( + controls_template, + {"experiment": experiment}, + request=self.request, + ) + + return HttpResponse( + f""" +
{timeline_html}
+
{controls_html}
+ """ + ) + + def form_valid(self, form): + """ + Override form_valid to include timeline and controls rendering. + """ + super().form_valid(form) + experiment = self.object + controls_template = self.get_controls_template(experiment) + return self.render_response_with_timeline_and_controls( + experiment, controls_template + ) + + def get_controls_template(self, experiment): + """ + Override in the view if the controls template depends on the experiment state. + """ + raise NotImplementedError("You must define get_controls_template in the view.") + + +class LaunchToPreviewView( + NimbusExperimentViewMixin, RequestFormMixin, TimelineAndControlsMixin, UpdateView +): + form_class = LaunchToPreviewForm + + def get_controls_template(self, experiment): + return ( + "nimbus_experiments/preview_controls.html" + if experiment.is_preview + else "nimbus_experiments/launch_controls.html" + ) + + +class LaunchWithoutPreviewView( + NimbusExperimentViewMixin, RequestFormMixin, TimelineAndControlsMixin, UpdateView +): + form_class = LaunchWithoutPreviewForm + + def get_controls_template(self, experiment): + return "nimbus_experiments/launch_controls.html" + + +class LaunchPreviewToReviewView( + NimbusExperimentViewMixin, RequestFormMixin, TimelineAndControlsMixin, UpdateView +): + form_class = LaunchPreviewToReviewForm + + def get_controls_template(self, experiment): + return "nimbus_experiments/preview_controls.html" + + +class LaunchPreviewToDraftView( + NimbusExperimentViewMixin, RequestFormMixin, TimelineAndControlsMixin, UpdateView +): + form_class = LaunchPreviewToDraftForm + + def get_controls_template(self, experiment): + return "nimbus_experiments/launch_controls.html" From 611c3939a4ecac2e5622c912ce768203417f3154 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Tue, 10 Dec 2024 23:48:04 -0800 Subject: [PATCH 02/19] feat(nimbus): Cancel review flow --- .../experimenter/experiments/constants.py | 7 ++ .../experimenter/nimbus_ui_new/forms.py | 40 +++++-- .../templates/nimbus_experiments/detail.html | 4 +- .../nimbus_experiments/launch_controls.html | 4 +- ...html => launch_with_preview_controls.html} | 11 +- .../launch_without_preview_controls.html | 49 ++++++++ .../nimbus_experiments/review_controls.html | 11 ++ .../experimenter/nimbus_ui_new/urls.py | 7 +- .../experimenter/nimbus_ui_new/views.py | 106 ++++-------------- 9 files changed, 135 insertions(+), 104 deletions(-) rename experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/{preview_controls.html => launch_with_preview_controls.html} (79%) create mode 100644 experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html create mode 100644 experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html diff --git a/experimenter/experimenter/experiments/constants.py b/experimenter/experimenter/experiments/constants.py index ea4c7c9f63..8a88ccd7f5 100644 --- a/experimenter/experimenter/experiments/constants.py +++ b/experimenter/experimenter/experiments/constants.py @@ -821,6 +821,13 @@ class EmailType(models.TextChoices): ENROLLMENT_END = "Enrollment End" +EXTERNAL_URLS = { + "SIGNOFF_QA": "https://experimenter.info/qa-sign-off", + "TRAINING_AND_PLANNING_DOC": "https://experimenter.info/for-product", + "PREVIEW_LAUNCH_DOC": "https://mana.mozilla.org/wiki/display/FJT/Nimbus", +} + + RISK_QUESTIONS = { "BRAND": ( "If the public, users or press, were to discover this experiment and " diff --git a/experimenter/experimenter/nimbus_ui_new/forms.py b/experimenter/experimenter/nimbus_ui_new/forms.py index 20c3fee218..8974a99fcc 100644 --- a/experimenter/experimenter/nimbus_ui_new/forms.py +++ b/experimenter/experimenter/nimbus_ui_new/forms.py @@ -254,9 +254,30 @@ class Meta: model = NimbusExperiment fields = [] + # def save(self, commit=True): + # experiment = super().save(commit=commit) + # # Update experiment status or other relevant fields + # experiment.publish_status = NimbusExperiment.PublishStatus.REVIEW + # experiment.status = NimbusExperiment.Status.DRAFT + # experiment.status_next = NimbusExperiment.Status.LIVE + # experiment.save() + # # Generate the changelog after all updates + # generate_nimbus_changelog( + # experiment, self.request.user, self.get_changelog_message() + # ) + # return experiment + + def get_changelog_message(self): + return f"{self.request.user} requested launch without Preview" + + +class LaunchPreviewToReviewForm(NimbusChangeLogFormMixin, forms.ModelForm): + class Meta: + model = NimbusExperiment + fields = [] + def save(self, commit=True): experiment = super().save(commit=commit) - # Update experiment status or other relevant fields experiment.publish_status = NimbusExperiment.PublishStatus.REVIEW experiment.status = NimbusExperiment.Status.DRAFT experiment.status_next = NimbusExperiment.Status.LIVE @@ -268,18 +289,18 @@ def save(self, commit=True): return experiment def get_changelog_message(self): - return f"{self.request.user} requested launch without Preview" + return f"{self.request.user} requested launch from Preview" -class LaunchPreviewToReviewForm(NimbusChangeLogFormMixin, forms.ModelForm): +class LaunchPreviewToDraftForm(NimbusChangeLogFormMixin, forms.ModelForm): class Meta: model = NimbusExperiment fields = [] def save(self, commit=True): experiment = super().save(commit=commit) - experiment.publish_status = NimbusExperiment.PublishStatus.REVIEW - experiment.status_next = NimbusExperiment.Status.LIVE + experiment.status = NimbusExperiment.Status.DRAFT + experiment.status_next = None experiment.save() # Generate the changelog after all updates generate_nimbus_changelog( @@ -288,24 +309,23 @@ def save(self, commit=True): return experiment def get_changelog_message(self): - return f"{self.request.user} requested launch from Preview" + return f"{self.request.user} moved the experiment back to Draft" -class LaunchPreviewToDraftForm(NimbusChangeLogFormMixin, forms.ModelForm): +class CancelReviewForm(NimbusChangeLogFormMixin, forms.ModelForm): class Meta: model = NimbusExperiment fields = [] def save(self, commit=True): experiment = super().save(commit=commit) - experiment.status = NimbusExperiment.Status.DRAFT + experiment.publish_status = NimbusExperiment.PublishStatus.IDLE experiment.status_next = None experiment.save() - # Generate the changelog after all updates generate_nimbus_changelog( experiment, self.request.user, self.get_changelog_message() ) return experiment def get_changelog_message(self): - return f"{self.request.user} moved the experiment back to Draft" + return f"{self.request.user} cancelled the review" diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html index 9b835f4c8d..d2ce1da564 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html @@ -25,7 +25,9 @@

{{ experiment.name }}

{% if experiment.is_draft %} {% include "nimbus_experiments/launch_controls.html" %} {% elif experiment.is_preview %} - {% include "nimbus_experiments/preview_controls.html" %} + {% include "nimbus_experiments/launch_with_preview_controls.html" %} + {% elif experiment.is_review %} + {% include "nimbus_experiments/review_controls.html" %} {% endif %} diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html index 2040505922..52833b6c9d 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html @@ -1,7 +1,7 @@ {# templates/nimbus_experiments/launch_controls.html #}

Do you want to test this experiment before launching to production? - Learn more + Learn more

@@ -12,7 +12,7 @@ hx-target="#experiment-container" hx-swap="innerHTML"> {% csrf_token %} - +
{% csrf_token %}

- This experiment is currently live for testing, but you will need to let QA know in your - PI request. When you have received a sign-off, click “Request Launch” to launch the experiment. + This experiment is currently live for testing, but you will need to let QA know in your + PI request. When you have received a sign-off, click “Request Launch” to launch the experiment. Note: It can take up to an hour before clients receive a preview experiment.

- +
- +
-
- + +
+ +
+ {% csrf_token %} +
+ + +
+
+ + +
+ + +
+ + diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html new file mode 100644 index 0000000000..f544779678 --- /dev/null +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html @@ -0,0 +1,11 @@ +
+ {% csrf_token %} +
+

The experiment is currently under review. If you wish to cancel the review, click the button below:

+ +
+
diff --git a/experimenter/experimenter/nimbus_ui_new/urls.py b/experimenter/experimenter/nimbus_ui_new/urls.py index 68151dac23..46b08dca91 100644 --- a/experimenter/experimenter/nimbus_ui_new/urls.py +++ b/experimenter/experimenter/nimbus_ui_new/urls.py @@ -1,6 +1,7 @@ from django.urls import re_path from experimenter.nimbus_ui_new.views import ( + CancelReviewView, LaunchPreviewToDraftView, LaunchPreviewToReviewView, LaunchToPreviewView, @@ -88,5 +89,9 @@ LaunchPreviewToDraftView.as_view(), name="launch-preview-to-draft", ), - + re_path( + r"^(?P[\w-]+)/cancel-review/$", + CancelReviewView.as_view(), + name="cancel-review", + ), ] diff --git a/experimenter/experimenter/nimbus_ui_new/views.py b/experimenter/experimenter/nimbus_ui_new/views.py index be4e7a15ec..bc297c0210 100644 --- a/experimenter/experimenter/nimbus_ui_new/views.py +++ b/experimenter/experimenter/nimbus_ui_new/views.py @@ -9,7 +9,7 @@ from django.template.loader import render_to_string from django.http import JsonResponse -from experimenter.experiments.constants import RISK_QUESTIONS +from experimenter.experiments.constants import EXTERNAL_URLS, RISK_QUESTIONS from experimenter.experiments.models import NimbusExperiment from experimenter.nimbus_ui_new.filtersets import ( STATUS_FILTERS, @@ -18,6 +18,7 @@ StatusChoices, ) from experimenter.nimbus_ui_new.forms import ( + CancelReviewForm, LaunchPreviewToDraftForm, LaunchPreviewToReviewForm, LaunchToPreviewForm, @@ -131,6 +132,7 @@ def build_experiment_context(experiment): ] context = { "RISK_QUESTIONS": RISK_QUESTIONS, + "EXTERNAL_URLS": EXTERNAL_URLS, "primary_outcome_links": primary_outcome_links, "secondary_outcome_links": secondary_outcome_links, "segment_links": segment_links, @@ -245,90 +247,21 @@ def form_valid(self, form): return self.render_to_response(self.get_context_data(form=form)) -# class LaunchToPreviewView(NimbusExperimentViewMixin, RequestFormMixin, UpdateView): -# form_class = LaunchToPreviewForm -# template_name = "nimbus_experiments/launch_controls.html" - -# def render_combined_response(self, experiment): -# # Render the timeline -# timeline_html = render_to_string( -# "nimbus_experiments/timeline.html", -# {"experiment": experiment}, -# request=self.request, -# ) -# # Render appropriate controls based on state -# controls_template = ( -# "nimbus_experiments/preview_controls.html" -# if experiment.is_preview -# else "nimbus_experiments/launch_controls.html" -# ) -# controls_html = render_to_string( -# controls_template, -# {"experiment": experiment}, -# request=self.request, -# ) -# return HttpResponse( -# f""" -#
{timeline_html}
-#
{controls_html}
-# """ -# ) - -# def form_valid(self, form): -# super().form_valid(form) -# return self.render_combined_response(self.object) - - -# class LaunchWithoutPreviewView(NimbusExperimentViewMixin, RequestFormMixin, UpdateView): -# form_class = LaunchWithoutPreviewForm -# template_name = "nimbus_experiments/launch_controls.html" - -# def form_valid(self, form): -# super().form_valid(form) -# return self.render_to_response(self.get_context_data(form=form)) - - -# class LaunchPreviewToReviewView( -# NimbusExperimentViewMixin, RequestFormMixin, UpdateView -# ): -# form_class = LaunchPreviewToReviewForm -# template_name = "nimbus_experiments/preview_controls.html" - -# def form_valid(self, form): -# form.save() -# return self.render_to_response(self.get_context_data(form=form)) - - -# class LaunchPreviewToDraftView(NimbusExperimentViewMixin, RequestFormMixin, UpdateView): -# form_class = LaunchPreviewToDraftForm -# template_name = "nimbus_experiments/preview_controls.html" - -# def form_valid(self, form): -# super().form_valid(form) -# return render( -# self.request, -# "nimbus_experiments/launch_controls.html", -# self.get_context_data(form=form), -# ) - - class TimelineAndControlsMixin: """ - Mixin to handle rendering of the timeline and controls for a given experiment. + Mixin to handle rendering of timeline and controls for a given experiment. """ def render_response_with_timeline_and_controls(self, experiment, controls_template): """ - Renders the timeline and controls together as a combined response. + Renders both the timeline and controls together as a response. """ - # Render timeline timeline_html = render_to_string( "nimbus_experiments/timeline.html", {"experiment": experiment}, request=self.request, ) - # Render controls controls_html = render_to_string( controls_template, {"experiment": experiment}, @@ -337,14 +270,16 @@ def render_response_with_timeline_and_controls(self, experiment, controls_templa return HttpResponse( f""" -
{timeline_html}
-
{controls_html}
+
+
{timeline_html}
+
{controls_html}
+
""" ) def form_valid(self, form): """ - Override form_valid to include timeline and controls rendering. + Override form_valid to include timeline and controls in the response. """ super().form_valid(form) experiment = self.object @@ -353,12 +288,6 @@ def form_valid(self, form): experiment, controls_template ) - def get_controls_template(self, experiment): - """ - Override in the view if the controls template depends on the experiment state. - """ - raise NotImplementedError("You must define get_controls_template in the view.") - class LaunchToPreviewView( NimbusExperimentViewMixin, RequestFormMixin, TimelineAndControlsMixin, UpdateView @@ -367,7 +296,7 @@ class LaunchToPreviewView( def get_controls_template(self, experiment): return ( - "nimbus_experiments/preview_controls.html" + "nimbus_experiments/launch_with_preview_controls.html" if experiment.is_preview else "nimbus_experiments/launch_controls.html" ) @@ -379,7 +308,7 @@ class LaunchWithoutPreviewView( form_class = LaunchWithoutPreviewForm def get_controls_template(self, experiment): - return "nimbus_experiments/launch_controls.html" + return "nimbus_experiments/launch_without_preview_controls.html" class LaunchPreviewToReviewView( @@ -388,7 +317,7 @@ class LaunchPreviewToReviewView( form_class = LaunchPreviewToReviewForm def get_controls_template(self, experiment): - return "nimbus_experiments/preview_controls.html" + return "nimbus_experiments/review_controls.html" class LaunchPreviewToDraftView( @@ -398,3 +327,12 @@ class LaunchPreviewToDraftView( def get_controls_template(self, experiment): return "nimbus_experiments/launch_controls.html" + + +class CancelReviewView( + NimbusExperimentViewMixin, RequestFormMixin, TimelineAndControlsMixin, UpdateView +): + form_class = CancelReviewForm + + def get_controls_template(self, experiment): + return "nimbus_experiments/launch_controls.html" From cf977c6f33daf4d19c402eabfde3a5ceb8253c09 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Wed, 11 Dec 2024 01:42:56 -0800 Subject: [PATCH 03/19] feat(nimbus): Cancel review flow --- .../experimenter/experiments/models.py | 5 +- .../nimbus_ui_new/static/js/control.js | 13 +++ .../nimbus_ui_new/static/webpack.config.js | 1 + .../templates/nimbus_experiments/detail.html | 36 +++--- .../nimbus_experiments/launch_controls.html | 53 ++++----- .../launch_with_preview_controls.html | 106 +++++++++--------- .../launch_without_preview_controls.html | 88 +++++++-------- .../nimbus_experiments/review_controls.html | 22 ++-- .../experimenter/nimbus_ui_new/views.py | 5 +- 9 files changed, 176 insertions(+), 153 deletions(-) create mode 100644 experimenter/experimenter/nimbus_ui_new/static/js/control.js diff --git a/experimenter/experimenter/experiments/models.py b/experimenter/experimenter/experiments/models.py index c4f3d411b0..9c5e99f40b 100644 --- a/experimenter/experimenter/experiments/models.py +++ b/experimenter/experimenter/experiments/models.py @@ -628,7 +628,10 @@ def treatment_branches(self): @property def is_draft(self): - return self.status == self.Status.DRAFT and self.publish_status == self.PublishStatus.IDLE + return ( + self.status == self.Status.DRAFT + and self.publish_status == self.PublishStatus.IDLE + ) @property def is_review(self): diff --git a/experimenter/experimenter/nimbus_ui_new/static/js/control.js b/experimenter/experimenter/nimbus_ui_new/static/js/control.js new file mode 100644 index 0000000000..7fb9939dfd --- /dev/null +++ b/experimenter/experimenter/nimbus_ui_new/static/js/control.js @@ -0,0 +1,13 @@ +export default function toggleSubmitButton() { + const checkboxes = document.querySelectorAll(".form-check-input"); + const submitButton = document.getElementById("request-launch-button"); + const allChecked = Array.from(checkboxes).every( + (checkbox) => checkbox.checked, + ); + submitButton.disabled = !allChecked; +} +document.body.addEventListener("htmx:afterSwap", () => { + document.querySelectorAll(".form-check-input").forEach((checkbox) => { + checkbox.addEventListener("change", toggleSubmitButton); + }); +}); diff --git a/experimenter/experimenter/nimbus_ui_new/static/webpack.config.js b/experimenter/experimenter/nimbus_ui_new/static/webpack.config.js index 254a035c75..be0183c2ab 100644 --- a/experimenter/experimenter/nimbus_ui_new/static/webpack.config.js +++ b/experimenter/experimenter/nimbus_ui_new/static/webpack.config.js @@ -5,6 +5,7 @@ module.exports = { entry: { app: "./js/index.js", experiment_list: "./js/experiment_list.js", + control: "./js/control.js", }, output: { filename: "[name].bundle.js", diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html index d2ce1da564..7910c4a712 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html @@ -16,22 +16,25 @@

{{ experiment.name }}

{{ experiment.slug }}

-
-
- {% include "nimbus_experiments/timeline.html" %} -
-
- {% if experiment.is_draft %} - {% include "nimbus_experiments/launch_controls.html" %} - {% elif experiment.is_preview %} - {% include "nimbus_experiments/launch_with_preview_controls.html" %} - {% elif experiment.is_review %} - {% include "nimbus_experiments/review_controls.html" %} - {% endif %} -
-
- +
+
+ {% include "nimbus_experiments/timeline.html" %} + +
+
+ {% if experiment.is_draft %} + {% include "nimbus_experiments/launch_controls.html" %} + + {% elif experiment.is_preview %} + {% include "nimbus_experiments/launch_with_preview_controls.html" %} + + {% elif experiment.is_review %} + {% include "nimbus_experiments/review_controls.html" %} + + {% endif %} +
+
+ {% include "nimbus_experiments/takeaways_card.html" %} @@ -331,4 +334,5 @@
{% block extrascripts %} {{ block.super }} + {% endblock %} diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html index 52833b6c9d..75baf18816 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html @@ -1,28 +1,31 @@ {# templates/nimbus_experiments/launch_controls.html #}
-

Do you want to test this experiment before launching to production? - Learn more - -

- -
-
- {% csrf_token %} - -
- -
- {% csrf_token %} - -
-
+

+ Do you want to test this experiment before launching to production? + Learn more +

+
+
+ {% csrf_token %} + +
+
+ {% csrf_token %} + +
+
- diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html index 7e9908ec2e..059372637b 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html @@ -1,55 +1,61 @@ {# templates/nimbus_experiments/preview_success.html #}
-

- - - - All set! Your experiment is in Preview mode and you can test it now. -

+

+ + + + All set! Your experiment is in Preview mode and you can test it now. +

-
-
- {% csrf_token %} -

- This experiment is currently live for testing, but you will need to let QA know in your - PI request. When you have received a sign-off, click “Request Launch” to launch the experiment. - Note: It can take up to an hour before clients receive a preview experiment. -

- - -
- - -
-
- - -
- -
- - -
-
+
+ {% csrf_token %} +

+ This experiment is currently live for testing, but you will need to let QA know in your + PI request. When you have received a sign-off, click “Request Launch” to launch the experiment. + Note: It can take up to an hour before clients receive a preview experiment. +

+ +
+ + +
+
+ + +
+ +
+ + +
+
- - diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html index c4dfabf606..bbbfaa436f 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html @@ -1,49 +1,45 @@
- -
- {% csrf_token %} - We recommend previewing before launch - -
-
- -
+ {% csrf_token %} -
- - -
-
- - -
- - + We recommend previewing before launch + +
+ +
+ {% csrf_token %} +
+ + +
+
+ + +
+ +
- - diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html index f544779678..3980bf3457 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html @@ -1,11 +1,11 @@ -
- {% csrf_token %} -
-

The experiment is currently under review. If you wish to cancel the review, click the button below:

- -
-
+
+ {% csrf_token %} +
+

The experiment is currently under review. If you wish to cancel the review, click the button below:

+ +
+
diff --git a/experimenter/experimenter/nimbus_ui_new/views.py b/experimenter/experimenter/nimbus_ui_new/views.py index bc297c0210..cb489a9748 100644 --- a/experimenter/experimenter/nimbus_ui_new/views.py +++ b/experimenter/experimenter/nimbus_ui_new/views.py @@ -1,13 +1,10 @@ from django.conf import settings from django.http import HttpResponse +from django.template.loader import render_to_string from django.urls import reverse -from django.shortcuts import render - from django.views.generic import CreateView, DetailView from django.views.generic.edit import UpdateView from django_filters.views import FilterView -from django.template.loader import render_to_string -from django.http import JsonResponse from experimenter.experiments.constants import EXTERNAL_URLS, RISK_QUESTIONS from experimenter.experiments.models import NimbusExperiment From e1260b9257ddd60ecd992bca8df8023ca0f43ca6 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Wed, 11 Dec 2024 01:47:04 -0800 Subject: [PATCH 04/19] feat(nimbus): Cancel review flow --- .../experimenter/nimbus_ui_new/forms.py | 19 ------------------- .../launch_with_preview_controls.html | 4 +--- .../launch_without_preview_controls.html | 1 + .../nimbus_experiments/review_controls.html | 1 + 4 files changed, 3 insertions(+), 22 deletions(-) diff --git a/experimenter/experimenter/nimbus_ui_new/forms.py b/experimenter/experimenter/nimbus_ui_new/forms.py index 8974a99fcc..97b178d7a1 100644 --- a/experimenter/experimenter/nimbus_ui_new/forms.py +++ b/experimenter/experimenter/nimbus_ui_new/forms.py @@ -201,7 +201,6 @@ class Meta: def save(self, commit=True): experiment = super().save(commit=commit) experiment.subscribers.add(self.request.user) - # Generate the changelog after all updates generate_nimbus_changelog( experiment, self.request.user, self.get_changelog_message() ) @@ -219,7 +218,6 @@ class Meta: def save(self, commit=True): experiment = super().save(commit=commit) experiment.subscribers.remove(self.request.user) - # Generate the changelog after all updates generate_nimbus_changelog( experiment, self.request.user, self.get_changelog_message() ) @@ -236,10 +234,8 @@ class Meta: def save(self, commit=True): experiment = super().save(commit=commit) - # Update experiment status or other relevant fields experiment.status = NimbusExperiment.Status.PREVIEW experiment.save() - # Generate the changelog after all updates generate_nimbus_changelog( experiment, self.request.user, self.get_changelog_message() ) @@ -254,19 +250,6 @@ class Meta: model = NimbusExperiment fields = [] - # def save(self, commit=True): - # experiment = super().save(commit=commit) - # # Update experiment status or other relevant fields - # experiment.publish_status = NimbusExperiment.PublishStatus.REVIEW - # experiment.status = NimbusExperiment.Status.DRAFT - # experiment.status_next = NimbusExperiment.Status.LIVE - # experiment.save() - # # Generate the changelog after all updates - # generate_nimbus_changelog( - # experiment, self.request.user, self.get_changelog_message() - # ) - # return experiment - def get_changelog_message(self): return f"{self.request.user} requested launch without Preview" @@ -282,7 +265,6 @@ def save(self, commit=True): experiment.status = NimbusExperiment.Status.DRAFT experiment.status_next = NimbusExperiment.Status.LIVE experiment.save() - # Generate the changelog after all updates generate_nimbus_changelog( experiment, self.request.user, self.get_changelog_message() ) @@ -302,7 +284,6 @@ def save(self, commit=True): experiment.status = NimbusExperiment.Status.DRAFT experiment.status_next = None experiment.save() - # Generate the changelog after all updates generate_nimbus_changelog( experiment, self.request.user, self.get_changelog_message() ) diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html index 059372637b..1577dcb3c0 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html @@ -1,4 +1,4 @@ -{# templates/nimbus_experiments/preview_success.html #} +{# templates/nimbus_experiments/launch_with_preview_controls.html #}

PI request. When you have received a sign-off, click “Request Launch” to launch the experiment. Note: It can take up to an hour before clients receive a preview experiment.

-
experiment onboarding program
-
{% csrf_token %} diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html index 1577dcb3c0..2790408d98 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html @@ -14,8 +14,8 @@
@@ -45,13 +45,13 @@
diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html index ff8712bddc..c32d1f27bd 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html @@ -1,8 +1,8 @@ {# templates/nimbus_experiments/launch_without_preview_controls.html #}
{% csrf_token %} @@ -13,8 +13,8 @@
{% csrf_token %} @@ -40,7 +40,7 @@ disabled>Request Launch
diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html index ee504d8c2a..3eb551fb4c 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html @@ -1,7 +1,7 @@ {# templates/nimbus_experiments/review_controls.html #}
{% csrf_token %} diff --git a/experimenter/experimenter/nimbus_ui_new/urls.py b/experimenter/experimenter/nimbus_ui_new/urls.py index 46b08dca91..b40393924d 100644 --- a/experimenter/experimenter/nimbus_ui_new/urls.py +++ b/experimenter/experimenter/nimbus_ui_new/urls.py @@ -72,26 +72,26 @@ re_path( r"^(?P[\w-]+)/launch-to-preview/$", LaunchToPreviewView.as_view(), - name="launch-to-preview", + name="nimbus-new-launch-to-preview", ), re_path( r"^(?P[\w-]+)/launch-without-preview/$", LaunchWithoutPreviewView.as_view(), - name="launch-without-preview", + name="nimbus-new-launch-without-preview", ), re_path( r"^(?P[\w-]+)/launch-preview-to-review/$", LaunchPreviewToReviewView.as_view(), - name="launch-preview-to-review", + name="nimbus-new-launch-preview-to-review", ), re_path( r"^(?P[\w-]+)/launch-preview-to-draft/$", LaunchPreviewToDraftView.as_view(), - name="launch-preview-to-draft", + name="nimbus-new-launch-preview-to-draft", ), re_path( r"^(?P[\w-]+)/cancel-review/$", CancelReviewView.as_view(), - name="cancel-review", + name="nimbus-new-cancel-review", ), ] From 0c770c3642c4ef4389fb756d0296ee2ebf503019 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Wed, 11 Dec 2024 10:28:01 -0800 Subject: [PATCH 06/19] test(nimbus): Test cases --- .../nimbus_ui_new/tests/test_forms.py | 84 ++++++++++++++++++- .../nimbus_ui_new/tests/test_views.py | 67 +++++++++++++++ 2 files changed, 149 insertions(+), 2 deletions(-) diff --git a/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py b/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py index 694b83e27a..e0150f15b2 100644 --- a/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py +++ b/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py @@ -5,6 +5,11 @@ from experimenter.experiments.tests.factories import NimbusExperimentFactory from experimenter.nimbus_ui_new.constants import NimbusUIConstants from experimenter.nimbus_ui_new.forms import ( + CancelReviewForm, + LaunchPreviewToDraftForm, + LaunchPreviewToReviewForm, + LaunchToPreviewForm, + LaunchWithoutPreviewForm, MetricsForm, NimbusExperimentCreateForm, QAStatusForm, @@ -295,7 +300,7 @@ def test_subscribe_form_adds_subscriber(self): self.assertTrue(form.is_valid()) form.save() self.assertIn(self.request.user, self.experiment.subscribers.all()) - changelog = self.experiment.changes.get() + changelog = self.experiment.changes.latest("changed_on") self.assertEqual(changelog.changed_by, self.user) self.assertIn("dev@example.com added subscriber", changelog.message) @@ -305,6 +310,81 @@ def test_unsubscribe_form_removes_subscriber(self): self.assertTrue(form.is_valid()) form.save() self.assertNotIn(self.request.user, self.experiment.subscribers.all()) - changelog = self.experiment.changes.get() + changelog = self.experiment.changes.latest("changed_on") self.assertEqual(changelog.changed_by, self.user) self.assertIn("dev@example.com removed subscriber", changelog.message) + + +class TestLaunchForms(RequestFormTestCase): + def setUp(self): + super().setUp() + self.experiment = NimbusExperimentFactory.create() + + def test_launch_to_preview_form(self): + form = LaunchToPreviewForm( + data={}, instance=self.experiment, request=self.request + ) + self.assertTrue(form.is_valid(), form.errors) + + experiment = form.save() + self.assertEqual(experiment.status, NimbusExperiment.Status.PREVIEW) + + changelog = experiment.changes.latest("changed_on") + self.assertEqual(changelog.changed_by, self.user) + self.assertIn("launched experiment to Preview", changelog.message) + + def test_launch_without_preview_form(self): + form = LaunchWithoutPreviewForm( + data={}, instance=self.experiment, request=self.request + ) + self.assertTrue(form.is_valid(), form.errors) + + experiment = form.save() + self.assertEqual(experiment.status, self.experiment.status) + + changelog = experiment.changes.latest("changed_on") + self.assertEqual(changelog.changed_by, self.user) + self.assertIn("requested launch without Preview", changelog.message) + + def test_launch_preview_to_review_form(self): + form = LaunchPreviewToReviewForm( + data={}, instance=self.experiment, request=self.request + ) + self.assertTrue(form.is_valid(), form.errors) + + experiment = form.save() + self.assertEqual( + experiment.publish_status, NimbusExperiment.PublishStatus.REVIEW + ) + self.assertEqual(experiment.status, NimbusExperiment.Status.DRAFT) + self.assertEqual(experiment.status_next, NimbusExperiment.Status.LIVE) + + changelog = experiment.changes.latest("changed_on") + self.assertEqual(changelog.changed_by, self.user) + self.assertIn("requested launch from Preview", changelog.message) + + def test_launch_preview_to_draft_form(self): + form = LaunchPreviewToDraftForm( + data={}, instance=self.experiment, request=self.request + ) + self.assertTrue(form.is_valid(), form.errors) + + experiment = form.save() + self.assertEqual(experiment.status, NimbusExperiment.Status.DRAFT) + self.assertIsNone(experiment.status_next) + + changelog = experiment.changes.latest("changed_on") + self.assertEqual(changelog.changed_by, self.user) + self.assertIn("moved the experiment back to Draft", changelog.message) + + def test_cancel_review_form(self): + form = CancelReviewForm(data={}, instance=self.experiment, request=self.request) + self.assertTrue(form.is_valid(), form.errors) + + experiment = form.save() + self.assertEqual(experiment.publish_status, NimbusExperiment.PublishStatus.IDLE) + self.assertIsNone(experiment.status_next) + + changelog = experiment.changes.latest("changed_on") + self.assertEqual(changelog.changed_by, self.user) + self.assertIn("cancelled the review", changelog.message) diff --git a/experimenter/experimenter/nimbus_ui_new/tests/test_views.py b/experimenter/experimenter/nimbus_ui_new/tests/test_views.py index c7d70540d6..94f5a0522d 100644 --- a/experimenter/experimenter/nimbus_ui_new/tests/test_views.py +++ b/experimenter/experimenter/nimbus_ui_new/tests/test_views.py @@ -1183,3 +1183,70 @@ def test_post_updates_metrics_and_segments(self): self.assertEqual(experiment.primary_outcomes, [outcome1.slug]) self.assertEqual(experiment.secondary_outcomes, [outcome2.slug]) self.assertEqual(experiment.segments, [segment1.slug, segment2.slug]) + + +class TestLaunchViews(AuthTestCase): + def setUp(self): + super().setUp() + self.experiment = NimbusExperimentFactory.create() + + def test_launch_to_preview_view(self): + response = self.client.post( + reverse( + "nimbus-new-launch-to-preview", kwargs={"slug": self.experiment.slug} + ), + ) + self.assertEqual(response.status_code, 200) + self.experiment.refresh_from_db() + self.assertEqual(self.experiment.status, NimbusExperiment.Status.PREVIEW) + + def test_launch_without_preview_view(self): + response = self.client.post( + reverse( + "nimbus-new-launch-without-preview", + kwargs={"slug": self.experiment.slug}, + ), + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(self.experiment.status, self.experiment.status) + + def test_launch_preview_to_review_view(self): + response = self.client.post( + reverse( + "nimbus-new-launch-preview-to-review", + kwargs={"slug": self.experiment.slug}, + ), + ) + self.assertEqual(response.status_code, 200) + self.experiment.refresh_from_db() + self.assertEqual( + self.experiment.publish_status, NimbusExperiment.PublishStatus.REVIEW + ) + self.assertEqual(self.experiment.status, NimbusExperiment.Status.DRAFT) + self.assertEqual(self.experiment.status_next, NimbusExperiment.Status.LIVE) + + def test_launch_preview_to_draft_view(self): + response = self.client.post( + reverse( + "nimbus-new-launch-preview-to-draft", + kwargs={"slug": self.experiment.slug}, + ), + ) + self.assertEqual(response.status_code, 200) + self.experiment.refresh_from_db() + self.assertEqual(self.experiment.status, NimbusExperiment.Status.DRAFT) + self.assertIsNone(self.experiment.status_next) + + def test_cancel_review_view(self): + self.experiment.publish_status = NimbusExperiment.PublishStatus.REVIEW + self.experiment.save() + + response = self.client.post( + reverse("nimbus-new-cancel-review", kwargs={"slug": self.experiment.slug}), + ) + self.assertEqual(response.status_code, 200) + self.experiment.refresh_from_db() + self.assertEqual( + self.experiment.publish_status, NimbusExperiment.PublishStatus.IDLE + ) + self.assertIsNone(self.experiment.status_next) From eefc8e1675ea768f8b28ddf8c1dfa1f846c562d4 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Wed, 11 Dec 2024 10:29:05 -0800 Subject: [PATCH 07/19] test(nimbus): Test cases --- experimenter/experimenter/nimbus_ui_new/tests/test_forms.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py b/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py index e0150f15b2..c1639a31c7 100644 --- a/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py +++ b/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py @@ -353,9 +353,7 @@ def test_launch_preview_to_review_form(self): self.assertTrue(form.is_valid(), form.errors) experiment = form.save() - self.assertEqual( - experiment.publish_status, NimbusExperiment.PublishStatus.REVIEW - ) + self.assertEqual(experiment.publish_status, NimbusExperiment.PublishStatus.REVIEW) self.assertEqual(experiment.status, NimbusExperiment.Status.DRAFT) self.assertEqual(experiment.status_next, NimbusExperiment.Status.LIVE) From 0064d5679eeb9cbdbc5570d0251c75ab2f8697d3 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Wed, 11 Dec 2024 10:54:29 -0800 Subject: [PATCH 08/19] test(nimbus): Test cases --- experimenter/experimenter/nimbus_ui_new/tests/test_forms.py | 1 + experimenter/experimenter/nimbus_ui_new/views.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py b/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py index 12c57edd77..9d4e885685 100644 --- a/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py +++ b/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py @@ -394,6 +394,7 @@ def test_cancel_review_form(self): self.assertEqual(changelog.changed_by, self.user) self.assertIn("cancelled the review", changelog.message) + class TestOverviewForm(RequestFormTestCase): def test_valid_form_saves(self): project = ProjectFactory.create() diff --git a/experimenter/experimenter/nimbus_ui_new/views.py b/experimenter/experimenter/nimbus_ui_new/views.py index d7d246ca9d..d2226c1e6c 100644 --- a/experimenter/experimenter/nimbus_ui_new/views.py +++ b/experimenter/experimenter/nimbus_ui_new/views.py @@ -6,7 +6,6 @@ from django.views.generic.edit import UpdateView from django_filters.views import FilterView - from experimenter.experiments.constants import EXTERNAL_URLS, RISK_QUESTIONS from experimenter.experiments.models import ( NimbusExperiment, From 47137d1893046cb4399a152184ebeb98d4651624 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Wed, 11 Dec 2024 11:18:27 -0800 Subject: [PATCH 09/19] test(nimbus): Test cases --- .../templates/nimbus_experiments/detail.html | 33 ++++++++++--------- .../experimenter/nimbus_ui_new/views.py | 5 --- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html index 3fec3cbdfd..94184fc22d 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html @@ -20,26 +20,27 @@ rel="noopener noreferrer" class="btn btn-link p-0">Learn more
-
-
- {% include "nimbus_experiments/timeline.html" %} + {% endfor %} + {% endif %} + +
+
+ {% include "nimbus_experiments/timeline.html" %} -
-
- {% if experiment.is_draft %} - {% include "nimbus_experiments/launch_controls.html" %} +
+
+ {% if experiment.is_draft %} + {% include "nimbus_experiments/launch_controls.html" %} - {% elif experiment.is_preview %} - {% include "nimbus_experiments/launch_with_preview_controls.html" %} + {% elif experiment.is_preview %} + {% include "nimbus_experiments/launch_with_preview_controls.html" %} - {% elif experiment.is_review %} - {% include "nimbus_experiments/review_controls.html" %} + {% elif experiment.is_review %} + {% include "nimbus_experiments/review_controls.html" %} - {% endif %} -
-
- {% endfor %} - {% endif %} + {% endif %} +
+
{% include "nimbus_experiments/takeaways_card.html" %} diff --git a/experimenter/experimenter/nimbus_ui_new/views.py b/experimenter/experimenter/nimbus_ui_new/views.py index d2226c1e6c..c94c585b63 100644 --- a/experimenter/experimenter/nimbus_ui_new/views.py +++ b/experimenter/experimenter/nimbus_ui_new/views.py @@ -272,11 +272,6 @@ class UnsubscribeView( form_class = UnsubscribeForm template_name = "nimbus_experiments/subscribers_list.html" - def form_valid(self, form): - super().form_valid(form) - return self.render_to_response(self.get_context_data(form=form)) - - class TimelineAndControlsMixin: """ Mixin to handle rendering of timeline and controls for a given experiment. From 84d22e5d24948cb168d4bc2529e3a789e20b67ea Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Wed, 11 Dec 2024 16:54:18 -0800 Subject: [PATCH 10/19] test(nimbus): Test cases --- .../templates/nimbus_experiments/experiment_base.html | 11 +---------- .../templates/nimbus_experiments/timeline.html | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/experiment_base.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/experiment_base.html index 7db9910ce1..b1fe8a6fa9 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/experiment_base.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/experiment_base.html @@ -22,15 +22,6 @@

{{ experiment.name }}

{% endif %} -
-
    - {% for status in experiment.timeline %} -
  • - {{ status.label }} - {{ status.date|default:'---' }} -
  • - {% endfor %} -
-
+ {% include "nimbus_experiments/timeline.html" %} {% endblock %} diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html index f2d8db5a40..71cb4ea82e 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html @@ -1,4 +1,4 @@ -
+
    {% for status in experiment.timeline %}
  • From aa6e85d0dacba3f5898127d3f137940e7eea71b6 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Thu, 12 Dec 2024 10:46:26 -0800 Subject: [PATCH 11/19] test(nimbus): Test cases --- .../templates/nimbus_experiments/detail.html | 24 +++++++------------ .../nimbus_experiments/experiment_base.html | 5 +++- .../nimbus_experiments/launch_controls.html | 4 ++-- .../launch_with_preview_controls.html | 6 ++--- .../launch_without_preview_controls.html | 10 ++++---- .../nimbus_experiments/review_controls.html | 4 ++-- .../experimenter/nimbus_ui_new/views.py | 3 +-- 7 files changed, 26 insertions(+), 30 deletions(-) diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html index 94184fc22d..504fad014d 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html @@ -22,24 +22,17 @@
{% endfor %} {% endif %} - -
-
- {% include "nimbus_experiments/timeline.html" %} +
+ {% if experiment.is_draft %} + {% include "nimbus_experiments/launch_controls.html" %} -
-
- {% if experiment.is_draft %} - {% include "nimbus_experiments/launch_controls.html" %} - - {% elif experiment.is_preview %} - {% include "nimbus_experiments/launch_with_preview_controls.html" %} + {% elif experiment.is_preview %} + {% include "nimbus_experiments/launch_with_preview_controls.html" %} - {% elif experiment.is_review %} - {% include "nimbus_experiments/review_controls.html" %} + {% elif experiment.is_review %} + {% include "nimbus_experiments/review_controls.html" %} - {% endif %} -
+ {% endif %}
{% include "nimbus_experiments/takeaways_card.html" %} @@ -383,4 +376,5 @@
{{ block.super }} + {% endblock %} diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/experiment_base.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/experiment_base.html index b1fe8a6fa9..390b89842e 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/experiment_base.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/experiment_base.html @@ -22,6 +22,9 @@

{{ experiment.name }}

{% endif %}
- {% include "nimbus_experiments/timeline.html" %} +
+ {% include "nimbus_experiments/timeline.html" %} + +
{% endblock %} diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html index b88c1c08b5..2771c4e116 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html @@ -10,7 +10,7 @@ {% csrf_token %} diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html index c32d1f27bd..f907f126f4 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html @@ -3,8 +3,8 @@
+ hx-target="#launch-controls, #experiment-timeline" + hx-swap="outerHTML"> {% csrf_token %} We recommend previewing before launch
diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html index 3eb551fb4c..1de7f45c00 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html @@ -2,8 +2,8 @@
+ hx-target="#launch-controls, #experiment-timeline" + hx-swap="outerHTML"> {% csrf_token %}

The experiment is currently under review. If you wish to cancel the review, click the button below:

diff --git a/experimenter/experimenter/nimbus_ui_new/views.py b/experimenter/experimenter/nimbus_ui_new/views.py index c94c585b63..2b5b952023 100644 --- a/experimenter/experimenter/nimbus_ui_new/views.py +++ b/experimenter/experimenter/nimbus_ui_new/views.py @@ -272,6 +272,7 @@ class UnsubscribeView( form_class = UnsubscribeForm template_name = "nimbus_experiments/subscribers_list.html" + class TimelineAndControlsMixin: """ Mixin to handle rendering of timeline and controls for a given experiment. @@ -295,10 +296,8 @@ def render_response_with_timeline_and_controls(self, experiment, controls_templa return HttpResponse( f""" -
{timeline_html}
{controls_html}
-
""" ) From 1c9e951a662f6717e858cfa7b170189b3fe21fc6 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Thu, 12 Dec 2024 11:40:05 -0800 Subject: [PATCH 12/19] test(nimbus): Test cases --- .../nimbus_ui_new/templates/nimbus_experiments/detail.html | 2 ++ experimenter/experimenter/nimbus_ui_new/views.py | 7 ++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html index 504fad014d..f0452a56df 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html @@ -23,6 +23,8 @@ {% endfor %} {% endif %}
+ {{experiment.is_draft}} + {{experiment.is_preview}} {% if experiment.is_draft %} {% include "nimbus_experiments/launch_controls.html" %} diff --git a/experimenter/experimenter/nimbus_ui_new/views.py b/experimenter/experimenter/nimbus_ui_new/views.py index 2b5b952023..22a09e831c 100644 --- a/experimenter/experimenter/nimbus_ui_new/views.py +++ b/experimenter/experimenter/nimbus_ui_new/views.py @@ -319,11 +319,8 @@ class LaunchToPreviewView( form_class = LaunchToPreviewForm def get_controls_template(self, experiment): - return ( - "nimbus_experiments/launch_with_preview_controls.html" - if experiment.is_preview - else "nimbus_experiments/launch_controls.html" - ) + return "nimbus_experiments/launch_with_preview_controls.html" + class LaunchWithoutPreviewView( From fab606b98280f35fc7a04233eb682af73bd67553 Mon Sep 17 00:00:00 2001 From: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:06:04 -0500 Subject: [PATCH 13/19] Some ideas from jared --- .../experimenter/experiments/models.py | 8 ++ .../experimenter/nimbus_ui_new/forms.py | 88 ++++++----------- .../templates/nimbus_experiments/detail.html | 48 +++++++-- .../nimbus_experiments/launch_controls.html | 8 +- .../launch_with_preview_controls.html | 12 +-- .../launch_without_preview_controls.html | 10 +- .../nimbus_experiments/review_controls.html | 4 +- .../nimbus_ui_new/tests/test_forms.py | 26 ++--- .../nimbus_ui_new/tests/test_views.py | 12 +-- .../experimenter/nimbus_ui_new/urls.py | 40 ++++---- .../experimenter/nimbus_ui_new/views.py | 99 ++++--------------- 11 files changed, 145 insertions(+), 210 deletions(-) diff --git a/experimenter/experimenter/experiments/models.py b/experimenter/experimenter/experiments/models.py index 233161694f..6c64ab644e 100644 --- a/experimenter/experimenter/experiments/models.py +++ b/experimenter/experimenter/experiments/models.py @@ -677,6 +677,14 @@ def is_observation(self): def is_started(self): return self.status in (self.Status.LIVE, self.Status.COMPLETE) + @property + def can_draft_to_preview(self): + return self.is_draft and not self.is_review + + @property + def can_preview_to_draft(self): + return self.is_preview + @property def draft_date(self): if change := self.changes.all().order_by("changed_on").first(): diff --git a/experimenter/experimenter/nimbus_ui_new/forms.py b/experimenter/experimenter/nimbus_ui_new/forms.py index e3b0f2d466..5203e039b1 100644 --- a/experimenter/experimenter/nimbus_ui_new/forms.py +++ b/experimenter/experimenter/nimbus_ui_new/forms.py @@ -335,9 +335,6 @@ class Meta: def save(self, commit=True): experiment = super().save(commit=commit) experiment.subscribers.add(self.request.user) - generate_nimbus_changelog( - experiment, self.request.user, self.get_changelog_message() - ) return experiment def get_changelog_message(self): @@ -352,95 +349,70 @@ class Meta: def save(self, commit=True): experiment = super().save(commit=commit) experiment.subscribers.remove(self.request.user) - generate_nimbus_changelog( - experiment, self.request.user, self.get_changelog_message() - ) return experiment def get_changelog_message(self): return f"{self.request.user} removed subscriber" -class LaunchToPreviewForm(NimbusChangeLogFormMixin, forms.ModelForm): +class UpdateStatusForm(NimbusChangeLogFormMixin, forms.ModelForm): + status = None + status_next = None + publish_status = None + class Meta: model = NimbusExperiment fields = [] def save(self, commit=True): experiment = super().save(commit=commit) - experiment.status = NimbusExperiment.Status.PREVIEW + experiment.status = self.status + experiment.status_next = self.status_next + experiment.publish_status = self.publish_status experiment.save() - generate_nimbus_changelog( - experiment, self.request.user, self.get_changelog_message() - ) return experiment + +class DraftToPreviewForm(UpdateStatusForm): + status = NimbusExperiment.Status.PREVIEW + status_next = NimbusExperiment.Status.PREVIEW + publish_status = NimbusExperiment.PublishStatus.IDLE + def get_changelog_message(self): return f"{self.request.user} launched experiment to Preview" -class LaunchWithoutPreviewForm(NimbusChangeLogFormMixin, forms.ModelForm): - class Meta: - model = NimbusExperiment - fields = [] +class DraftToReviewForm(UpdateStatusForm): + status = NimbusExperiment.Status.DRAFT + status_next = NimbusExperiment.Status.LIVE + publish_status = NimbusExperiment.PublishStatus.REVIEW def get_changelog_message(self): return f"{self.request.user} requested launch without Preview" -class LaunchPreviewToReviewForm(NimbusChangeLogFormMixin, forms.ModelForm): - class Meta: - model = NimbusExperiment - fields = [] - - def save(self, commit=True): - experiment = super().save(commit=commit) - experiment.publish_status = NimbusExperiment.PublishStatus.REVIEW - experiment.status = NimbusExperiment.Status.DRAFT - experiment.status_next = NimbusExperiment.Status.LIVE - experiment.save() - generate_nimbus_changelog( - experiment, self.request.user, self.get_changelog_message() - ) - return experiment +class PreviewToReviewForm(UpdateStatusForm): + status = NimbusExperiment.Status.DRAFT + status_next = NimbusExperiment.Status.LIVE + publish_status = NimbusExperiment.PublishStatus.REVIEW def get_changelog_message(self): return f"{self.request.user} requested launch from Preview" -class LaunchPreviewToDraftForm(NimbusChangeLogFormMixin, forms.ModelForm): - class Meta: - model = NimbusExperiment - fields = [] - - def save(self, commit=True): - experiment = super().save(commit=commit) - experiment.status = NimbusExperiment.Status.DRAFT - experiment.status_next = None - experiment.save() - generate_nimbus_changelog( - experiment, self.request.user, self.get_changelog_message() - ) - return experiment +class PreviewToDraftForm(UpdateStatusForm): + status = NimbusExperiment.Status.DRAFT + status_next = NimbusExperiment.Status.DRAFT + publish_status = NimbusExperiment.PublishStatus.IDLE def get_changelog_message(self): return f"{self.request.user} moved the experiment back to Draft" -class CancelReviewForm(NimbusChangeLogFormMixin, forms.ModelForm): - class Meta: - model = NimbusExperiment - fields = [] - - def save(self, commit=True): - experiment = super().save(commit=commit) - experiment.publish_status = NimbusExperiment.PublishStatus.IDLE - experiment.status_next = None - experiment.save() - generate_nimbus_changelog( - experiment, self.request.user, self.get_changelog_message() - ) - return experiment +class ReviewToDraftForm(UpdateStatusForm): + status = NimbusExperiment.Status.DRAFT + status_next = NimbusExperiment.Status.DRAFT + publish_status = NimbusExperiment.PublishStatus.IDLE def get_changelog_message(self): return f"{self.request.user} cancelled the review" diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html index f0452a56df..29abf9ce74 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html @@ -22,20 +22,48 @@
{% endfor %} {% endif %} -
- {{experiment.is_draft}} - {{experiment.is_preview}} - {% if experiment.is_draft %} - {% include "nimbus_experiments/launch_controls.html" %} - {% elif experiment.is_preview %} - {% include "nimbus_experiments/launch_with_preview_controls.html" %} - {% elif experiment.is_review %} - {% include "nimbus_experiments/review_controls.html" %} - {% endif %} + +
+ + {% csrf_token %} + + {% if experiment.is_draft %} + {% if experiment.can_draft_to_preview %} + + {% endif %} + {% if experiment.can_draft_to_review %} + + {% endif %} + {% elif experiment.is_preview %} + {% if experiment.can_preview_to_draft %} + {% endif %} + {% if eperiment.can_preview_to_review %} + {% endif %} + {% elif experiment.is_review %} + {% endif %} +
+ + + + {% include "nimbus_experiments/takeaways_card.html" %} diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html index 2771c4e116..ab5c14f9b8 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html @@ -8,8 +8,8 @@

{% csrf_token %} @@ -18,8 +18,8 @@ {% if is_loading %}disabled{% endif %}>Preview for Testing
{% csrf_token %} diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html index 9d1d4fd990..1d4d248c87 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html @@ -14,8 +14,8 @@
@@ -45,15 +45,11 @@ - +
diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html index f907f126f4..ef5ceb61b6 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html @@ -1,8 +1,8 @@ {# templates/nimbus_experiments/launch_without_preview_controls.html #}
{% csrf_token %} @@ -13,8 +13,8 @@
{% csrf_token %} @@ -40,7 +40,7 @@ disabled>Request Launch
diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html index 1de7f45c00..7e6f546c89 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html @@ -1,7 +1,7 @@ {# templates/nimbus_experiments/review_controls.html #}
{% csrf_token %} diff --git a/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py b/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py index 9d4e885685..b320d65035 100644 --- a/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py +++ b/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py @@ -8,13 +8,13 @@ ) from experimenter.nimbus_ui_new.constants import NimbusUIConstants from experimenter.nimbus_ui_new.forms import ( - CancelReviewForm, + ReviewToDraftForm, DocumentationLinkCreateForm, DocumentationLinkDeleteForm, - LaunchPreviewToDraftForm, - LaunchPreviewToReviewForm, - LaunchToPreviewForm, - LaunchWithoutPreviewForm, + PreviewToDraftForm, + PreviewToReviewForm, + DraftToPreviewForm, + DraftToReviewForm, MetricsForm, NimbusExperimentCreateForm, OverviewForm, @@ -328,9 +328,7 @@ def setUp(self): self.experiment = NimbusExperimentFactory.create() def test_launch_to_preview_form(self): - form = LaunchToPreviewForm( - data={}, instance=self.experiment, request=self.request - ) + form = DraftToPreviewForm(data={}, instance=self.experiment, request=self.request) self.assertTrue(form.is_valid(), form.errors) experiment = form.save() @@ -341,9 +339,7 @@ def test_launch_to_preview_form(self): self.assertIn("launched experiment to Preview", changelog.message) def test_launch_without_preview_form(self): - form = LaunchWithoutPreviewForm( - data={}, instance=self.experiment, request=self.request - ) + form = DraftToReviewForm(data={}, instance=self.experiment, request=self.request) self.assertTrue(form.is_valid(), form.errors) experiment = form.save() @@ -354,7 +350,7 @@ def test_launch_without_preview_form(self): self.assertIn("requested launch without Preview", changelog.message) def test_launch_preview_to_review_form(self): - form = LaunchPreviewToReviewForm( + form = PreviewToReviewForm( data={}, instance=self.experiment, request=self.request ) self.assertTrue(form.is_valid(), form.errors) @@ -369,9 +365,7 @@ def test_launch_preview_to_review_form(self): self.assertIn("requested launch from Preview", changelog.message) def test_launch_preview_to_draft_form(self): - form = LaunchPreviewToDraftForm( - data={}, instance=self.experiment, request=self.request - ) + form = PreviewToDraftForm(data={}, instance=self.experiment, request=self.request) self.assertTrue(form.is_valid(), form.errors) experiment = form.save() @@ -383,7 +377,7 @@ def test_launch_preview_to_draft_form(self): self.assertIn("moved the experiment back to Draft", changelog.message) def test_cancel_review_form(self): - form = CancelReviewForm(data={}, instance=self.experiment, request=self.request) + form = ReviewToDraftForm(data={}, instance=self.experiment, request=self.request) self.assertTrue(form.is_valid(), form.errors) experiment = form.save() diff --git a/experimenter/experimenter/nimbus_ui_new/tests/test_views.py b/experimenter/experimenter/nimbus_ui_new/tests/test_views.py index 25d264503d..b3b119fce2 100644 --- a/experimenter/experimenter/nimbus_ui_new/tests/test_views.py +++ b/experimenter/experimenter/nimbus_ui_new/tests/test_views.py @@ -1291,9 +1291,7 @@ def setUp(self): def test_launch_to_preview_view(self): response = self.client.post( - reverse( - "nimbus-new-launch-to-preview", kwargs={"slug": self.experiment.slug} - ), + reverse("nimbus-new-draft-to-preview", kwargs={"slug": self.experiment.slug}), ) self.assertEqual(response.status_code, 200) self.experiment.refresh_from_db() @@ -1302,7 +1300,7 @@ def test_launch_to_preview_view(self): def test_launch_without_preview_view(self): response = self.client.post( reverse( - "nimbus-new-launch-without-preview", + "nimbus-new-draft-to-review", kwargs={"slug": self.experiment.slug}, ), ) @@ -1312,7 +1310,7 @@ def test_launch_without_preview_view(self): def test_launch_preview_to_review_view(self): response = self.client.post( reverse( - "nimbus-new-launch-preview-to-review", + "nimbus-new-preview-to-review", kwargs={"slug": self.experiment.slug}, ), ) @@ -1327,7 +1325,7 @@ def test_launch_preview_to_review_view(self): def test_launch_preview_to_draft_view(self): response = self.client.post( reverse( - "nimbus-new-launch-preview-to-draft", + "nimbus-new-preview-to-draft", kwargs={"slug": self.experiment.slug}, ), ) @@ -1341,7 +1339,7 @@ def test_cancel_review_view(self): self.experiment.save() response = self.client.post( - reverse("nimbus-new-cancel-review", kwargs={"slug": self.experiment.slug}), + reverse("nimbus-new-review-to-draft", kwargs={"slug": self.experiment.slug}), ) self.assertEqual(response.status_code, 200) self.experiment.refresh_from_db() diff --git a/experimenter/experimenter/nimbus_ui_new/urls.py b/experimenter/experimenter/nimbus_ui_new/urls.py index c6f2cf186f..3f6345597b 100644 --- a/experimenter/experimenter/nimbus_ui_new/urls.py +++ b/experimenter/experimenter/nimbus_ui_new/urls.py @@ -1,13 +1,13 @@ from django.urls import re_path from experimenter.nimbus_ui_new.views import ( - CancelReviewView, + ReviewToDraftView, DocumentationLinkCreateView, DocumentationLinkDeleteView, - LaunchPreviewToDraftView, - LaunchPreviewToReviewView, - LaunchToPreviewView, - LaunchWithoutPreviewView, + PreviewToDraftView, + PreviewToReviewView, + DraftToPreviewView, + DraftToReviewView, MetricsUpdateView, NimbusChangeLogsView, NimbusExperimentDetailView, @@ -88,28 +88,28 @@ name="nimbus-new-unsubscribe", ), re_path( - r"^(?P[\w-]+)/launch-to-preview/$", - LaunchToPreviewView.as_view(), - name="nimbus-new-launch-to-preview", + r"^(?P[\w-]+)/draft-to-preview/$", + DraftToPreviewView.as_view(), + name="nimbus-new-draft-to-preview", ), re_path( - r"^(?P[\w-]+)/launch-without-preview/$", - LaunchWithoutPreviewView.as_view(), - name="nimbus-new-launch-without-preview", + r"^(?P[\w-]+)/draft-to-review/$", + DraftToReviewView.as_view(), + name="nimbus-new-draft-to-review", ), re_path( - r"^(?P[\w-]+)/launch-preview-to-review/$", - LaunchPreviewToReviewView.as_view(), - name="nimbus-new-launch-preview-to-review", + r"^(?P[\w-]+)/preview-to-review/$", + PreviewToReviewView.as_view(), + name="nimbus-new-preview-to-review", ), re_path( - r"^(?P[\w-]+)/launch-preview-to-draft/$", - LaunchPreviewToDraftView.as_view(), - name="nimbus-new-launch-preview-to-draft", + r"^(?P[\w-]+)/preview-to-draft/$", + PreviewToDraftView.as_view(), + name="nimbus-new-preview-to-draft", ), re_path( - r"^(?P[\w-]+)/cancel-review/$", - CancelReviewView.as_view(), - name="nimbus-new-cancel-review", + r"^(?P[\w-]+)/review-to-draft/$", + ReviewToDraftView.as_view(), + name="nimbus-new-review-to-draft", ), ] diff --git a/experimenter/experimenter/nimbus_ui_new/views.py b/experimenter/experimenter/nimbus_ui_new/views.py index 22a09e831c..eeb8ca9322 100644 --- a/experimenter/experimenter/nimbus_ui_new/views.py +++ b/experimenter/experimenter/nimbus_ui_new/views.py @@ -18,13 +18,13 @@ StatusChoices, ) from experimenter.nimbus_ui_new.forms import ( - CancelReviewForm, + ReviewToDraftForm, DocumentationLinkCreateForm, DocumentationLinkDeleteForm, - LaunchPreviewToDraftForm, - LaunchPreviewToReviewForm, - LaunchToPreviewForm, - LaunchWithoutPreviewForm, + PreviewToDraftForm, + PreviewToReviewForm, + DraftToPreviewForm, + DraftToReviewForm, MetricsForm, NimbusExperimentCreateForm, OverviewForm, @@ -157,8 +157,9 @@ def build_experiment_context(experiment): return context -class NimbusExperimentDetailView(NimbusExperimentViewMixin, DetailView): +class NimbusExperimentDetailView(NimbusExperimentViewMixin, UpdateView): template_name = "nimbus_experiments/detail.html" + fields = [] def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -273,87 +274,25 @@ class UnsubscribeView( template_name = "nimbus_experiments/subscribers_list.html" -class TimelineAndControlsMixin: - """ - Mixin to handle rendering of timeline and controls for a given experiment. - """ +class StatusUpdateView(RequestFormMixin, RenderResponseMixin, NimbusExperimentDetailView): + fields = None - def render_response_with_timeline_and_controls(self, experiment, controls_template): - """ - Renders both the timeline and controls together as a response. - """ - timeline_html = render_to_string( - "nimbus_experiments/timeline.html", - {"experiment": experiment}, - request=self.request, - ) - controls_html = render_to_string( - controls_template, - {"experiment": experiment}, - request=self.request, - ) +class DraftToPreviewView(StatusUpdateView): + form_class = DraftToPreviewForm - return HttpResponse( - f""" -
{timeline_html}
-
{controls_html}
- """ - ) - def form_valid(self, form): - """ - Override form_valid to include timeline and controls in the response. - """ - super().form_valid(form) - experiment = self.object - controls_template = self.get_controls_template(experiment) - return self.render_response_with_timeline_and_controls( - experiment, controls_template - ) +class PreviewToDraftView(StatusUpdateView): + form_class = PreviewToDraftForm -class LaunchToPreviewView( - NimbusExperimentViewMixin, RequestFormMixin, TimelineAndControlsMixin, UpdateView -): - form_class = LaunchToPreviewForm - - def get_controls_template(self, experiment): - return "nimbus_experiments/launch_with_preview_controls.html" - +class DraftToReviewView(StatusUpdateView): + form_class = DraftToReviewForm -class LaunchWithoutPreviewView( - NimbusExperimentViewMixin, RequestFormMixin, TimelineAndControlsMixin, UpdateView -): - form_class = LaunchWithoutPreviewForm - - def get_controls_template(self, experiment): - return "nimbus_experiments/launch_without_preview_controls.html" +class PreviewToReviewView(StatusUpdateView): + form_class = PreviewToReviewForm -class LaunchPreviewToReviewView( - NimbusExperimentViewMixin, RequestFormMixin, TimelineAndControlsMixin, UpdateView -): - form_class = LaunchPreviewToReviewForm - - def get_controls_template(self, experiment): - return "nimbus_experiments/review_controls.html" - - -class LaunchPreviewToDraftView( - NimbusExperimentViewMixin, RequestFormMixin, TimelineAndControlsMixin, UpdateView -): - form_class = LaunchPreviewToDraftForm - - def get_controls_template(self, experiment): - return "nimbus_experiments/launch_controls.html" - - -class CancelReviewView( - NimbusExperimentViewMixin, RequestFormMixin, TimelineAndControlsMixin, UpdateView -): - form_class = CancelReviewForm - - def get_controls_template(self, experiment): - return "nimbus_experiments/launch_controls.html" +class ReviewToDraftView(StatusUpdateView): + form_class = ReviewToDraftForm From c98ad23c68a66c96fdf66ba2b21545937b3902cc Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Tue, 24 Dec 2024 12:10:13 -0800 Subject: [PATCH 14/19] feat(nimbus): Launch control flow --- .../experimenter/experiments/models.py | 8 + .../nimbus_ui_new/static/js/control.js | 25 +-- .../templates/nimbus_experiments/detail.html | 43 +---- .../nimbus_experiments/launch_controls.html | 159 ++++++++++++++---- .../launch_with_preview_controls.html | 55 ------ .../launch_without_preview_controls.html | 46 ----- .../nimbus_ui_new/tests/test_forms.py | 39 +++-- .../nimbus_ui_new/tests/test_views.py | 51 +++--- .../experimenter/nimbus_ui_new/urls.py | 6 +- .../experimenter/nimbus_ui_new/views.py | 7 +- 10 files changed, 215 insertions(+), 224 deletions(-) delete mode 100644 experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html delete mode 100644 experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html diff --git a/experimenter/experimenter/experiments/models.py b/experimenter/experimenter/experiments/models.py index 6c64ab644e..ed6b112395 100644 --- a/experimenter/experimenter/experiments/models.py +++ b/experimenter/experimenter/experiments/models.py @@ -681,10 +681,18 @@ def is_started(self): def can_draft_to_preview(self): return self.is_draft and not self.is_review + @property + def can_draft_to_review(self): + return self.can_draft_to_preview + @property def can_preview_to_draft(self): return self.is_preview + @property + def can_preview_to_review(self): + return self.is_preview + @property def draft_date(self): if change := self.changes.all().order_by("changed_on").first(): diff --git a/experimenter/experimenter/nimbus_ui_new/static/js/control.js b/experimenter/experimenter/nimbus_ui_new/static/js/control.js index 7fb9939dfd..88faa90720 100644 --- a/experimenter/experimenter/nimbus_ui_new/static/js/control.js +++ b/experimenter/experimenter/nimbus_ui_new/static/js/control.js @@ -1,13 +1,14 @@ -export default function toggleSubmitButton() { - const checkboxes = document.querySelectorAll(".form-check-input"); - const submitButton = document.getElementById("request-launch-button"); - const allChecked = Array.from(checkboxes).every( - (checkbox) => checkbox.checked, +window.showRecommendation = function () { + const defaultControls = document.getElementById("default-controls"); + const recommendationMessage = document.getElementById( + "recommendation-message", ); - submitButton.disabled = !allChecked; -} -document.body.addEventListener("htmx:afterSwap", () => { - document.querySelectorAll(".form-check-input").forEach((checkbox) => { - checkbox.addEventListener("change", toggleSubmitButton); - }); -}); + defaultControls.classList.add("d-none"); + recommendationMessage.classList.remove("d-none"); +}; +window.toggleSubmitButton = function () { + const checkbox1 = document.getElementById("checkbox-1"); + const checkbox2 = document.getElementById("checkbox-2"); + const submitButton = document.getElementById("request-launch-button"); + submitButton.disabled = !(checkbox1.checked && checkbox2.checked); +}; diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html index 29abf9ce74..f851011df7 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html @@ -22,47 +22,8 @@
{% endfor %} {% endif %} - - - - -
- - {% csrf_token %} - - {% if experiment.is_draft %} - {% if experiment.can_draft_to_preview %} - - {% endif %} - {% if experiment.can_draft_to_review %} - - {% endif %} - {% elif experiment.is_preview %} - {% if experiment.can_preview_to_draft %} - {% endif %} - {% if eperiment.can_preview_to_review %} - {% endif %} - {% elif experiment.is_review %} - {% endif %} - -
- - - + + {% include "nimbus_experiments/launch_controls.html" %} {% include "nimbus_experiments/takeaways_card.html" %} diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html index ab5c14f9b8..ab676d4b18 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_controls.html @@ -1,31 +1,130 @@ -{# templates/nimbus_experiments/launch_controls.html #} -
-

- Do you want to test this experiment before launching to production? - Learn more -

-
-
- {% csrf_token %} - -
-
- {% csrf_token %} - -
-
+
+
+ {% csrf_token %} + + {% if experiment.is_draft %} +
+

+ Do you want to test this experiment before launching to production? + Learn more +

+ {% if experiment.can_draft_to_preview %} + + {% endif %} + {% if experiment.can_draft_to_review %} + + {% endif %} +
+ +
+
+

+ We recommend previewing before launch + +

+
+ + +
+
+ + +
+ + +
+
+ + {% elif experiment.is_preview %} +
+

All set! Your experiment is in Preview mode and you can test it now.

+
+
+

+ This experiment is currently live for testing, but you will need to let QA know in your + PI request. When you have received a sign-off, click “Request Launch” to launch the experiment. + Note: It can take up to an hour before clients receive a preview experiment. +

+
+ + +
+
+ + +
+ {% if experiment.can_preview_to_review %} + + {% endif %} + {% if experiment.can_preview_to_draft %} + + {% endif %} +
+ + {% elif experiment.is_review %} +
+

The experiment is currently under review. If you wish to cancel the review, click the button below:

+ +
+ {% endif %} +
diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html deleted file mode 100644 index 1d4d248c87..0000000000 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_with_preview_controls.html +++ /dev/null @@ -1,55 +0,0 @@ -{# templates/nimbus_experiments/launch_with_preview_controls.html #} -
-

- - - - All set! Your experiment is in Preview mode and you can test it now. -

-
-
-
- {% csrf_token %} -

- This experiment is currently live for testing, but you will need to let QA know in your - PI request. When you have received a sign-off, click “Request Launch” to launch the experiment. - Note: It can take up to an hour before clients receive a preview experiment. -

-
- - -
-
- - -
-
- - -
-
-
diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html deleted file mode 100644 index ef5ceb61b6..0000000000 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/launch_without_preview_controls.html +++ /dev/null @@ -1,46 +0,0 @@ -{# templates/nimbus_experiments/launch_without_preview_controls.html #} -
-
- {% csrf_token %} - We recommend previewing before launch - -
-
-
- {% csrf_token %} -
- - -
-
- - -
- - -
diff --git a/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py b/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py index b320d65035..f79f41cc7f 100644 --- a/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py +++ b/experimenter/experimenter/nimbus_ui_new/tests/test_forms.py @@ -8,17 +8,17 @@ ) from experimenter.nimbus_ui_new.constants import NimbusUIConstants from experimenter.nimbus_ui_new.forms import ( - ReviewToDraftForm, DocumentationLinkCreateForm, DocumentationLinkDeleteForm, - PreviewToDraftForm, - PreviewToReviewForm, DraftToPreviewForm, DraftToReviewForm, MetricsForm, NimbusExperimentCreateForm, OverviewForm, + PreviewToDraftForm, + PreviewToReviewForm, QAStatusForm, + ReviewToDraftForm, SignoffForm, SubscribeForm, TakeawaysForm, @@ -327,62 +327,77 @@ def setUp(self): super().setUp() self.experiment = NimbusExperimentFactory.create() - def test_launch_to_preview_form(self): + def test_draft_to_preview_form(self): form = DraftToPreviewForm(data={}, instance=self.experiment, request=self.request) self.assertTrue(form.is_valid(), form.errors) experiment = form.save() self.assertEqual(experiment.status, NimbusExperiment.Status.PREVIEW) + self.assertEqual(experiment.status_next, NimbusExperiment.Status.PREVIEW) + self.assertEqual(experiment.publish_status, NimbusExperiment.PublishStatus.IDLE) changelog = experiment.changes.latest("changed_on") self.assertEqual(changelog.changed_by, self.user) self.assertIn("launched experiment to Preview", changelog.message) - def test_launch_without_preview_form(self): + def test_draft_to_review_form(self): form = DraftToReviewForm(data={}, instance=self.experiment, request=self.request) self.assertTrue(form.is_valid(), form.errors) experiment = form.save() - self.assertEqual(experiment.status, self.experiment.status) + self.assertEqual(experiment.status, NimbusExperiment.Status.DRAFT) + self.assertEqual(experiment.status_next, NimbusExperiment.Status.LIVE) + self.assertEqual(experiment.publish_status, NimbusExperiment.PublishStatus.REVIEW) changelog = experiment.changes.latest("changed_on") self.assertEqual(changelog.changed_by, self.user) self.assertIn("requested launch without Preview", changelog.message) - def test_launch_preview_to_review_form(self): + def test_preview_to_review_form(self): + self.experiment.status = NimbusExperiment.Status.PREVIEW + self.experiment.save() + form = PreviewToReviewForm( data={}, instance=self.experiment, request=self.request ) self.assertTrue(form.is_valid(), form.errors) experiment = form.save() - self.assertEqual(experiment.publish_status, NimbusExperiment.PublishStatus.REVIEW) self.assertEqual(experiment.status, NimbusExperiment.Status.DRAFT) self.assertEqual(experiment.status_next, NimbusExperiment.Status.LIVE) + self.assertEqual(experiment.publish_status, NimbusExperiment.PublishStatus.REVIEW) changelog = experiment.changes.latest("changed_on") self.assertEqual(changelog.changed_by, self.user) self.assertIn("requested launch from Preview", changelog.message) - def test_launch_preview_to_draft_form(self): + def test_preview_to_draft_form(self): + self.experiment.status = NimbusExperiment.Status.PREVIEW + self.experiment.save() + form = PreviewToDraftForm(data={}, instance=self.experiment, request=self.request) self.assertTrue(form.is_valid(), form.errors) experiment = form.save() self.assertEqual(experiment.status, NimbusExperiment.Status.DRAFT) - self.assertIsNone(experiment.status_next) + self.assertEqual(experiment.status_next, NimbusExperiment.Status.DRAFT) + self.assertEqual(experiment.publish_status, NimbusExperiment.PublishStatus.IDLE) changelog = experiment.changes.latest("changed_on") self.assertEqual(changelog.changed_by, self.user) self.assertIn("moved the experiment back to Draft", changelog.message) - def test_cancel_review_form(self): + def test_review_to_draft_form(self): + self.experiment.publish_status = NimbusExperiment.PublishStatus.REVIEW + self.experiment.save() + form = ReviewToDraftForm(data={}, instance=self.experiment, request=self.request) self.assertTrue(form.is_valid(), form.errors) experiment = form.save() + self.assertEqual(experiment.status, NimbusExperiment.Status.DRAFT) + self.assertEqual(experiment.status_next, NimbusExperiment.Status.DRAFT) self.assertEqual(experiment.publish_status, NimbusExperiment.PublishStatus.IDLE) - self.assertIsNone(experiment.status_next) changelog = experiment.changes.latest("changed_on") self.assertEqual(changelog.changed_by, self.user) diff --git a/experimenter/experimenter/nimbus_ui_new/tests/test_views.py b/experimenter/experimenter/nimbus_ui_new/tests/test_views.py index b3b119fce2..fddd14d5ff 100644 --- a/experimenter/experimenter/nimbus_ui_new/tests/test_views.py +++ b/experimenter/experimenter/nimbus_ui_new/tests/test_views.py @@ -1289,61 +1289,70 @@ def setUp(self): super().setUp() self.experiment = NimbusExperimentFactory.create() - def test_launch_to_preview_view(self): + def test_draft_to_preview(self): response = self.client.post( reverse("nimbus-new-draft-to-preview", kwargs={"slug": self.experiment.slug}), ) self.assertEqual(response.status_code, 200) self.experiment.refresh_from_db() self.assertEqual(self.experiment.status, NimbusExperiment.Status.PREVIEW) + self.assertEqual(self.experiment.status_next, NimbusExperiment.Status.PREVIEW) + self.assertEqual( + self.experiment.publish_status, NimbusExperiment.PublishStatus.IDLE + ) - def test_launch_without_preview_view(self): + def test_draft_to_review(self): response = self.client.post( - reverse( - "nimbus-new-draft-to-review", - kwargs={"slug": self.experiment.slug}, - ), + reverse("nimbus-new-draft-to-review", kwargs={"slug": self.experiment.slug}), ) self.assertEqual(response.status_code, 200) - self.assertEqual(self.experiment.status, self.experiment.status) + self.experiment.refresh_from_db() + self.assertEqual(self.experiment.status, NimbusExperiment.Status.DRAFT) + self.assertEqual(self.experiment.status_next, NimbusExperiment.Status.LIVE) + self.assertEqual( + self.experiment.publish_status, NimbusExperiment.PublishStatus.REVIEW + ) - def test_launch_preview_to_review_view(self): + def test_preview_to_review(self): + self.experiment.status = NimbusExperiment.Status.PREVIEW + self.experiment.save() response = self.client.post( reverse( - "nimbus-new-preview-to-review", - kwargs={"slug": self.experiment.slug}, + "nimbus-new-preview-to-review", kwargs={"slug": self.experiment.slug} ), ) self.assertEqual(response.status_code, 200) self.experiment.refresh_from_db() + self.assertEqual(self.experiment.status, NimbusExperiment.Status.DRAFT) + self.assertEqual(self.experiment.status_next, NimbusExperiment.Status.LIVE) self.assertEqual( self.experiment.publish_status, NimbusExperiment.PublishStatus.REVIEW ) - self.assertEqual(self.experiment.status, NimbusExperiment.Status.DRAFT) - self.assertEqual(self.experiment.status_next, NimbusExperiment.Status.LIVE) - def test_launch_preview_to_draft_view(self): + def test_preview_to_draft(self): + self.experiment.status = NimbusExperiment.Status.PREVIEW + self.experiment.save() response = self.client.post( - reverse( - "nimbus-new-preview-to-draft", - kwargs={"slug": self.experiment.slug}, - ), + reverse("nimbus-new-preview-to-draft", kwargs={"slug": self.experiment.slug}), ) self.assertEqual(response.status_code, 200) self.experiment.refresh_from_db() self.assertEqual(self.experiment.status, NimbusExperiment.Status.DRAFT) - self.assertIsNone(self.experiment.status_next) + self.assertEqual(self.experiment.status_next, NimbusExperiment.Status.DRAFT) + self.assertEqual( + self.experiment.publish_status, NimbusExperiment.PublishStatus.IDLE + ) - def test_cancel_review_view(self): + def test_cancel_review(self): self.experiment.publish_status = NimbusExperiment.PublishStatus.REVIEW self.experiment.save() - response = self.client.post( reverse("nimbus-new-review-to-draft", kwargs={"slug": self.experiment.slug}), ) self.assertEqual(response.status_code, 200) self.experiment.refresh_from_db() + self.assertEqual(self.experiment.status, NimbusExperiment.Status.DRAFT) + self.assertEqual(self.experiment.status_next, NimbusExperiment.Status.DRAFT) self.assertEqual( self.experiment.publish_status, NimbusExperiment.PublishStatus.IDLE ) - self.assertIsNone(self.experiment.status_next) diff --git a/experimenter/experimenter/nimbus_ui_new/urls.py b/experimenter/experimenter/nimbus_ui_new/urls.py index 3f6345597b..1ae7f3fd8b 100644 --- a/experimenter/experimenter/nimbus_ui_new/urls.py +++ b/experimenter/experimenter/nimbus_ui_new/urls.py @@ -1,11 +1,8 @@ from django.urls import re_path from experimenter.nimbus_ui_new.views import ( - ReviewToDraftView, DocumentationLinkCreateView, DocumentationLinkDeleteView, - PreviewToDraftView, - PreviewToReviewView, DraftToPreviewView, DraftToReviewView, MetricsUpdateView, @@ -14,7 +11,10 @@ NimbusExperimentsCreateView, NimbusExperimentsListTableView, OverviewUpdateView, + PreviewToDraftView, + PreviewToReviewView, QAStatusUpdateView, + ReviewToDraftView, SignoffUpdateView, SubscribeView, TakeawaysUpdateView, diff --git a/experimenter/experimenter/nimbus_ui_new/views.py b/experimenter/experimenter/nimbus_ui_new/views.py index eeb8ca9322..17c1e4f33e 100644 --- a/experimenter/experimenter/nimbus_ui_new/views.py +++ b/experimenter/experimenter/nimbus_ui_new/views.py @@ -1,6 +1,5 @@ from django.conf import settings from django.http import HttpResponse -from django.template.loader import render_to_string from django.urls import reverse from django.views.generic import CreateView, DetailView from django.views.generic.edit import UpdateView @@ -18,17 +17,17 @@ StatusChoices, ) from experimenter.nimbus_ui_new.forms import ( - ReviewToDraftForm, DocumentationLinkCreateForm, DocumentationLinkDeleteForm, - PreviewToDraftForm, - PreviewToReviewForm, DraftToPreviewForm, DraftToReviewForm, MetricsForm, NimbusExperimentCreateForm, OverviewForm, + PreviewToDraftForm, + PreviewToReviewForm, QAStatusForm, + ReviewToDraftForm, SignoffForm, SubscribeForm, TakeawaysForm, From 30cd3f1f05caeae0d2cfda50daf0f4fe9edae49e Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Tue, 24 Dec 2024 12:17:17 -0800 Subject: [PATCH 15/19] feat(nimbus): Launch control flow --- .../nimbus_ui_new/templates/nimbus_experiments/detail.html | 1 - 1 file changed, 1 deletion(-) diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html index f851011df7..786bc4796b 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html @@ -367,5 +367,4 @@
{{ block.super }} - {% endblock %} From 5fa331d18c54dfd94299bf0405f2f984fd497886 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Tue, 24 Dec 2024 12:18:00 -0800 Subject: [PATCH 16/19] feat(nimbus): Launch control flow --- .../nimbus_experiments/review_controls.html | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html deleted file mode 100644 index 7e6f546c89..0000000000 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/review_controls.html +++ /dev/null @@ -1,12 +0,0 @@ -{# templates/nimbus_experiments/review_controls.html #} -
- {% csrf_token %} -
-

The experiment is currently under review. If you wish to cancel the review, click the button below:

- -
-
From 8e265397a7d4aeddc8d01b3be585d286bd4872d6 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Tue, 24 Dec 2024 12:27:27 -0800 Subject: [PATCH 17/19] feat(nimbus): Launch control flow --- .../nimbus_experiments/experiment_base.html | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/experiment_base.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/experiment_base.html index 5faad4d1a5..5819ce9364 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/experiment_base.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/experiment_base.html @@ -3,11 +3,10 @@ {% block sidebar %} {% include "nimbus_experiments/sidebar.html" with experiment=experiment %} -{% endblock %} - -{% block main_content_header %} -
-
+{% endblock %}{% block main_content_header %} +
+ +

{{ experiment.name }}

QA Status: {{ experiment.qa_status|default:"Not Set"|title }} @@ -22,7 +21,9 @@

{{ experiment.name }}

{% endif %}
-
+ + +
{% include "nimbus_experiments/timeline.html" %}
From b4a0153ba078384827899fd1d6416eb5dd9fd441 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Tue, 24 Dec 2024 12:30:25 -0800 Subject: [PATCH 18/19] feat(nimbus): Launch control flow --- .../templates/nimbus_experiments/experiment_base.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/experiment_base.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/experiment_base.html index 5819ce9364..f1c56f47fa 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/experiment_base.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/experiment_base.html @@ -3,7 +3,9 @@ {% block sidebar %} {% include "nimbus_experiments/sidebar.html" with experiment=experiment %} -{% endblock %}{% block main_content_header %} +{% endblock %} + +{% block main_content_header %}
@@ -21,10 +23,10 @@

{{ experiment.name }}

{% endif %}
-
{% include "nimbus_experiments/timeline.html" %} +
{% endblock %} From c83735d09137b1cf665cb73d20d205f23ff893d2 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Tue, 24 Dec 2024 12:45:51 -0800 Subject: [PATCH 19/19] feat(nimbus): format --- experimenter/experimenter/nimbus_ui_new/tests/test_views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/experimenter/experimenter/nimbus_ui_new/tests/test_views.py b/experimenter/experimenter/nimbus_ui_new/tests/test_views.py index db2a0594d6..88d7bc7ca6 100644 --- a/experimenter/experimenter/nimbus_ui_new/tests/test_views.py +++ b/experimenter/experimenter/nimbus_ui_new/tests/test_views.py @@ -1359,6 +1359,8 @@ def test_cancel_review(self): self.assertEqual(self.experiment.status_next, NimbusExperiment.Status.DRAFT) self.assertEqual( self.experiment.publish_status, NimbusExperiment.PublishStatus.IDLE + ) + class TestAudienceUpdateView(AuthTestCase): def test_get_renders_page(self):