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:
Yashika Khurana 2024-10-08 02:27:16 +05:30 коммит произвёл GitHub
Родитель 1a691ddc38
Коммит c6bc8e47df
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
4 изменённых файлов: 200 добавлений и 11 удалений

Просмотреть файл

@ -612,10 +612,55 @@ class NimbusExperiment(NimbusConstants, TargetingConstants, FilterMixin, models.
branches = branches.exclude(id=self.reference_branch.id)
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
def is_started(self):
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
def start_date(self):
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.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
def should_end(self):
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):
experiment = NimbusExperimentFactory.create(
slug="experiment",

Просмотреть файл

@ -8,18 +8,13 @@
{% block main_content %}
<div class="container-fluid">
<!-- Experiment Details Card -->
<div class="card mb-3">
<div class="card-header">
<h4>Experiment Details</h4>
</div>
<div class="card-body">
<p>
<strong>Slug:</strong> {{ experiment.slug }}
</p>
<p>
<strong>Name:</strong> {{ experiment.name }}
</p>
<div class="row">
<div class="col-6">
<h4 class="mb-0">{{ experiment.name }}</h4>
<p class="text-secondary">{{ experiment.slug }}</p>
</div>
{% include "nimbus_experiments/timeline.html" %}
</div>
<!-- Takeaways Card -->
{% 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>