Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(nimbus): Draft/Preview/Review workflow #11932

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions experimenter/experimenter/experiments/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,13 @@ class FirefoxLabsGroups(models.TextChoices):
ENROLLMENT = "Enrollment"


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 "
Expand Down
8 changes: 4 additions & 4 deletions experimenter/experimenter/experiments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -652,10 +652,10 @@ def is_draft(self):

@property
def is_review(self):
return self.status == self.Status.DRAFT and self.publish_status in [
self.PublishStatus.REVIEW,
self.PublishStatus.WAITING,
]
return (
self.status == self.Status.DRAFT
and self.publish_status == self.PublishStatus.REVIEW
)

@property
def is_preview(self):
Expand Down
91 changes: 91 additions & 0 deletions experimenter/experimenter/nimbus_ui_new/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,9 @@ 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()
)
yashikakhurana marked this conversation as resolved.
Show resolved Hide resolved
return experiment

def get_changelog_message(self):
Expand All @@ -349,7 +352,95 @@ 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 Meta:
model = NimbusExperiment
fields = []

def save(self, commit=True):
experiment = super().save(commit=commit)
experiment.status = NimbusExperiment.Status.PREVIEW
experiment.save()
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 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

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

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

def get_changelog_message(self):
return f"{self.request.user} cancelled the review"
13 changes: 13 additions & 0 deletions experimenter/experimenter/nimbus_ui_new/static/js/control.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,25 @@
</div>
{% endfor %}
{% endif %}
<!-- Experiment Container -->
<div id="experiment-container">
<div id="experiment-timeline">
{% include "nimbus_experiments/timeline.html" %}

</div>
<div id="launch-controls">
{% 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 %}
</div>
</div>
<!-- Takeaways Card -->
{% include "nimbus_experiments/takeaways_card.html" %}

