Skip to content

Commit

Permalink
feat(nimbus): add overview form to new nimbus ui
Browse files Browse the repository at this point in the history
Becuase

* We need to add the overview to the new Nimbus UI

This commit

* Adds the overview form
* Adds documentation link add/remove functionality with HTMX

fixes #10841
  • Loading branch information
jaredlockhart committed Dec 10, 2024
1 parent 3001e69 commit e98dc37
Show file tree
Hide file tree
Showing 11 changed files with 609 additions and 18 deletions.
3 changes: 3 additions & 0 deletions experimenter/experimenter/experiments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,9 @@ def get_detail_url(self):
def get_history_url(self):
return reverse("nimbus-new-history", kwargs={"slug": self.slug})

def get_update_overview_url(self):
return reverse("nimbus-new-update-overview", kwargs={"slug": self.slug})

def get_update_metrics_url(self):
return reverse("nimbus-new-update-metrics", kwargs={"slug": self.slug})

Expand Down
136 changes: 135 additions & 1 deletion experimenter/experimenter/nimbus_ui_new/forms.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from django import forms
from django.contrib.auth.models import User
from django.forms import inlineformset_factory
from django.http import HttpRequest
from django.utils.text import slugify

from experimenter.experiments.changelog_utils import generate_nimbus_changelog
from experimenter.experiments.models import NimbusExperiment
from experimenter.experiments.models import NimbusDocumentationLink, NimbusExperiment
from experimenter.nimbus_ui_new.constants import NimbusUIConstants
from experimenter.outcomes import Outcomes
from experimenter.projects.models import Project
from experimenter.segments import Segments


Expand Down Expand Up @@ -153,6 +155,138 @@ def __init__(self, *args, attrs=None, **kwargs):
super().__init__(*args, attrs=attrs, **kwargs)


class InlineRadioSelect(forms.RadioSelect):
template_name = "common/widgets/inline_radio.html"
option_template_name = "common/widgets/inline_radio_option.html"


class NimbusDocumentationLinkForm(forms.ModelForm):
title = forms.ChoiceField(
choices=NimbusExperiment.DocumentationLink.choices,
required=False,
widget=forms.Select(attrs={"class": "form-select"}),
)
link = forms.CharField(
required=False, widget=forms.TextInput(attrs={"class": "form-control"})
)

class Meta:
model = NimbusDocumentationLink
fields = ("title", "link")


class OverviewForm(NimbusChangeLogFormMixin, forms.ModelForm):
YES_NO_CHOICES = (
(True, "Yes"),
(False, "No"),
)

name = forms.CharField(
required=False, widget=forms.TextInput(attrs={"class": "form-control"})
)
hypothesis = forms.CharField(
required=False, widget=forms.Textarea(attrs={"class": "form-control"})
)
risk_brand = forms.TypedChoiceField(
required=False,
choices=YES_NO_CHOICES,
widget=InlineRadioSelect,
coerce=lambda x: x == "True",
)
risk_message = forms.TypedChoiceField(
required=False,
choices=YES_NO_CHOICES,
widget=InlineRadioSelect,
coerce=lambda x: x == "True",
)
projects = forms.ModelMultipleChoiceField(
required=False, queryset=Project.objects.all(), widget=MultiSelectWidget()
)
public_description = forms.CharField(
required=False, widget=forms.Textarea(attrs={"class": "form-control", "rows": 3})
)
risk_revenue = forms.TypedChoiceField(
required=False,
choices=YES_NO_CHOICES,
widget=InlineRadioSelect,
coerce=lambda x: x == "True",
)
risk_partner_related = forms.TypedChoiceField(
required=False,
choices=YES_NO_CHOICES,
widget=InlineRadioSelect,
coerce=lambda x: x == "True",
)

class Meta:
model = NimbusExperiment
fields = [
"name",
"hypothesis",
"projects",
"public_description",
"risk_partner_related",
"risk_revenue",
"risk_brand",
"risk_message",
]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.NimbusDocumentationLinkFormSet = inlineformset_factory(
NimbusExperiment,
NimbusDocumentationLink,
form=NimbusDocumentationLinkForm,
extra=0, # Number of empty forms to display initially
)
self.documentation_links = self.NimbusDocumentationLinkFormSet(
data=self.data or None,
instance=self.instance,
)

def is_valid(self):
return super().is_valid() and self.documentation_links.is_valid()

def save(self):
experiment = super().save()
self.documentation_links.save()
return experiment

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


