From c8416805841fe9037a091613b0ca537fa06287c1 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Thu, 19 Sep 2024 18:10:31 +0530 Subject: [PATCH 1/9] feat(nimbus): Summary page timeline --- .../experimenter/experiments/models.py | 17 ++++++++++++ .../experiments/tests/test_models.py | 27 +++++++++++++++++++ .../templates/nimbus_experiments/detail.html | 2 ++ .../nimbus_experiments/timeline.html | 25 +++++++++++++++++ 4 files changed, 71 insertions(+) create mode 100644 experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html diff --git a/experimenter/experimenter/experiments/models.py b/experimenter/experimenter/experiments/models.py index 6aae015a1b..2313cc2cd9 100644 --- a/experimenter/experimenter/experiments/models.py +++ b/experimenter/experimenter/experiments/models.py @@ -1240,6 +1240,23 @@ def recipe_json(self): .replace("\\n", "\n") # Handle hard coded newlines in targeting ) + @property + def draft_date(self): + first_changelog_entry = self.changes.order_by("changed_on").first() + return first_changelog_entry.changed_on.date() if first_changelog_entry else None + + @property + def preview_date(self): + """ + Finds the first changelog entry where the status changed to 'PREVIEW'. + """ + preview_changelog = ( + self.changes.filter(new_status=self.Status.PREVIEW) + .order_by("changed_on") + .first() + ) + return preview_changelog.changed_on.date() if preview_changelog else None + class NimbusBranch(models.Model): experiment = models.ForeignKey( diff --git a/experimenter/experimenter/experiments/tests/test_models.py b/experimenter/experimenter/experiments/tests/test_models.py index 66c3aaf5e0..6aeb2c117b 100644 --- a/experimenter/experimenter/experiments/tests/test_models.py +++ b/experimenter/experimenter/experiments/tests/test_models.py @@ -1640,6 +1640,33 @@ def test_computed_end_date_returns_proposed_with_actual_enrollment_duration(self ), ) + 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_monitoring_dashboard_url_is_valid_when_experiment_not_begun(self): experiment = NimbusExperimentFactory.create( slug="experiment", diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html index 9bdd9bf0e5..1e23a5d949 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html @@ -18,6 +18,8 @@

Experiment Details

+ {% include "nimbus_experiments/timeline.html" %} +

Slug: {{ experiment.slug }}

diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html new file mode 100644 index 0000000000..0689427489 --- /dev/null +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html @@ -0,0 +1,25 @@ +
+ +
+ +
+
Draft
+
{{ experiment.draft_date|default:'---' }}
+
+ +
+
Preview
+
{{ experiment.preview_date|default:'---' }}
+
+ +
+
Live
+
{{ experiment.start_date|default:'---' }}
+
+ +
+
Complete
+
{{ experiment.end_date|default:'---' }}
+
+
+
From 9d99f51e215d46cefd0ad3852d35f719c4db53e2 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Thu, 19 Sep 2024 18:14:21 +0530 Subject: [PATCH 2/9] feat(nimbus): Summary page timeline --- experimenter/experimenter/experiments/models.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/experimenter/experimenter/experiments/models.py b/experimenter/experimenter/experiments/models.py index 2313cc2cd9..ad1cebe5e3 100644 --- a/experimenter/experimenter/experiments/models.py +++ b/experimenter/experimenter/experiments/models.py @@ -1247,9 +1247,6 @@ def draft_date(self): @property def preview_date(self): - """ - Finds the first changelog entry where the status changed to 'PREVIEW'. - """ preview_changelog = ( self.changes.filter(new_status=self.Status.PREVIEW) .order_by("changed_on") From 0f4d2ac84c13a6af7c360826abf333eef9cd7d0c Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Mon, 23 Sep 2024 20:06:12 +0530 Subject: [PATCH 3/9] feat(nimbus): Update headings --- .../templates/nimbus_experiments/timeline.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html index 0689427489..03f0305992 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html @@ -3,22 +3,22 @@
-
Draft
+
Draft
{{ experiment.draft_date|default:'---' }}
-
Preview
+
Preview
{{ experiment.preview_date|default:'---' }}
-
Live
+
Live
{{ experiment.start_date|default:'---' }}
-
Complete
+
Complete
{{ experiment.end_date|default:'---' }}
From d09b1d4793da99df3b52dac3fea8073663390d48 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Tue, 24 Sep 2024 19:33:04 +0530 Subject: [PATCH 4/9] feat(nimbus): Review status --- .../experimenter/experiments/models.py | 23 +++++++--- .../experiments/tests/test_models.py | 20 +++++++++ .../templates/nimbus_experiments/detail.html | 17 +++---- .../nimbus_experiments/timeline.html | 44 ++++++++++--------- 4 files changed, 65 insertions(+), 39 deletions(-) diff --git a/experimenter/experimenter/experiments/models.py b/experimenter/experimenter/experiments/models.py index ad1cebe5e3..4e34ee00f4 100644 --- a/experimenter/experimenter/experiments/models.py +++ b/experimenter/experimenter/experiments/models.py @@ -1240,19 +1240,28 @@ def recipe_json(self): .replace("\\n", "\n") # Handle hard coded newlines in targeting ) + def get_changelog_date(self, status_field=None, status_value=None): + queryset = self.changes.order_by("changed_on") + if status_field and status_value: + queryset = queryset.filter(**{status_field: status_value}) + changelog = queryset.first() + return changelog.changed_on.date() if changelog else None + @property def draft_date(self): - first_changelog_entry = self.changes.order_by("changed_on").first() - return first_changelog_entry.changed_on.date() if first_changelog_entry else None + return self.get_changelog_date() @property def preview_date(self): - preview_changelog = ( - self.changes.filter(new_status=self.Status.PREVIEW) - .order_by("changed_on") - .first() + return self.get_changelog_date( + status_field="new_status", status_value=self.Status.PREVIEW + ) + + @property + def review_date(self): + return self.get_changelog_date( + status_field="new_publish_status", status_value=self.PublishStatus.REVIEW ) - return preview_changelog.changed_on.date() if preview_changelog else None class NimbusBranch(models.Model): diff --git a/experimenter/experimenter/experiments/tests/test_models.py b/experimenter/experimenter/experiments/tests/test_models.py index 6aeb2c117b..28e48f9825 100644 --- a/experimenter/experimenter/experiments/tests/test_models.py +++ b/experimenter/experimenter/experiments/tests/test_models.py @@ -1667,6 +1667,26 @@ def test_preview_date_returns_none_if_no_preview_status(self): ) 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_monitoring_dashboard_url_is_valid_when_experiment_not_begun(self): experiment = NimbusExperimentFactory.create( slug="experiment", diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html index 1e23a5d949..1231311735 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html @@ -13,20 +13,13 @@ {% block main_content %}
-
-
-

Experiment Details

+
+
+

{{ experiment.name }}

+

{{ experiment.slug }}

-
- {% include "nimbus_experiments/timeline.html" %} + {% include "nimbus_experiments/timeline.html" %} -

- Slug: {{ experiment.slug }} -

-

- Name: {{ experiment.name }} -

-
{% include "nimbus_experiments/takeaways_card.html" %} diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html index 03f0305992..0cf6f88126 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html @@ -1,25 +1,29 @@ -
- -
+ +
+
    -
    -
    Draft
    -
    {{ experiment.draft_date|default:'---' }}
    -
    +
  • + Draft +

    {{ experiment.draft_date|default:'---' }}

    +
  • -
    -
    Preview
    -
    {{ experiment.preview_date|default:'---' }}
    -
    +
  • + Preview +

    {{ experiment.preview_date|default:'---' }}

    +
  • +
  • + Review +

    {{ experiment.review_date|default:'---' }}

    +
  • -
    -
    Live
    -
    {{ experiment.start_date|default:'---' }}
    -
    +
  • + Live +

    {{ experiment.start_date|default:'---' }}

    +
  • -
    -
    Complete
    -
    {{ experiment.end_date|default:'---' }}
    -
    -
+
  • + Complete +

    {{ experiment.end_date|default:'---' }}

    +
  • +
    From 9ace2ebb72450eaba702e64aaaf2ece19d9dbc52 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Tue, 24 Sep 2024 23:07:09 +0530 Subject: [PATCH 5/9] feat(nimbus): Review status --- .../nimbus_experiments/timeline.html | 59 +++++++++++++++---- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html index 0cf6f88126..3901ff7227 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html @@ -1,29 +1,64 @@ -
    -
      +
        -
      • +
      • Draft -

        {{ experiment.draft_date|default:'---' }}

        + {{ experiment.draft_date|default:'---' }}
      • + -
      • +
      • Preview -

        {{ experiment.preview_date|default:'---' }}

        + {{ experiment.preview_date|default:'---' }}
      • -
      • + + +
      • Review -

        {{ experiment.review_date|default:'---' }}

        + {{ experiment.review_date|default:'---' }}
      • + -
      • +
      • Live -

        {{ experiment.start_date|default:'---' }}

        + {{ experiment.start_date|default:'---' }}
      • + -
      • +
      • Complete -

        {{ experiment.end_date|default:'---' }}

        + {{ experiment.end_date|default:'---' }}
    From 678d4a341d86d2b3c4572820790e11179b8ff6de Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Tue, 24 Sep 2024 23:14:39 +0530 Subject: [PATCH 6/9] feat(nimbus): format --- .../nimbus_experiments/timeline.html | 45 +++---------------- 1 file changed, 5 insertions(+), 40 deletions(-) diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html index 3901ff7227..b4f9d70cda 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html @@ -1,62 +1,27 @@
      -
    • +
    • Draft {{ experiment.draft_date|default:'---' }}
    • - -
    • +
    • Preview {{ experiment.preview_date|default:'---' }}
    • - -
    • +
    • Review {{ experiment.review_date|default:'---' }}
    • - -
    • +
    • Live {{ experiment.start_date|default:'---' }}
    • - -
    • +
    • Complete {{ experiment.end_date|default:'---' }}
    • From 99615a1136bbfe760a2342e64d6257f7a6d50c14 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Wed, 25 Sep 2024 02:24:24 +0530 Subject: [PATCH 7/9] feat(nimbus): update classes --- .../nimbus_ui_new/static/css/style.scss | 7 +++++++ .../templates/nimbus_experiments/detail.html | 2 +- .../nimbus_experiments/timeline.html | 20 +++++++++---------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/experimenter/experimenter/nimbus_ui_new/static/css/style.scss b/experimenter/experimenter/nimbus_ui_new/static/css/style.scss index ee31d3c806..ce8ee3725e 100644 --- a/experimenter/experimenter/nimbus_ui_new/static/css/style.scss +++ b/experimenter/experimenter/nimbus_ui_new/static/css/style.scss @@ -61,6 +61,9 @@ } } } + .mode-sensitive { + background-color: var(--bg-light-color); + } } @include color-mode(dark) { @@ -123,4 +126,8 @@ } } } + .mode-sensitive { + background-color: var(--bs-secondary); + /* Bootstrap's secondary color */ + } } diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html index 1231311735..bfe9f23dc3 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html @@ -11,7 +11,7 @@ {% endblock %} {% block main_content %} -
      +
      diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html index b4f9d70cda..b64f931316 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html @@ -1,29 +1,29 @@
        -
      • +
      • Draft - {{ experiment.draft_date|default:'---' }} + {{ experiment.draft_date|default:'---' }}
      • -
      • +
      • Preview - {{ experiment.preview_date|default:'---' }} + {{ experiment.preview_date|default:'---' }}
      • -
      • +
      • Review - {{ experiment.review_date|default:'---' }} + {{ experiment.review_date|default:'---' }}
      • -
      • +
      • Live - {{ experiment.start_date|default:'---' }} + {{ experiment.start_date|default:'---' }}
      • -
      • +
      • Complete - {{ experiment.end_date|default:'---' }} + {{ experiment.end_date|default:'---' }}
      From f7bbce8e3aecaac03bf5e0d75475364893d76793 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Wed, 25 Sep 2024 02:36:17 +0530 Subject: [PATCH 8/9] feat(nimbus): update classes --- .../nimbus_ui_new/templates/nimbus_experiments/detail.html | 2 +- .../nimbus_ui_new/templates/nimbus_experiments/timeline.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html index bfe9f23dc3..d9641884cb 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/detail.html @@ -14,7 +14,7 @@
      -
      +

      {{ experiment.name }}

      {{ experiment.slug }}

      diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html index b64f931316..2962a9654b 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html @@ -1,4 +1,4 @@ -
      +
      • From 18b5967ff7a52e523acc575b7058f744d9490ef0 Mon Sep 17 00:00:00 2001 From: yashikakhurana Date: Wed, 25 Sep 2024 17:41:27 +0530 Subject: [PATCH 9/9] feat(nimbus): update complete end date --- .../experimenter/experiments/models.py | 35 +++++++++++ .../experiments/tests/test_models.py | 63 +++++++++++++++++++ .../nimbus_experiments/timeline.html | 31 ++------- 3 files changed, 104 insertions(+), 25 deletions(-) diff --git a/experimenter/experimenter/experiments/models.py b/experimenter/experimenter/experiments/models.py index 4e34ee00f4..7fd2682c8c 100644 --- a/experimenter/experimenter/experiments/models.py +++ b/experimenter/experimenter/experiments/models.py @@ -1263,6 +1263,41 @@ def review_date(self): status_field="new_publish_status", status_value=self.PublishStatus.REVIEW ) + def timeline(self): + statuses = [ + ("Draft", self.draft_date, self.Status.DRAFT, self.PublishStatus.IDLE), + ("Preview", self.preview_date, self.Status.PREVIEW, self.PublishStatus.IDLE), + ("Review", self.review_date, [self.Status.DRAFT, self.Status.PREVIEW], None), + ("Live", self.start_date, self.Status.LIVE, None), + ] + + timeline = [] + is_live_active = self.status == self.Status.LIVE + + for label, date, status, publish_status in statuses: + if label == "Review": + is_active = ( + self.publish_status != self.PublishStatus.IDLE + and self.status in status + ) + else: + is_active = self.status == status and ( + publish_status is None or self.publish_status == publish_status + ) + + timeline.append({"label": label, "date": date, "is_active": is_active}) + + complete_date = self.computed_end_date if is_live_active else self.end_date + timeline.append( + { + "label": "Complete", + "date": complete_date, + "is_active": self.status == self.Status.COMPLETE, + } + ) + + return timeline + class NimbusBranch(models.Model): experiment = models.ForeignKey( diff --git a/experimenter/experimenter/experiments/tests/test_models.py b/experimenter/experimenter/experiments/tests/test_models.py index 28e48f9825..26844fbe7c 100644 --- a/experimenter/experimenter/experiments/tests/test_models.py +++ b/experimenter/experimenter/experiments/tests/test_models.py @@ -1687,6 +1687,69 @@ def test_review_date_returns_none_if_no_review_status(self): ) 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", diff --git a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html index 2962a9654b..4fd407a3bb 100644 --- a/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html +++ b/experimenter/experimenter/nimbus_ui_new/templates/nimbus_experiments/timeline.html @@ -1,29 +1,10 @@
          - -
        • - Draft - {{ experiment.draft_date|default:'---' }} -
        • - -
        • - Preview - {{ experiment.preview_date|default:'---' }} -
        • - -
        • - Review - {{ experiment.review_date|default:'---' }} -
        • - -
        • - Live - {{ experiment.start_date|default:'---' }} -
        • - -
        • - Complete - {{ experiment.end_date|default:'---' }} -
        • + {% for status in experiment.timeline %} +
        • + {{ status.label }} + {{ status.date|default:'---' }} +
        • + {% endfor %}