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 9d4a7ea commit 72bf452
Show file tree
Hide file tree
Showing 12 changed files with 645 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
36 changes: 36 additions & 0 deletions experimenter/experimenter/nimbus_ui_new/static/css/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,42 @@
@import "~bootstrap/scss/variables";
@import "~bootstrap/scss/mixins";

.bootstrap-select {
border-radius: var(--bs-border-radius) !important;
border-width: 1px !important;
border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important;
--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3e%3cpath fill=%27none%27 stroke=%27%23343a40%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%272%27 d=%27m2 5 6 6 6-6%27/%3e%3c/svg%3e");
background-image: var(--bs-form-select-bg-img),
var(--bs-form-select-bg-icon, none);
background-repeat: no-repeat;
background-position: right 0.75rem center;
background-size:
16px 12px,
calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);

.dropdown-toggle::after {
content: none;
}
}

.was-validated .bootstrap-select {
border-radius: var(--bs-border-radius) !important;
border-width: 1px !important;
border: var(--bs-border-width) var(--bs-border-style)
var(--bs-form-valid-border-color) !important;
--bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27%3e%3cpath fill=%27none%27 stroke=%27%23343a40%27 stroke-linecap=%27round%27 stroke-linejoin=%27round%27 stroke-width=%272%27 d=%27m2 5 6 6 6-6%27/%3e%3c/svg%3e");
--bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 8 8%27%3e%3cpath fill=%27%23198754%27 d=%27M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z%27/%3e%3c/svg%3e");
background-image: var(--bs-form-select-bg-img),
var(--bs-form-select-bg-icon, none);
background-repeat: no-repeat;
background-position:
right 0.75rem center,
center right 2.25rem;
background-size:
16px 12px,
calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}

@include color-mode(light) {
.bootstrap-select {
.dropdown-menu {
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" %}
>
Loading

0 comments on commit 72bf452

Please sign in to comment.