class DocumentationLinkCreateForm(NimbusChangeLogFormMixin, forms.ModelForm):
class Meta:
model = NimbusExperiment
fields = []

def save(self):
super().save(commit=False)
self.instance.documentation_links.create()
return self.instance

def get_changelog_message(self):
return f"{self.request.user} added a documentation link"


class DocumentationLinkDeleteForm(NimbusChangeLogFormMixin, forms.ModelForm):
link_id = forms.ModelChoiceField(queryset=NimbusDocumentationLink.objects.all())

class Meta:
model = NimbusExperiment
fields = ["link_id"]

def save(self):
super().save(commit=False)
documentation_link = self.cleaned_data["link_id"]
documentation_link.delete()
return self.instance

def get_changelog_message(self):
return f"{self.request.user} removed a documentation link"


class MetricsForm(NimbusChangeLogFormMixin, forms.ModelForm):
primary_outcomes = forms.MultipleChoiceField(
required=False, widget=MultiSelectWidget(attrs={"data-max-options": 2})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{% with id=widget.attrs.id %}
<div {% if id %}id="{{ id }}"{% endif %}
{% if widget.attrs.class %}class="{{ widget.attrs.class }}"{% endif %}>
{% for group, options, index in widget.optgroups %}
{% if group %}
<div>
<label>{{ group }}</label>
{% endif %}
{% for option in options %}
<div class="form-check form-check-inline ms-3 me-0">
{% include option.template_name with widget=option %}

</div>
{% endfor %}
{% if group %}</div>{% endif %}
{% endfor %}
</div>
{% endwith %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% if widget.wrap_label %}
<label class="form-check-label"
{% if widget.attrs.id %}for="{{ widget.attrs.id }}"{% endif %}>
{% endif %}
{% include "common/widgets/inline_radio_option_input.html" %}

{% if widget.wrap_label %}
{{ widget.label }}
</label>
{% endif %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<input type="{{ widget.type }}" class="form-check-input" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}
>
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
{% extends "nimbus_experiments/experiment_base.html" %}

{% load static %}
{% load nimbus_extras %}
{% load django_bootstrap5 %}
{% load widget_tweaks %}

{% block title %}{{ experiment.name }} - Overview{% endblock %}

{% block main_content %}
<form id="metrics-form"
{% if form.is_bound %}class="was-validated"{% endif %}
hx-post="{% url 'nimbus-new-update-overview' experiment.slug %}"
hx-select="#metrics-form"
hx-target="#metrics-form"
hx-swap="outerHTML">
{% csrf_token %}
{{ form.errors }}
<div class="card mb-3">
<div class="card-header">
<h4>Overview</h4>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col">
<label for="id_name" class="form-label">Public Name</label>
{{ form.name }}
<p class="form-text">This name will be public to users in about:studies.</p>
</div>
</div>
<div class="row mb-3">
<div class="col">
<label for="id_hypothesis" class="form-label">Hypothesis</label>
{{ form.hypothesis }}
<p class="form-text">You can add any supporting documents here.</p>
</div>
</div>
<div class="row mb-3">
<div class="col-10">
<label for="id_risk_brand" class="form-label">
If the public, users or press, were to discover this experiment and description, do you think it could negatively impact their perception of the brand?
<a target="_blank"
href="https://mana.mozilla.org/wiki/display/FIREFOX/Pref-Flip+and+Add-On+Experiments#PrefFlipandAddOnExperiments-Doesthishavehighrisktothebrand?">
Learn more
</a>
</label>
</div>
<div class="col-2 text-end">{{ form.risk_brand }}</div>
</div>
<div class="row mb-3">
<div class="col-10">
<label for="id_risk_message" class="form-label">
Does your experiment include ANY messages? If yes, this requires the
<a target="_blank"
href="https://mozilla-hub.atlassian.net/wiki/spaces/FIREFOX/pages/208308555/Message+Consult+Creation">
Message Consult
</a>
</label>
</div>
<div class="col-2 text-end">{{ form.risk_message }}</div>
</div>
<div class="row mb-3">
<div class="col">
<label for="id_application" class="form-label">Application</label>
<input class="form-control"
value="{{ experiment.get_application_display }}"
disabled>
<p class="form-text">
Experiments can only target one Application at a time.
<br>
Application can not be changed after an experiment is created.
</p>
</div>
</div>
<div class="row mb-3">
<div class="col">
<label for="id_projects" class="form-label">Team Projects</label>
{{ form.projects }}
</div>
</div>
<div class="row mb-3">
<div class="col">
<label for="id_public_description" class="form-label">Public Description</label>
{{ form.public_description }}
<p class="form-text">This description will be public to users on about:studies</p>
</div>
</div>
<div class="row mb-3">
<div class="col-10">
<label for="id_risk_revenue" class="form-label">
Does this experiment have a risk to negatively impact revenue (e.g. search, Pocket revenue)?
<a target="_blank"
href="https://mana.mozilla.org/wiki/display/FIREFOX/Pref-Flip+and+Add-On+Experiments#PrefFlipandAddOnExperiments-riskREV">
Learn more
</a>
</label>
</div>
<div class="col-2 text-end">{{ form.risk_revenue }}</div>
</div>
<div class="row mb-3">
<div class="col-10">
<label for="id_risk_partner_related" class="form-label">
Does this experiment impact or rely on a partner or outside company (e.g. Google, Amazon) or deliver any encryption or VPN?
<a target="_blank"
href="https://mana.mozilla.org/wiki/display/FIREFOX/Pref-Flip+and+Add-On+Experiments#PrefFlipandAddOnExperiments-Isthisstudypartnerrelated?riskPARTNER">
Learn more
</a>
</label>
</div>
<div class="col-2 text-end">{{ form.risk_partner_related }}</div>
</div>
<div class="row mb-2">
<div class="col-12">
<div>
Additional Links
<i class="fa-regular fa-circle-question"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-title="Any additional links you would like to add, for example, Jira DS Ticket, Jira QA ticket, or experiment brief."></i>
</div>
</div>
</div>
<div id="documentation-links">
{{ form.documentation_links.management_form }}
{% for link_form in form.documentation_links %}
<div class="form-group">
{{ link_form.id }}
<div class="row mb-3">
<div class="col-4">
{{ link_form.title }}
{{ link_form.title.errors }}
</div>
<div class="col-8 d-flex align-items-center">
<div class="flex-grow-1 me-2">
{{ link_form.link|add_class:"form-control"|add_error_class:"is-invalid" }}
{% for error in link_form.link.errors %}<div class="invalid-feedback">{{ error }}</div>{% endfor %}
</div>
<a class="text-primary"
hx-post="{% url 'nimbus-new-delete-documentation-link' slug=experiment.slug %}"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-params="link_id"
hx-vals='{"link_id": {{ link_form.instance.id }} }'
hx-select="#documentation-links"
hx-target="#documentation-links">
<i class="fa-solid fa-circle-xmark"></i>
</a>
</div>
</div>
<div class="row mb-3">
<div class="col-8 d-flex justify-content-between align-items-center"></div>
</div>
</div>
{% endfor %}
</div>
<div class="row mb-3">
<div class="col-12 text-end">
<button class="btn btn-outline-primary btn-sm"
type="button"
hx-post="{% url 'nimbus-new-create-documentation-link' slug=experiment.slug %}"
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-params="none"
hx-select="#documentation-links"
hx-target="#documentation-links">+ Add Link</button>
</div>
</div>
</div>
</div>
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary">Save</button>
<button type="submit" class="btn btn-secondary ms-2">Save and Continue</button>
</div>
{% if form.is_bound %}
<div class="toast text-bg-success position-fixed top-0 end-0 m-3 w-auto"
role="alert"
aria-live="assertive"
aria-atomic="true">
<div class="toast-body">
<i class="fa-regular fa-circle-check"></i>
Overview saved!
</div>
</div>
{% endif %}
</form>
{% endblock main_content %}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<strong class="ms-3">Edit</strong>
<hr class="my-0 mb-2">
{% include "common/sidebar_link.html" with title="Overview" link="" icon="fa-regular fa-solid fa-gear" disabled=True %}
{% include "common/sidebar_link.html" with title="Overview" link=experiment.get_update_overview_url icon="fa-regular fa-solid fa-gear" %}
{% include "common/sidebar_link.html" with title="Branches" link="" icon="fa-solid fa-layer-group" disabled=True %}
{% include "common/sidebar_link.html" with title="Metrics" link=experiment.get_update_metrics_url icon="fa-solid fa-arrow-trend-up" %}
{% include "common/sidebar_link.html" with title="Audience" link="" icon="fa-solid fa-user-group" disabled=True %}
Expand Down
Loading

0 comments on commit e98dc37

Please sign in to comment.