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 all 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
24 changes: 20 additions & 4 deletions experimenter/experimenter/experiments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,10 +658,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 All @@ -683,6 +683,22 @@ 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_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():
Expand Down
63 changes: 63 additions & 0 deletions experimenter/experimenter/nimbus_ui_new/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,3 +518,66 @@ def save(self, commit=True):

def get_changelog_message(self):
return f"{self.request.user} removed subscriber"


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 = self.status
experiment.status_next = self.status_next
experiment.publish_status = self.publish_status
experiment.save()
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 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 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 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 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"
14 changes: 14 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,14 @@
window.showRecommendation = function () {
const defaultControls = document.getElementById("default-controls");
const recommendationMessage = document.getElementById(
"recommendation-message",
);
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);
};
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",
edit_audience: "./js/edit_audience.js",
},
output: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
</div>
{% endfor %}
{% endif %}
<!-- Launch Controls Card-->
{% include "nimbus_experiments/launch_controls.html" %}

<!-- Takeaways Card -->
{% include "nimbus_experiments/takeaways_card.html" %}

Expand Down Expand Up @@ -363,4 +366,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
Expand Up @@ -6,8 +6,9 @@
{% endblock %}

{% block main_content_header %}
<div class="row mb-2">
<div class="col-md-12 col-xl-6">
<div class="row mb-2 align-items-center">
<!-- Experiment Details -->
<div class="col-md-6 col-xl-6">
<h4 class="mb-0">{{ experiment.name }}</h4>
<span class="{{ experiment.qa_status_badge_class }}">
QA Status: {{ experiment.qa_status|default:"Not Set"|title }}
Expand All @@ -22,15 +23,10 @@ <h4 class="mb-0">{{ experiment.name }}</h4>
</p>
{% endif %}
</div>
<div class="col-md-12 col-xl-6">
<ul class="list-group list-group-horizontal justify-content-between mb-3">
{% for status in experiment.timeline %}
<li class="list-group-item flex-fill text-center d-flex flex-column justify-content-center {% if status.is_active %}bg-primary text-white{% endif %}">
<strong>{{ status.label }}</strong>
<small>{{ status.date|default:'---' }}</small>
</li>
{% endfor %}
</ul>
<!-- Experiment Timeline -->
<div class="col-md-6 col-xl-6 text-end" id="experiment-timeline">
{% include "nimbus_experiments/timeline.html" %}

</div>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<div id="launch-controls">
<form>
{% csrf_token %}
<!-- Draft Mode Controls -->
{% if experiment.is_draft %}
<div id="default-controls" 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>
{% if experiment.can_draft_to_preview %}
<button type="button"
class="btn btn-primary"
hx-post="{% url 'nimbus-new-draft-to-preview' slug=experiment.slug %}"
hx-select="#content"
hx-target="#content"
hx-swap="outerHTML">Preview for Testing</button>
{% endif %}
{% if experiment.can_draft_to_review %}
<button type="button"
class="btn btn-secondary"
onclick="showRecommendation()">Request Launch without Preview</button>
{% endif %}
</div>
<!-- Recommendation Message -->
<div id="recommendation-message" class="d-none">
<div class="alert alert-warning">
<p>
<strong>We recommend previewing before launch</strong>
<button type="button"
class="btn btn-primary"
hx-post="{% url 'nimbus-new-draft-to-preview' slug=experiment.slug %}"
hx-select="#content"
hx-target="#content"
hx-swap="outerHTML">Preview Now</button>
</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>
<button type="button"
class="btn btn-primary"
id="request-launch-button"
hx-post="{% url 'nimbus-new-draft-to-review' slug=experiment.slug %}"
hx-select="#content"
hx-target="#content"
hx-swap="outerHTML"
disabled>Request Launch</button>
<button type="button"
class="btn btn-secondary"
hx-post="{% url 'nimbus-new-review-to-draft' slug=experiment.slug %}"
hx-select="#content"
hx-target="#content"
hx-swap="outerHTML">Cancel</button>
</div>
</div>
<!-- Preview Mode Controls -->
{% elif experiment.is_preview %}
<div class="alert alert-success bg-transparent text-success">
<p class="my-1">All set! Your experiment is in Preview mode and you can test it now.</p>
</div>
<div class="alert alert-secondary">
<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>
{% if experiment.can_preview_to_review %}
<button type="button"
class="btn btn-primary"
id="request-launch-button"
hx-post="{% url 'nimbus-new-preview-to-review' slug=experiment.slug %}"
hx-select="#content"
hx-target="#content"
hx-swap="outerHTML"
disabled>Request Launch</button>
{% endif %}
{% if experiment.can_preview_to_draft %}
<button type="button"
class="btn btn-secondary"
hx-post="{% url 'nimbus-new-preview-to-draft' slug=experiment.slug %}"
hx-select="#content"
hx-target="#content"
hx-swap="outerHTML">Go back to Draft</button>
{% endif %}
</div>
<!-- Review Mode Controls -->
{% elif experiment.is_review %}
<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="button"
class="btn btn-primary"
hx-post="{% url 'nimbus-new-review-to-draft' slug=experiment.slug %}"
hx-select="#content"
hx-target="#content"
hx-swap="outerHTML">Cancel Review</button>
</div>
{% endif %}
</form>
</div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="col-7">
<div class="col-6">
<ul class="list-group list-group-horizontal justify-content-between mb-3">
{% for status in experiment.timeline %}
<li class="list-group-item flex-fill text-center d-flex flex-column justify-content-center {% if status.is_active %}bg-primary text-white{% endif %}">
Expand Down
Loading