Expand Down Expand Up @@ -363,4 +382,5 @@ <h6>
{% block extrascripts %}
{{ block.super }}
<script src="{% static 'nimbus_ui_new/setup_selectpicker.bundle.js' %}"></script>
<script src="{% static 'nimbus_ui_new/control.bundle.js' %}"></script>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{# templates/nimbus_experiments/launch_controls.html #}
<div class="alert alert-secondary">
<p>
Do you want to test this experiment before launching to production?
<a href="{{ EXTERNAL_URLS.PREVIEW_LAUNCH_DOC }}"
target="_blank"
class="mr-1">Learn more</a>
</p>
<div class="d-flex">
<form method="post"
action="{% url 'nimbus-new-launch-to-preview' slug=experiment.slug %}"
hx-post="{% url 'nimbus-new-launch-to-preview' slug=experiment.slug %}"
hx-target="#experiment-container"
hx-swap="innerHTML">
{% csrf_token %}
<button type="submit"
class="btn btn-primary mx-2"
{% if is_loading %}disabled{% endif %}>Preview for Testing</button>
</form>
<form method="post"
action="{% url 'nimbus-new-launch-without-preview' slug=experiment.slug %}"
hx-post="{% url 'nimbus-new-launch-without-preview' slug=experiment.slug %}"
hx-target="#experiment-container"
hx-swap="outerHTML">
{% csrf_token %}
<button type="submit"
class="btn btn-secondary"
{% if is_loading %}disabled{% endif %}>Request Launch without Preview</button>
</form>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{# templates/nimbus_experiments/launch_with_preview_controls.html #}
<div class="alert alert-success bg-transparent text-success">
<p class="my-1">
<svg class="align-top"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 16 16"
width="16"
height="16">
<path d="M13.485 1.4a.5.5 0 0 1 .035.71L6.543 9.793 3.379 6.93a.5.5 0 1 1 .708-.707l2.1 2.1 6.514-7.1a.5.5 0 0 1 .707-.035z" />
</svg>
yashikakhurana marked this conversation as resolved.
Show resolved Hide resolved
All set! Your experiment is in Preview mode and you can test it now.
</p>
</div>
<div class="alert alert-secondary">
<form method="post"
action="{% url 'nimbus-new-launch-preview-to-review' slug=experiment.slug %}"
hx-post="{% url 'nimbus-new-launch-preview-to-review' slug=experiment.slug %}"
hx-target="#launch-controls"
hx-swap="outerHTML"
id="preview-controls-form">
{% csrf_token %}
<p class="my-1">
This experiment is currently <strong>live for testing</strong>, but you will need to let QA know in your
<a href="{{ EXTERNAL_URLS.SIGNOFF_QA }}" target="_blank">PI request</a>. When you have received a sign-off, click “Request Launch” to launch the experiment.
<strong>Note: It can take up to an hour before clients receive a preview experiment.</strong>
</p>
<div class="form-check">
<input type="checkbox"
class="form-check-input"
id="checkbox-1"
onchange="toggleSubmitButton()">
<label class="form-check-label" for="checkbox-1">I understand the risks associated with launching an experiment</label>
</div>
<div class="form-check">
<input type="checkbox"
class="form-check-input"
id="checkbox-2"
onchange="toggleSubmitButton()">
<label class="form-check-label" for="checkbox-2">
I have gone through the <a href="{{ EXTERNAL_URLS.TRAINING_AND_PLANNING_DOC }}" target="_blank">experiment onboarding program</a>
</label>
</div>
<div class="d-flex bd-highlight py-2">
<button type="submit"
class="btn btn-primary mx-3"
id="request-launch-button"
hx-post="{% url 'nimbus-new-launch-preview-to-review' slug=experiment.slug %}"
hx-target="#experiment-container"
hx-swap="innerHTML"
disabled>Request Launch</button>
<button type="button"
class="btn btn-secondary"
hx-post="{% url 'nimbus-new-launch-preview-to-draft' slug=experiment.slug %}"
hx-target="#experiment-container"
hx-swap="innerHTML">Go Back to Draft</button>
</div>
</form>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{# templates/nimbus_experiments/launch_without_preview_controls.html #}
<div class="alert alert-warning">
<form method="post"
action="{% url 'nimbus-new-launch-to-preview' slug=experiment.slug %}"
hx-post="{% url 'nimbus-new-launch-to-preview' slug=experiment.slug %}"
hx-target="#experiment-container"
hx-swap="innerHTML">
{% csrf_token %}
<span class="text-danger">We recommend previewing before launch</span>
<button type="submit"
class="btn btn-primary"
{% if is_loading %}disabled{% endif %}>Preview for Testing</button>
</form>
</div>
<form method="post"
action="{% url 'nimbus-new-launch-preview-to-review' slug=experiment.slug %}"
hx-post="{% url 'nimbus-new-launch-preview-to-review' slug=experiment.slug %}"
hx-target="#experiment-container"
hx-swap="innerHTML">
{% csrf_token %}
<div class="form-check">
<input type="checkbox"
class="form-check-input"
id="checkbox-1"
onchange="toggleSubmitButton()">
<label class="form-check-label" for="checkbox-1">I understand the risks associated with launching an experiment</label>
</div>
<div class="form-check">
<input type="checkbox"
class="form-check-input"
id="checkbox-2"
onchange="toggleSubmitButton()">
<label class="form-check-label" for="checkbox-2">
I have gone through the <a href="{{ EXTERNAL_URLS.TRAINING_AND_PLANNING_DOC }}" target="_blank">experiment onboarding program</a>
</label>
</div>
<button type="submit"
class="btn btn-primary my-2"
id="request-launch-button"
disabled>Request Launch</button>
<button type="button"
class="btn btn-secondary"
hx-post="{% url 'nimbus-new-launch-preview-to-draft' slug=experiment.slug %}"
hx-target="#experiment-container"
hx-swap="innerHTML">Cancel</button>
</form>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{# templates/nimbus_experiments/review_controls.html #}
<form method="post"
action="{% url 'nimbus-new-cancel-review' slug=experiment.slug %}"
hx-post="{% url 'nimbus-new-cancel-review' slug=experiment.slug %}"
hx-target="#experiment-container"
hx-swap="innerHTML">
{% csrf_token %}
<div class="alert alert-warning">
<p>The experiment is currently under review. If you wish to cancel the review, click the button below:</p>
<button type="submit" class="btn btn-danger">Cancel Review</button>
</div>
</form>
Loading