feat(nimbus): Summary page timeline (#11393)
Because - We want to show the timeline on the new summary page This commit - Adds new status timeline Note: On the same timeline, in the next PR, I will add the functionality to go back from preview to draft or preview to launch Fixes #11361 <img width="1589" alt="Screenshot 2024-09-19 at 5 15 39 PM" src="https://github.com/user-attachments/assets/60532b4f-b305-4c3a-ade0-5b22bbce53c6"> <img width="1589" alt="Screenshot 2024-09-19 at 5 10 35 PM" src="https://github.com/user-attachments/assets/ea32031f-0455-415b-8a26-46dd4986ee37"> <img width="1589" alt="Screenshot 2024-09-19 at 5 09 49 PM" src="https://github.com/user-attachments/assets/cdab6df7-ed3b-442b-bed8-2f921e3a0a89"> <img width="1589" alt="Screenshot 2024-09-19 at 5 08 56 PM" src="https://github.com/user-attachments/assets/58898acc-047a-498f-ab82-c0b2b661566f"> Future design ![image (7)](https://github.com/user-attachments/assets/ce31d166-9dc9-4dba-a754-76f0293a58eb)
This commit is contained in:
Родитель
1a691ddc38
Коммит
c6bc8e47df
|
@ -612,10 +612,55 @@ class NimbusExperiment(NimbusConstants, TargetingConstants, FilterMixin, models.
|
||||||
branches = branches.exclude(id=self.reference_branch.id)
|
branches = branches.exclude(id=self.reference_branch.id)
|
||||||
return list(branches)
|
return list(branches)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_draft(self):
|
||||||
|
return self.status == self.Status.DRAFT
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_review(self):
|
||||||
|
return self.is_draft and self.publish_status == self.PublishStatus.REVIEW
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_preview(self):
|
||||||
|
return self.status == self.Status.PREVIEW
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_live(self):
|
||||||
|
return self.status == self.Status.LIVE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_complete(self):
|
||||||
|
return self.status == self.Status.COMPLETE
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_started(self):
|
def is_started(self):
|
||||||
return self.status in (self.Status.LIVE, self.Status.COMPLETE)
|
return self.status in (self.Status.LIVE, self.Status.COMPLETE)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def draft_date(self):
|
||||||
|
if change := self.changes.all().order_by("changed_on").first():
|
||||||
|
return change.changed_on.date()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def preview_date(self):
|
||||||
|
if change := (
|
||||||
|
self.changes.filter(new_status=self.Status.PREVIEW)
|
||||||
|
.order_by("changed_on")
|
||||||
|
.first()
|
||||||
|
):
|
||||||
|
return change.changed_on.date()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def review_date(self):
|
||||||
|
if change := (
|
||||||
|
self.changes.filter(
|
||||||
|
new_status=self.Status.DRAFT, new_publish_status=self.PublishStatus.REVIEW
|
||||||
|
)
|
||||||
|
.order_by("changed_on")
|
||||||
|
.first()
|
||||||
|
):
|
||||||
|
return change.changed_on.date()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def start_date(self):
|
def start_date(self):
|
||||||
if self._start_date is not None:
|
if self._start_date is not None:
|
||||||
|
@ -747,6 +792,35 @@ class NimbusExperiment(NimbusConstants, TargetingConstants, FilterMixin, models.
|
||||||
return (self.computed_end_date - self.enrollment_start_date).days
|
return (self.computed_end_date - self.enrollment_start_date).days
|
||||||
return self.proposed_duration
|
return self.proposed_duration
|
||||||
|
|
||||||
|
def timeline(self):
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"label": self.Status.DRAFT,
|
||||||
|
"date": self.draft_date,
|
||||||
|
"is_active": self.is_draft,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": self.Status.PREVIEW,
|
||||||
|
"date": self.preview_date,
|
||||||
|
"is_active": self.is_preview,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": self.PublishStatus.REVIEW,
|
||||||
|
"date": self.review_date,
|
||||||
|
"is_active": self.is_review,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": self.Status.LIVE,
|
||||||
|
"date": self.start_date,
|
||||||
|
"is_active": self.is_live,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": self.Status.COMPLETE,
|
||||||
|
"date": self.computed_end_date,
|
||||||
|
"is_active": self.is_complete,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_end(self):
|
def should_end(self):
|
||||||
if self.proposed_end_date:
|
if self.proposed_end_date:
|
||||||
|
|
|
@ -1640,6 +1640,116 @@ class TestNimbusExperiment(TestCase):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_draft_date_uses_first_changelog_if_no_start_date(self):
|
||||||
|
experiment = NimbusExperimentFactory.create(_start_date=None)
|
||||||
|
first_changelog = NimbusChangeLogFactory.create(
|
||||||
|
experiment=experiment, changed_on=datetime.datetime(2023, 2, 1)
|
||||||
|
)
|
||||||
|
self.assertEqual(experiment.draft_date, first_changelog.changed_on.date())
|
||||||
|
|
||||||
|
def test_preview_date_returns_first_preview_change(self):
|
||||||
|
experiment = NimbusExperimentFactory.create()
|
||||||
|
preview_change = NimbusChangeLogFactory.create(
|
||||||
|
experiment=experiment,
|
||||||
|
old_status=NimbusExperiment.Status.DRAFT,
|
||||||
|
new_status=NimbusExperiment.Status.PREVIEW,
|
||||||
|
changed_on=datetime.datetime(2023, 3, 1),
|
||||||
|
)
|
||||||
|
self.assertEqual(experiment.preview_date, preview_change.changed_on.date())
|
||||||
|
|
||||||
|
def test_preview_date_returns_none_if_no_preview_status(self):
|
||||||
|
experiment = NimbusExperimentFactory.create()
|
||||||
|
NimbusChangeLogFactory.create(
|
||||||
|
experiment=experiment,
|
||||||
|
old_status=NimbusExperiment.Status.DRAFT,
|
||||||
|
new_status=NimbusExperiment.Status.DRAFT,
|
||||||
|
changed_on=datetime.datetime(2023, 4, 1),
|
||||||
|
)
|
||||||
|
self.assertIsNone(experiment.preview_date)
|
||||||
|
|
||||||
|
def test_review_date_returns_first_review_change(self):
|
||||||
|
experiment = NimbusExperimentFactory.create()
|
||||||
|
review_change = NimbusChangeLogFactory.create(
|
||||||
|
experiment=experiment,
|
||||||
|
old_publish_status=NimbusExperiment.Status.PREVIEW,
|
||||||
|
new_publish_status=NimbusExperiment.PublishStatus.REVIEW,
|
||||||
|
changed_on=datetime.datetime(2023, 5, 1),
|
||||||
|
)
|
||||||
|
self.assertEqual(experiment.review_date, review_change.changed_on.date())
|
||||||
|
|
||||||
|
def test_review_date_returns_none_if_no_review_status(self):
|
||||||
|
experiment = NimbusExperimentFactory.create()
|
||||||
|
NimbusChangeLogFactory.create(
|
||||||
|
experiment=experiment,
|
||||||
|
old_publish_status=NimbusExperiment.Status.DRAFT,
|
||||||
|
new_publish_status=NimbusExperiment.Status.PREVIEW,
|
||||||
|
changed_on=datetime.datetime(2023, 6, 1),
|
||||||
|
)
|
||||||
|
self.assertIsNone(experiment.review_date)
|
||||||
|
|
||||||
|
def test_timeline_dates_includes_correct_status_dates_and_flags(self):
|
||||||
|
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||||
|
lifecycle=NimbusExperimentFactory.Lifecycles.LIVE_APPROVE,
|
||||||
|
)
|
||||||
|
NimbusChangeLogFactory.create(
|
||||||
|
experiment=experiment,
|
||||||
|
new_status=NimbusExperiment.Status.DRAFT,
|
||||||
|
changed_on=datetime.datetime(2023, 1, 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
NimbusChangeLogFactory.create(
|
||||||
|
experiment=experiment,
|
||||||
|
old_status=NimbusExperiment.Status.DRAFT,
|
||||||
|
new_status=NimbusExperiment.Status.PREVIEW,
|
||||||
|
changed_on=datetime.datetime(2023, 3, 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
NimbusChangeLogFactory.create(
|
||||||
|
experiment=experiment,
|
||||||
|
old_publish_status=NimbusExperiment.Status.PREVIEW,
|
||||||
|
new_publish_status=NimbusExperiment.PublishStatus.REVIEW,
|
||||||
|
changed_on=datetime.datetime(2023, 4, 1),
|
||||||
|
)
|
||||||
|
timeline = experiment.timeline()
|
||||||
|
expected_timeline = [
|
||||||
|
{
|
||||||
|
"label": "Draft",
|
||||||
|
"date": experiment.draft_date,
|
||||||
|
"is_active": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Preview",
|
||||||
|
"date": experiment.preview_date,
|
||||||
|
"is_active": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Review",
|
||||||
|
"date": experiment.review_date,
|
||||||
|
"is_active": False,
|
||||||
|
},
|
||||||
|
{"label": "Live", "date": experiment.start_date, "is_active": True},
|
||||||
|
{
|
||||||
|
"label": "Complete",
|
||||||
|
"date": experiment.computed_end_date,
|
||||||
|
"is_active": False,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
for i, expected in enumerate(expected_timeline):
|
||||||
|
self.assertEqual(timeline[i]["label"], expected["label"])
|
||||||
|
self.assertEqual(timeline[i]["date"], expected["date"])
|
||||||
|
self.assertEqual(timeline[i]["is_active"], expected["is_active"])
|
||||||
|
|
||||||
|
def test_timeline_dates_complete_is_active_when_status_is_complete(self):
|
||||||
|
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||||
|
lifecycle=NimbusExperimentFactory.Lifecycles.ENDING_APPROVE_APPROVE,
|
||||||
|
end_date=datetime.date(2023, 7, 1),
|
||||||
|
)
|
||||||
|
timeline = experiment.timeline()
|
||||||
|
self.assertTrue(
|
||||||
|
timeline[-1]["is_active"]
|
||||||
|
) # Check if the last status "Complete" is active
|
||||||
|
self.assertEqual(timeline[-1]["date"], experiment.end_date)
|
||||||
|
|
||||||
def test_monitoring_dashboard_url_is_valid_when_experiment_not_begun(self):
|
def test_monitoring_dashboard_url_is_valid_when_experiment_not_begun(self):
|
||||||
experiment = NimbusExperimentFactory.create(
|
experiment = NimbusExperimentFactory.create(
|
||||||
slug="experiment",
|
slug="experiment",
|
||||||
|
|
|
@ -8,18 +8,13 @@
|
||||||
{% block main_content %}
|
{% block main_content %}
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<!-- Experiment Details Card -->
|
<!-- Experiment Details Card -->
|
||||||
<div class="card mb-3">
|
<div class="row">
|
||||||
<div class="card-header">
|
<div class="col-6">
|
||||||
<h4>Experiment Details</h4>
|
<h4 class="mb-0">{{ experiment.name }}</h4>
|
||||||
</div>
|
<p class="text-secondary">{{ experiment.slug }}</p>
|
||||||
<div class="card-body">
|
|
||||||
<p>
|
|
||||||
<strong>Slug:</strong> {{ experiment.slug }}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Name:</strong> {{ experiment.name }}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% include "nimbus_experiments/timeline.html" %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Takeaways Card -->
|
<!-- Takeaways Card -->
|
||||||
{% include "nimbus_experiments/takeaways_card.html" %}
|
{% include "nimbus_experiments/takeaways_card.html" %}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
<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 %}">
|
||||||
|
<strong>{{ status.label }}</strong>
|
||||||
|
<small>{{ status.date|default:'---' }}</small>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
Загрузка…
Ссылка в новой задаче