Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion experimenter/experimenter/experiments/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.template.loader import render_to_string
from django.utils.encoding import force_str
from django.utils.safestring import mark_safe
from django_summernote.admin import SummernoteModelAdmin
from import_export import fields, resources
from import_export.admin import ExportActionMixin, ImportMixin
from import_export.widgets import DecimalWidget, ForeignKeyWidget
Expand Down Expand Up @@ -319,7 +320,11 @@ class Meta:


class NimbusExperimentAdmin(
NoDeleteAdminMixin, ImportMixin, ExportActionMixin, admin.ModelAdmin[NimbusExperiment]
NoDeleteAdminMixin,
ImportMixin,
ExportActionMixin,
SummernoteModelAdmin,
admin.ModelAdmin[NimbusExperiment],
):
inlines = (
NimbusDocumentationLinkInlineAdmin,
Expand Down Expand Up @@ -351,6 +356,7 @@ class NimbusExperimentAdmin(
actions = [force_fetch_jetstream_data]
resource_class = NimbusExperimentResource
readonly_fields = ("_firefox_min_version_parsed", "changelog_display")
summernote_fields = ("takeaways_summary", "next_steps")

@admin.display(description="Change History")
def changelog_display(self, obj):
Expand Down
5 changes: 5 additions & 0 deletions experimenter/experimenter/experiments/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,11 @@ class Takeaways(models.TextChoices):

CHANGELOG_MESSAGE_ADMIN_EDIT = "Modified by an administrator."

class ProjectImpact(models.TextChoices):
HIGH = "HIGH"
MODERATE = "MODERATE"
TARGETED = "TARGETED"

class QAStatus(models.TextChoices):
RED = "RED", "QA: Red"
YELLOW = "YELLOW", "QA: Yellow"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.2.9 on 2025-12-18 17:43

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('experiments', '0306_remove_nimbusexperiment_qa_run_test_plan_and_more'),
]

operations = [
migrations.AddField(
model_name='nimbusexperiment',
name='next_steps',
field=models.TextField(blank=True, null=True, verbose_name='Next Steps'),
),
migrations.AddField(
model_name='nimbusexperiment',
name='project_impact',
field=models.CharField(blank=True, choices=[('HIGH', 'High'), ('MODERATE', 'Moderate'), ('TARGETED', 'Targeted')], default=None, max_length=255, null=True, verbose_name='Project Impact'),
),
]
11 changes: 11 additions & 0 deletions experimenter/experimenter/experiments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,15 @@ class NimbusExperiment(NimbusConstants, TargetingConstants, FilterMixin, models.
"Takeaways QBR Learning", default=False, blank=False, null=False
)
takeaways_summary = models.TextField("Takeaways Summary", blank=True, null=True)
next_steps = models.TextField("Next Steps", blank=True, null=True)
project_impact = models.CharField(
"Project Impact",
max_length=255,
blank=True,
null=True,
default=None,
choices=NimbusConstants.ProjectImpact.choices,
)
is_first_run = models.BooleanField("Is First Run Flag", default=False)
is_client_schema_disabled = models.BooleanField(
"Is Client Schema Disabled Flag", default=False
Expand Down Expand Up @@ -2255,6 +2264,8 @@ def clone(self, name, user, rollout_branch_slug=None, changed_on=None):
cloned.published_date = None
cloned.results_data = None
cloned.takeaways_summary = None
cloned.next_steps = None
cloned.project_impact = None
cloned.conclusion_recommendations = []
cloned.takeaways_metric_gain = False
cloned.takeaways_gain_amount = None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,13 @@ def test_outputs_expected_schema_for_empty_experiment(self):
"locales": [],
"localizations": None,
"name": "",
"next_steps": None,
"owner": owner.email,
"parent": None,
"population_percent": "0.0000",
"prevent_pref_conflicts": False,
"primary_outcomes": [],
"project_impact": None,
"projects": [],
"proposed_duration": NimbusExperiment.DEFAULT_PROPOSED_DURATION,
"proposed_enrollment": NimbusExperiment.DEFAULT_PROPOSED_ENROLLMENT,
Expand Down Expand Up @@ -216,11 +218,13 @@ def test_outputs_expected_schema_for_complete_experiment(self):
"legal_signoff": False,
"localizations": experiment.localizations,
"name": experiment.name,
"next_steps": None,
"owner": experiment.owner.email,
"parent": parent_experiment.slug,
"population_percent": str(experiment.population_percent),
"prevent_pref_conflicts": False,
"primary_outcomes": [primary_outcome],
"project_impact": None,
"projects": [project.slug],
"proposed_duration": experiment.proposed_duration,
"proposed_enrollment": experiment.proposed_enrollment,
Expand Down
28 changes: 28 additions & 0 deletions experimenter/experimenter/nimbus_ui/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from enum import Enum

from experimenter.experiments.constants import NimbusConstants


class NimbusUIConstants:
HYPOTHESIS_PLACEHOLDER = """
Expand Down Expand Up @@ -178,6 +180,32 @@ class NimbusUIConstants:
"for the selected versions."
)

KEY_TAKEAWAYS_EMPTY_TEXT = """Was your hypothesis right, wrong, or somewhere in
between? Call out what changed in a meaningful way (ideally things that were
statistically significant)."""

NEXT_STEPS_EMPTY_TEXT = """Ship, stop, or iterate? If shipping, how do we roll it
out? If iterating, what signals suggest it's worth more testing?"""

PROJECT_IMPACT_EMPTY_TEXT = """Rate the overall business impact of this project. Did
it shift key company metrics, or did it mainly deliver targeted learnings? Your
rating helps teams filter, compare, and learn from experiments across the org."""

PROJECT_IMPACT_SUBTITLES = {
NimbusConstants.ProjectImpact.HIGH: (
"Moved key company metrics or made visible progress toward "
"broader goals. Worth sharing across teams or with leadership."
),
NimbusConstants.ProjectImpact.MODERATE: (
"Provided meaningful progress or insights, with impact more "
"localized than company-wide."
),
NimbusConstants.ProjectImpact.TARGETED: (
"Met intended goals and delivered useful learnings, though "
"it didn't shift broader metrics."
),
}
Comment on lines +194 to +207
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be you just want to keep this and remove the other one


class ReviewRequestMessages(Enum):
END_EXPERIMENT = "end this experiment"
END_ENROLLMENT = "end enrollment for this experiment"
Expand Down
19 changes: 19 additions & 0 deletions experimenter/experimenter/nimbus_ui/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from django.urls import reverse
from django.utils import timezone
from django.utils.text import slugify
from django_summernote.widgets import SummernoteWidget

from experimenter.base.models import Country, Language, Locale
from experimenter.experiments.changelog_utils import generate_nimbus_changelog
Expand Down Expand Up @@ -1815,6 +1816,24 @@ def get_changelog_message(self):
return f"{self.request.user} updated collaborators"


class EditOutcomeSummaryForm(NimbusChangeLogFormMixin, forms.ModelForm):
takeaways_summary = forms.CharField(required=False, widget=SummernoteWidget())
next_steps = forms.CharField(required=False, widget=SummernoteWidget())
project_impact = forms.ChoiceField(
required=False,
choices=NimbusExperiment.ProjectImpact.choices,
widget=forms.RadioSelect,
label="Project Impact",
)

class Meta:
model = NimbusExperiment
fields = ["takeaways_summary", "next_steps", "project_impact"]

def get_changelog_message(self):
return f"{self.request.user} updated outcome summary"


class BranchLeadingScreenshotForm(NimbusChangeLogFormMixin, forms.ModelForm):
image = forms.ImageField(
required=False,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<form class="accordion"
method="post"
hx-post="{% url 'nimbus-ui-edit-outcome-summary' slug=experiment.slug %}"
id="edit-summary-form-{{ experiment.slug }}">
{% csrf_token %}
<div class="accordion-item border border-1 rounded-4 mt-4 py-4 px-5">
<button class="accordion-button shadow-none bg-transparent text-body"
type="button"
data-bs-toggle="collapse"
data-bs-target="#key-takeaways-{{ experiment_slug }}-{{ branch.slug }}"
aria-expanded="true"
aria-controls="key-takeaways-{{ experiment_slug }}-{{ branch.slug }}">
<div class="d-flex flex-column align-items-start">
<div class="d-flex align-items-center gap-2 mb-2">
<h5 class="mb-0">Key takeaways</h5>
{% if not experiment.takeaways_summary %}
<span class="badge rounded-pill text-bg-warning bg-opacity-25 border border-secondary-subtle">Incomplete</span>
{% endif %}
</div>
<small class="text-muted">{{ NimbusUIConstants.KEY_TAKEAWAYS_EMPTY_TEXT }}</small>
</div>
</button>
<div id="key-takeaways-{{ experiment_slug }}-{{ branch.slug }}"
class="accordion-collapse collapse show">
<div class="accordion-body">{{ edit_outcome_summary_form.takeaways_summary }}</div>
</div>
</div>
<div class="accordion-item border border-1 rounded-4 mt-4 py-4 px-5">
<button class="accordion-button shadow-none bg-transparent text-body"
type="button"
data-bs-toggle="collapse"
data-bs-target="#next-steps-{{ experiment_slug }}-{{ branch.slug }}"
aria-expanded="true"
aria-controls="next-steps-{{ experiment_slug }}-{{ branch.slug }}">
<div class="d-flex flex-column align-items-start">
<div class="d-flex align-items-center gap-2 mb-2">
<h5 class="mb-0">Next Steps</h5>
{% if not experiment.next_steps %}
<span class="badge rounded-pill text-bg-warning bg-opacity-25 border border-secondary-subtle">Incomplete</span>
{% endif %}
</div>
<small class="text-muted">{{ NimbusUIConstants.NEXT_STEPS_EMPTY_TEXT }}</small>
</div>
</button>
<div id="next-steps-{{ experiment_slug }}-{{ branch.slug }}"
class="accordion-collapse collapse show">
<div class="accordion-body">{{ edit_outcome_summary_form.next_steps }}</div>
</div>
</div>
<div class="accordion-item border border-1 rounded-4 mt-4 py-4 px-5">
<button class="accordion-button shadow-none bg-transparent text-body"
type="button"
data-bs-toggle="collapse"
data-bs-target="#project-impact-{{ experiment_slug }}-{{ branch.slug }}"
aria-expanded="true"
aria-controls="project-impact-{{ experiment_slug }}-{{ branch.slug }}">
<div class="d-flex flex-column align-items-start">
<div class="d-flex align-items-center gap-2 mb-2">
<h5 class="mb-0">Project Impact</h5>
{% if not experiment.project_impact %}
<span class="badge rounded-pill text-bg-warning bg-opacity-25 border border-secondary-subtle">Incomplete</span>
{% endif %}
</div>
<small class="text-muted">{{ NimbusUIConstants.PROJECT_IMPACT_EMPTY_TEXT }}</small>
</div>
</button>
<div id="project-impact-{{ experiment_slug }}-{{ branch.slug }}"
class="accordion-collapse collapse show">
<div class="accordion-body d-flex flex-column gap-4">
{% for radio in edit_outcome_summary_form.project_impact %}
<div class="project-impact-option {{ key }}">
<label class="d-flex gap-3 align-items-center">
<div style="transform: scale(1.75)">{{ radio.tag }}</div>
<div class="ms-2">
<div>{{ radio.choice_label }} impact</div>
<small class="option-subtitle text-muted">
{% for key, subtitle in NimbusUIConstants.PROJECT_IMPACT_SUBTITLES.items %}
{% if key == radio.choice_label|upper %}{{ subtitle }}{% endif %}
{% endfor %}
</small>
</div>
</label>
</div>
{% endfor %}
</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="submit"
class="btn btn-primary"
form="edit-summary-form-{{ experiment.slug }}">Save changes</button>
</div>
</form>
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{% extends "common/with_sidebar.html" %}

{% load nimbus_extras %}

{% block sidebar %}
{% include "nimbus_experiments/sidebar.html" with experiment=experiment %}

Expand All @@ -14,6 +16,9 @@ <h4 class="mb-0">{{ experiment.name }}</h4>
<span class="badge rounded-pill bg-danger" id="archive-badge">Archived</span>
{% endif %}
{% if experiment.is_rollout %}<span class="badge rounded-pill bg-primary" id="rollout-badge">Rollout</span>{% endif %}
{% if experiment.project_impact %}
<span class="badge rounded-pill text-bg-warning border border-secondary-subtle"><i class="fa-solid fa-trophy me-2"></i>{{ experiment.project_impact|lower|capfirst }} impact</span>
{% endif %}
<span id="qa-status-header" class="{{ experiment.qa_status_badge_class }}">
QA Status: {{ experiment.qa_status|default:"Not Set"|title }}
</span>
Expand Down
Loading