diff --git a/Makefile b/Makefile
index eb8f56b2a5..5b1f2dd886 100644
--- a/Makefile
+++ b/Makefile
@@ -19,7 +19,10 @@ test: compose_build
lint: compose_build
docker-compose run app flake8 .
-check: compose_build lint test
+black: compose_build
+ docker-compose run app black -l 79 .
+
+check: compose_build lint black test
echo "Success"
makemigrations: compose_build
diff --git a/app/experimenter/experiments/admin.py b/app/experimenter/experiments/admin.py
index f2ce81b09b..36513c1536 100644
--- a/app/experimenter/experiments/admin.py
+++ b/app/experimenter/experiments/admin.py
@@ -12,9 +12,7 @@
class BaseVariantInlineAdmin(admin.StackedInline):
max_num = 1
model = ExperimentVariant
- prepopulated_fields = {
- 'slug': ('name',)
- }
+ prepopulated_fields = {"slug": ("name",)}
def has_delete_permission(self, request, obj=None):
return False
@@ -32,34 +30,34 @@ class Meta:
class ControlVariantInlineAdmin(BaseVariantInlineAdmin):
+ fields = ("name", "slug", "ratio", "description", "value")
form = ControlVariantModelForm
- verbose_name = 'Control Variant'
- verbose_name_plural = 'Control Variant'
- fields = ('name', 'slug', 'ratio', 'description', 'value')
+ verbose_name = "Control Variant"
+ verbose_name_plural = "Control Variant"
def get_queryset(self, request):
return super().get_queryset(request).filter(is_control=True)
class ExperimentVariantInlineAdmin(BaseVariantInlineAdmin):
- verbose_name = 'Experiment Variant'
- verbose_name_plural = 'Experiment Variant'
- fields = ('name', 'slug', 'ratio', 'description', 'value')
+ fields = ("name", "slug", "ratio", "description", "value")
+ verbose_name = "Experiment Variant"
+ verbose_name_plural = "Experiment Variant"
def get_queryset(self, request):
return super().get_queryset(request).filter(is_control=False)
class ExperimentChangeLogInlineAdmin(admin.TabularInline):
- model = ExperimentChangeLog
extra = 1
+ model = ExperimentChangeLog
fields = (
- 'changed_by',
- 'changed_on',
- 'old_status',
- 'new_status',
- 'message',
+ "changed_by",
+ "changed_on",
+ "old_status",
+ "new_status",
+ "message",
)
@@ -69,61 +67,68 @@ class ExperimentAdmin(admin.ModelAdmin):
ExperimentVariantInlineAdmin,
ExperimentChangeLogInlineAdmin,
)
- list_display = (
- 'name', 'project', 'status')
+ list_display = ("name", "project", "status")
fieldsets = (
- ('Overview', {
- 'fields': (
- 'archived',
- 'owner',
- 'project',
- 'status',
- 'name',
- 'slug',
- 'short_description',
- 'proposed_start_date',
- 'proposed_end_date',
- ),
- }),
- ('Client Config', {
- 'fields': (
- 'pref_key',
- 'pref_type',
- 'pref_branch',
- 'firefox_channel',
- 'firefox_version',
- 'population_percent',
- 'client_matching',
- ),
- }),
- ('Notes', {
- 'fields': ('objectives', 'analysis'),
- }),
- ('Risks & Testing', {
- 'fields': (
- 'risk_partner_related',
- 'risk_brand',
- 'risk_fast_shipped',
- 'risk_confidential',
- 'risk_release_population',
- 'risks',
- 'testing',
- ),
- }),
- ('Telemetry', {
- 'fields': (
- 'dashboard_url',
- 'dashboard_image_url',
- 'enrollment_dashboard_url',
- 'total_users',
- ),
- }),
+ (
+ "Overview",
+ {
+ "fields": (
+ "archived",
+ "owner",
+ "project",
+ "status",
+ "name",
+ "slug",
+ "short_description",
+ "proposed_start_date",
+ "proposed_end_date",
+ )
+ },
+ ),
+ (
+ "Client Config",
+ {
+ "fields": (
+ "pref_key",
+ "pref_type",
+ "pref_branch",
+ "firefox_channel",
+ "firefox_version",
+ "population_percent",
+ "client_matching",
+ )
+ },
+ ),
+ ("Notes", {"fields": ("objectives", "analysis")}),
+ (
+ "Risks & Testing",
+ {
+ "fields": (
+ "risk_partner_related",
+ "risk_brand",
+ "risk_fast_shipped",
+ "risk_confidential",
+ "risk_release_population",
+ "risks",
+ "testing",
+ )
+ },
+ ),
+ (
+ "Telemetry",
+ {
+ "fields": (
+ "dashboard_url",
+ "dashboard_image_url",
+ "enrollment_dashboard_url",
+ "total_users",
+ )
+ },
+ ),
)
- prepopulated_fields = {
- 'slug': ('name',)
- }
+ prepopulated_fields = {"slug": ("name",)}
def get_actions(self, request):
return []
@@ -134,9 +139,11 @@ def has_delete_permission(self, request, obj=None):
def show_dashboard_url(self, obj):
return format_html(
'{url}'.format(
- url=obj.dashboard_url))
+ url=obj.dashboard_url
+ )
+ )
- show_dashboard_url.short_description = 'Dashboard URL'
+ show_dashboard_url.short_description = "Dashboard URL"
admin.site.register(Experiment, ExperimentAdmin)
diff --git a/app/experimenter/experiments/api_urls.py b/app/experimenter/experiments/api_urls.py
index 1acecb1032..a78cbb52e1 100644
--- a/app/experimenter/experiments/api_urls.py
+++ b/app/experimenter/experiments/api_urls.py
@@ -8,19 +8,15 @@
urlpatterns = [
+ url(r"^$", ExperimentListView.as_view(), name="experiments-api-list"),
url(
- r'^$',
- ExperimentListView.as_view(),
- name='experiments-api-list',
- ),
- url(
- r'^(?P
Choose which project this experiment belongs to. A project should correspond to a Firefox product or effort. If you do not see your project in this list, you can create one here.
- """, project_create_url=reverse_lazy('projects-create')) + """, + project_create_url=reverse_lazy("projects-create"), + ) NAME_HELP_TEXT = """@@ -231,8 +222,8 @@ class ExperimentConstants(object):
Example: default
""".format( url=( - 'https://developer.mozilla.org/en-US/docs/Archive/' - 'Add-ons/Code_snippets/Preferences#Default_preferences' + "https://developer.mozilla.org/en-US/docs/Archive/" + "Add-ons/Code_snippets/Preferences#Default_preferences" ) ) @@ -390,10 +381,11 @@ class ExperimentConstants(object): Studies: Any additional filters: - """) + """ + ) OBJECTIVES_DEFAULT = ( - 'What is the objective of this study? Explain in detail.' + "What is the objective of this study? Explain in detail." ) ANALYSIS_DEFAULT = ( @@ -406,7 +398,8 @@ class ExperimentConstants(object): Do you plan on surveying users at the end of the study? Yes/No. Strategy and Insights can help create surveys if needed - """) + """ + ) RISKS_DEFAULT = ( """If you answered yes to any of the above, your study is considered @@ -445,7 +438,8 @@ class ExperimentConstants(object): Risk Matrix Responsible: Experiment owner Accountable: Shield Team - """) + """ + ) TESTING_DEFAULT = ( """QA Status of your code: Green, yellow, red. @@ -453,4 +447,5 @@ class ExperimentConstants(object): If additional QA is required, provide a plan for testing each branch of this study: - """) + """ + ) diff --git a/app/experimenter/experiments/forms.py b/app/experimenter/experiments/forms.py index ade7bb1f94..9b98579393 100644 --- a/app/experimenter/experiments/forms.py +++ b/app/experimenter/experiments/forms.py @@ -7,7 +7,10 @@ from experimenter.projects.forms import AutoNameSlugFormMixin from experimenter.projects.models import Project from experimenter.experiments.models import ( - Experiment, ExperimentVariant, ExperimentChangeLog) + Experiment, + ExperimentChangeLog, + ExperimentVariant, +) from experimenter.experiments.constants import ExperimentConstants @@ -20,7 +23,7 @@ def clean(self, value): try: json.loads(cleaned_value) except json.JSONDecodeError: - raise forms.ValidationError('This is not valid JSON.') + raise forms.ValidationError("This is not valid JSON.") return cleaned_value @@ -30,8 +33,8 @@ class NameSlugMixin(object): def clean(self): cleaned_data = super().clean() - name = cleaned_data.get('name') - cleaned_data['slug'] = slugify(name) + name = cleaned_data.get("name") + cleaned_data["slug"] = slugify(name) return cleaned_data @@ -39,46 +42,47 @@ def clean(self): class ControlVariantForm(NameSlugMixin, forms.ModelForm): description = forms.CharField( - label='Description', + label="Description", help_text=Experiment.CONTROL_DESCRIPTION_HELP_TEXT, - widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3}) + widget=forms.Textarea(attrs={"class": "form-control", "rows": 3}), ) experiment = forms.ModelChoiceField( - queryset=Experiment.objects.all(), required=False) + queryset=Experiment.objects.all(), required=False + ) is_control = forms.BooleanField(required=False) slug = forms.CharField(required=False) ratio = forms.IntegerField( required=False, - label='Variant Split', - initial='50', + label="Variant Split", + initial="50", help_text=Experiment.CONTROL_RATIO_HELP_TEXT, widget=forms.NumberInput( - attrs={'type': 'range', 'min': '1', 'max': '99', 'step': '1'} - ) + attrs={"type": "range", "min": "1", "max": "99", "step": "1"} + ), ) name = forms.CharField( - label='Name', + label="Name", help_text=Experiment.CONTROL_NAME_HELP_TEXT, - widget=forms.TextInput(attrs={'class': 'form-control'}) + widget=forms.TextInput(attrs={"class": "form-control"}), ) value = JSONField( - label='Pref Value', + label="Pref Value", help_text=Experiment.CONTROL_VALUE_HELP_TEXT, - widget=forms.TextInput(attrs={'class': 'form-control'}) + widget=forms.TextInput(attrs={"class": "form-control"}), ) - prefix = 'control' + prefix = "control" class Meta: model = ExperimentVariant fields = [ - 'description', - 'experiment', - 'is_control', - 'name', - 'ratio', - 'slug', - 'value', + "description", + "experiment", + "is_control", + "name", + "ratio", + "slug", + "value", ] def clean_is_control(self): @@ -89,36 +93,37 @@ class ExperimentalVariantForm(NameSlugMixin, forms.ModelForm): slug = forms.CharField(required=False) experiment = forms.ModelChoiceField( - required=False, queryset=Experiment.objects.all()) - ratio = forms.IntegerField(required=False, initial='50') + required=False, queryset=Experiment.objects.all() + ) + ratio = forms.IntegerField(required=False, initial="50") name = forms.CharField( - label='Name', + label="Name", help_text=Experiment.VARIANT_NAME_HELP_TEXT, - widget=forms.TextInput(attrs={'class': 'form-control'}), + widget=forms.TextInput(attrs={"class": "form-control"}), ) description = forms.CharField( - label='Description', + label="Description", help_text=Experiment.VARIANT_DESCRIPTION_HELP_TEXT, - widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), + widget=forms.Textarea(attrs={"class": "form-control", "rows": 3}), ) value = JSONField( - label='Pref Value', + label="Pref Value", help_text=Experiment.VARIANT_VALUE_HELP_TEXT, - widget=forms.TextInput(attrs={'class': 'form-control'}), + widget=forms.TextInput(attrs={"class": "form-control"}), ) - prefix = 'experimental' + prefix = "experimental" class Meta: model = ExperimentVariant fields = [ - 'slug', - 'experiment', - 'ratio', - 'name', - 'description', - 'value', - 'is_control', + "slug", + "experiment", + "ratio", + "name", + "description", + "value", + "is_control", ] def clean_is_control(self): @@ -154,136 +159,134 @@ def save(self, *args, **kwargs): class ExperimentOverviewForm( - AutoNameSlugFormMixin, ChangeLogMixin, forms.ModelForm): + AutoNameSlugFormMixin, ChangeLogMixin, forms.ModelForm +): owner = forms.ModelChoiceField( required=False, - label='Owner', + label="Owner", help_text=Experiment.OWNER_HELP_TEXT, queryset=get_user_model().objects.all(), - widget=forms.Select(attrs={'class': 'form-control'}), + widget=forms.Select(attrs={"class": "form-control"}), ) project = forms.ModelChoiceField( required=False, - label='Project', + label="Project", help_text=Experiment.PROJECT_HELP_TEXT, queryset=Project.objects.all(), - widget=forms.Select(attrs={'class': 'form-control'}), + widget=forms.Select(attrs={"class": "form-control"}), ) name = forms.CharField( - label='Name', + label="Name", help_text=Experiment.NAME_HELP_TEXT, - widget=forms.TextInput(attrs={'class': 'form-control'}), + widget=forms.TextInput(attrs={"class": "form-control"}), ) slug = forms.CharField(required=False) short_description = forms.CharField( - label='Short Description', + label="Short Description", help_text=Experiment.SHORT_DESCRIPTION_HELP_TEXT, - widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3}), + widget=forms.Textarea(attrs={"class": "form-control", "rows": 3}), ) population_percent = forms.DecimalField( - label='Population Size', + label="Population Size", help_text=Experiment.POPULATION_PERCENT_HELP_TEXT, - initial='0.00', - widget=forms.NumberInput(attrs={'class': 'form-control'}), + initial="0.00", + widget=forms.NumberInput(attrs={"class": "form-control"}), ) firefox_version = forms.ChoiceField( choices=Experiment.VERSION_CHOICES, - widget=forms.Select(attrs={'class': 'form-control'}), + widget=forms.Select(attrs={"class": "form-control"}), ) firefox_channel = forms.ChoiceField( choices=Experiment.CHANNEL_CHOICES, - widget=forms.Select(attrs={'class': 'form-control'}), + widget=forms.Select(attrs={"class": "form-control"}), ) client_matching = forms.CharField( - label='Population Filtering', + label="Population Filtering", help_text=Experiment.CLIENT_MATCHING_HELP_TEXT, initial=Experiment.CLIENT_MATCHING_DEFAULT, - widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 10}), + widget=forms.Textarea(attrs={"class": "form-control", "rows": 10}), ) proposed_start_date = forms.DateField( - label='Proposed Start Date', + label="Proposed Start Date", help_text=Experiment.PROPOSED_START_DATE_HELP_TEXT, widget=forms.DateInput( - attrs={'type': 'date', 'class': 'form-control'}), + attrs={"type": "date", "class": "form-control"} + ), ) proposed_end_date = forms.DateField( - label='Proposed End Date', + label="Proposed End Date", help_text=Experiment.PROPOSED_END_DATE_HELP_TEXT, widget=forms.DateInput( - attrs={'type': 'date', 'class': 'form-control'}), + attrs={"type": "date", "class": "form-control"} + ), ) class Meta: model = Experiment fields = [ - 'owner', - 'project', - 'name', - 'slug', - 'short_description', - 'population_percent', - 'firefox_version', - 'firefox_channel', - 'client_matching', - 'proposed_start_date', - 'proposed_end_date', + "owner", + "project", + "name", + "slug", + "short_description", + "population_percent", + "firefox_version", + "firefox_channel", + "client_matching", + "proposed_start_date", + "proposed_end_date", ] def clean_population_percent(self): - population_percent = self.cleaned_data['population_percent'] + population_percent = self.cleaned_data["population_percent"] if not (0 < population_percent <= 100): raise forms.ValidationError( - 'The population size must be between 0 and 100 percent.') + "The population size must be between 0 and 100 percent." + ) return population_percent class ExperimentVariantsForm(ChangeLogMixin, forms.ModelForm): pref_key = forms.CharField( - label='Pref Name', + label="Pref Name", help_text=Experiment.PREF_KEY_HELP_TEXT, - widget=forms.TextInput(attrs={'class': 'form-control'}), + widget=forms.TextInput(attrs={"class": "form-control"}), ) pref_type = forms.ChoiceField( - label='Pref Type', + label="Pref Type", help_text=Experiment.PREF_TYPE_HELP_TEXT, choices=Experiment.PREF_TYPE_CHOICES, - widget=forms.Select(attrs={'class': 'form-control'}), + widget=forms.Select(attrs={"class": "form-control"}), ) pref_branch = forms.ChoiceField( - label='Pref Branch', + label="Pref Branch", help_text=Experiment.PREF_BRANCH_HELP_TEXT, choices=Experiment.PREF_BRANCH_CHOICES, - widget=forms.Select(attrs={'class': 'form-control'}), + widget=forms.Select(attrs={"class": "form-control"}), ) class Meta: model = Experiment - fields = [ - 'pref_key', - 'pref_type', - 'pref_branch', - ] + fields = ["pref_key", "pref_type", "pref_branch"] def __init__(self, data=None, instance=None, *args, **kwargs): super().__init__(data=data, instance=instance, *args, **kwargs) self.control_form = ControlVariantForm( - data=data, - instance=instance.control if instance else None, + data=data, instance=instance.control if instance else None ) self.experimental_form = ExperimentalVariantForm( - data=data, - instance=instance.variant if instance else None, + data=data, instance=instance.variant if instance else None ) def is_valid(self, *args, **kwargs): return ( - super().is_valid(*args, **kwargs) and - self.control_form.is_valid(*args, **kwargs) and - self.experimental_form.is_valid(*args, **kwargs) + super().is_valid(*args, **kwargs) + and self.control_form.is_valid(*args, **kwargs) + and self.experimental_form.is_valid(*args, **kwargs) ) def save(self, *args, **kwargs): @@ -296,7 +299,8 @@ def save(self, *args, **kwargs): if self.experimental_form.instance.slug: self.experimental_form.instance.experiment = experiment self.experimental_form.instance.ratio = ( - 100 - self.control_form.instance.ratio) + 100 - self.control_form.instance.ratio + ) self.experimental_form.save(*args, **kwargs) return experiment @@ -304,30 +308,27 @@ def save(self, *args, **kwargs): class ExperimentObjectivesForm(ChangeLogMixin, forms.ModelForm): objectives = forms.CharField( - label='Objectives', + label="Objectives", help_text=Experiment.OBJECTIVES_HELP_TEXT, - widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 20}), + widget=forms.Textarea(attrs={"class": "form-control", "rows": 20}), ) analysis = forms.CharField( - label='Analysis Plan', + label="Analysis Plan", help_text=Experiment.ANALYSIS_HELP_TEXT, - widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 20}), + widget=forms.Textarea(attrs={"class": "form-control", "rows": 20}), ) class Meta: model = Experiment - fields = ('objectives', 'analysis') + fields = ("objectives", "analysis") class RadioWidget(forms.widgets.RadioSelect): - template_name = 'experiments/radio_widget.html' + template_name = "experiments/radio_widget.html" class ExperimentRisksForm(ChangeLogMixin, forms.ModelForm): - RADIO_OPTIONS = ( - (False, 'No'), - (True, 'Yes'), - ) + RADIO_OPTIONS = ((False, "No"), (True, "Yes")) risk_partner_related = forms.ChoiceField( label=Experiment.RISK_PARTNER_RELATED_LABEL, @@ -355,46 +356,48 @@ class ExperimentRisksForm(ChangeLogMixin, forms.ModelForm): widget=RadioWidget, ) risks = forms.CharField( - label='Risks', + label="Risks", help_text=Experiment.RISKS_HELP_TEXT, - widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 20}), + widget=forms.Textarea(attrs={"class": "form-control", "rows": 20}), ) testing = forms.CharField( - label='Test Plan', + label="Test Plan", help_text=Experiment.TESTING_HELP_TEXT, - widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 20}), + widget=forms.Textarea(attrs={"class": "form-control", "rows": 20}), ) class Meta: model = Experiment fields = ( - 'risk_partner_related', - 'risk_brand', - 'risk_fast_shipped', - 'risk_confidential', - 'risk_release_population', - 'risks', - 'testing', + "risk_partner_related", + "risk_brand", + "risk_fast_shipped", + "risk_confidential", + "risk_release_population", + "risks", + "testing", ) class ExperimentStatusForm( - ExperimentConstants, ChangeLogMixin, forms.ModelForm): + ExperimentConstants, ChangeLogMixin, forms.ModelForm +): class Meta: model = Experiment - fields = ('status',) + fields = ("status",) def clean_status(self): old_status = self.instance.status - new_status = self.cleaned_data['status'] + new_status = self.cleaned_data["status"] expected_new_status = new_status in self.STATUS_TRANSITIONS[old_status] if old_status != new_status and not expected_new_status: - raise forms.ValidationError(( - 'You can not change an Experiment\'s status ' - 'from {old_status} to {new_status}' - ).format( - old_status=old_status, new_status=new_status)) + raise forms.ValidationError( + ( + "You can not change an Experiment's status " + "from {old_status} to {new_status}" + ).format(old_status=old_status, new_status=new_status) + ) return new_status diff --git a/app/experimenter/experiments/management/commands/generate_dashboards.py b/app/experimenter/experiments/management/commands/generate_dashboards.py index a7108f0ef1..34ec07aa5d 100644 --- a/app/experimenter/experiments/management/commands/generate_dashboards.py +++ b/app/experimenter/experiments/management/commands/generate_dashboards.py @@ -10,65 +10,78 @@ def sanitize_name(name): - return slugify(name).replace('-', ' ').title() + return slugify(name).replace("-", " ").title() -DASHBOARD_TAG_NAME = 'Experimenter Dashboard' +DASHBOARD_TAG_NAME = "Experimenter Dashboard" class Command(BaseCommand): - POPULATION_TEMPLATE = 'UT Experiment Template: Population Size' + POPULATION_TEMPLATE = "UT Experiment Template: Population Size" EXISTING_USERS_SCALARS_TEMPLATE = ( - "Experiment Template Rate Scalars: [Existing Users]") + "Experiment Template Rate Scalars: [Existing Users]" + ) EXISTING_USERS_MAPS_TEMPLATE = ( - "Experiment Template Rate Maps: [Existing Users]") + "Experiment Template Rate Maps: [Existing Users]" + ) NEW_USERS_SCALARS_TEMPLATE = ( - "Experiment Template Rate Scalars: [New Users]") + "Experiment Template Rate Scalars: [New Users]" + ) NEW_USERS_MAPS_TEMPLATE = "Experiment Template Rate Maps: [New Users]" - EVENTS_PER_HOUR_TEMPLATE = 'TTests Template Per Hour UT Five:' - UT_HOURLY_TTABLE = 'Statistical Analysis (Per Active Hour) - UT' - help = 'Generates Redash dashboards' + EVENTS_PER_HOUR_TEMPLATE = "TTests Template Per Hour UT Five:" + UT_HOURLY_TTABLE = "Statistical Analysis (Per Active Hour) - UT" + help = "Generates Redash dashboards" def generate_dashboards(self): recent_changelog_complete = ExperimentChangeLog.objects.filter( old_status=Experiment.STATUS_LIVE, - new_status=Experiment.STATUS_COMPLETE).filter( - changed_on__gte=(datetime.now(timezone.utc) - timedelta(days=3))) + new_status=Experiment.STATUS_COMPLETE, + ).filter( + changed_on__gte=(datetime.now(timezone.utc) - timedelta(days=3)) + ) recently_ended_experiments = Experiment.objects.filter( - status=Experiment.STATUS_COMPLETE).filter( - changes__in=recent_changelog_complete) + status=Experiment.STATUS_COMPLETE + ).filter(changes__in=recent_changelog_complete) in_flight_experiments = Experiment.objects.filter( status=Experiment.STATUS_LIVE ) missing_dashboard_experiments = Experiment.objects.filter( - dashboard_url__isnull=True) + dashboard_url__isnull=True + ) relevant_experiments = ( - recently_ended_experiments | - missing_dashboard_experiments | - in_flight_experiments - ).distinct()[:settings.DASHBOARD_RATE_LIMIT] + recently_ended_experiments + | missing_dashboard_experiments + | in_flight_experiments + ).distinct()[: settings.DASHBOARD_RATE_LIMIT] for exp in relevant_experiments: - end_date = (None - if exp.end_date is None - else exp.end_date.strftime("%Y-%m-%d")) + end_date = ( + None + if exp.end_date is None + else exp.end_date.strftime("%Y-%m-%d") + ) try: dash = ExperimentDashboard( - settings.REDASH_API_KEY, - DASHBOARD_TAG_NAME, - sanitize_name(exp.name), - exp.slug, - exp.start_date.strftime("%Y-%m-%d"), - end_date + settings.REDASH_API_KEY, + DASHBOARD_TAG_NAME, + sanitize_name(exp.name), + exp.slug, + exp.start_date.strftime("%Y-%m-%d"), + end_date, ) - expected_widget_count = ( - int((1 + 2 * len(dash.UT_HOURLY_EVENTS) - + 2 * len(dash.MAPPED_UT_EVENTS)) / 2)) + expected_widget_count = int( + ( + 1 + + 2 * len(dash.UT_HOURLY_EVENTS) + + 2 * len(dash.MAPPED_UT_EVENTS) + ) + / 2 + ) widget_count = len(dash.get_query_ids_and_names()) dashboard_presentable = widget_count >= expected_widget_count @@ -78,28 +91,33 @@ def generate_dashboards(self): # This dashboard was recently updated, no need to update again. update_begin = dash.get_update_range().get("min", None) - if update_begin is not None and dashboard_presentable and ( - update_begin > ( - datetime.now(timezone.utc) - timedelta(days=1))): + if ( + update_begin is not None + and dashboard_presentable + and ( + update_begin + > (datetime.now(timezone.utc) - timedelta(days=1)) + ) + ): continue dash.add_graph_templates(self.POPULATION_TEMPLATE) # Existing Users dash.add_graph_templates( - self.EXISTING_USERS_SCALARS_TEMPLATE, - dash.UT_HOURLY_EVENTS + self.EXISTING_USERS_SCALARS_TEMPLATE, dash.UT_HOURLY_EVENTS ) dash.add_graph_templates( - self.EXISTING_USERS_MAPS_TEMPLATE, - dash.MAPPED_UT_EVENTS + self.EXISTING_USERS_MAPS_TEMPLATE, dash.MAPPED_UT_EVENTS ) # New Users dash.add_graph_templates( - self.NEW_USERS_SCALARS_TEMPLATE, dash.UT_HOURLY_EVENTS) + self.NEW_USERS_SCALARS_TEMPLATE, dash.UT_HOURLY_EVENTS + ) dash.add_graph_templates( - self.NEW_USERS_MAPS_TEMPLATE, dash.MAPPED_UT_EVENTS) + self.NEW_USERS_MAPS_TEMPLATE, dash.MAPPED_UT_EVENTS + ) # recompute widget count after graphs are added widget_count = len(dash.get_query_ids_and_names()) @@ -108,15 +126,18 @@ def generate_dashboards(self): exp.dashboard_url = dash.public_url exp.save() except ExperimentDashboard.ExternalAPIError as external_api_err: - logging.error(( - 'ExternalAPIError ' - 'for {experiment}: {err}').format( - experiment=exp, err=external_api_err)) + logging.error( + ("ExternalAPIError " "for {experiment}: {err}").format( + experiment=exp, err=external_api_err + ) + ) except ValueError as val_err: - logging.error(( - 'ExperimentDashboard Value Error ' - 'for {experiment}: {err}').format( - experiment=exp, err=val_err)) + logging.error( + ( + "ExperimentDashboard Value Error " + "for {experiment}: {err}" + ).format(experiment=exp, err=val_err) + ) def handle(self, *args, **options): self.generate_dashboards() diff --git a/app/experimenter/experiments/migrations/0001_initial.py b/app/experimenter/experiments/migrations/0001_initial.py index 46cfb34d64..e9a547eb36 100644 --- a/app/experimenter/experiments/migrations/0001_initial.py +++ b/app/experimenter/experiments/migrations/0001_initial.py @@ -7,8 +7,6 @@ class Migration(migrations.Migration): - dependencies = [ - ] + dependencies = [] - operations = [ - ] + operations = [] diff --git a/app/experimenter/experiments/migrations/0002_auto_20171115_1918.py b/app/experimenter/experiments/migrations/0002_auto_20171115_1918.py index c97fe01080..673c3c610f 100644 --- a/app/experimenter/experiments/migrations/0002_auto_20171115_1918.py +++ b/app/experimenter/experiments/migrations/0002_auto_20171115_1918.py @@ -13,73 +13,214 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('projects', '0003_auto_20170630_1924'), + ("projects", "0003_auto_20170630_1924"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('experiments', '0001_initial'), + ("experiments", "0001_initial"), ] operations = [ migrations.CreateModel( - name='Experiment', + name="Experiment", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('status', models.CharField(choices=[('Created', 'Created'), ('Pending', 'Pending'), ('Accepted', 'Accepted'), ('Launched', 'Launched'), ('Complete', 'Complete'), ('Rejected', 'Rejected')], default='Created', max_length=255)), - ('pref_key', models.CharField(blank=True, max_length=255, null=True)), - ('pref_type', models.CharField(choices=[('boolean', 'boolean'), ('integer', 'integer'), ('string', 'string')], max_length=255)), - ('pref_branch', models.CharField(choices=[('default', 'default'), ('user', 'user')], max_length=255)), - ('firefox_version', models.CharField(max_length=255)), - ('firefox_channel', models.CharField(choices=[('Nightly', 'Nightly'), ('Beta', 'Beta'), ('Release', 'Release')], default='Nightly', max_length=255)), - ('client_matching', models.TextField(default='')), - ('name', models.CharField(max_length=255, unique=True)), - ('slug', models.SlugField(max_length=255, unique=True)), - ('objectives', models.TextField(default='')), - ('analysis', models.TextField(blank=True, default='', null=True)), - ('dashboard_url', models.URLField(blank=True, null=True)), - ('dashboard_image_url', models.URLField(blank=True, null=True)), - ('population_percent', models.DecimalField(decimal_places=4, default='0', max_digits=7)), - ('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='experiments', to='projects.Project')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "status", + models.CharField( + choices=[ + ("Created", "Created"), + ("Pending", "Pending"), + ("Accepted", "Accepted"), + ("Launched", "Launched"), + ("Complete", "Complete"), + ("Rejected", "Rejected"), + ], + default="Created", + max_length=255, + ), + ), + ( + "pref_key", + models.CharField(blank=True, max_length=255, null=True), + ), + ( + "pref_type", + models.CharField( + choices=[ + ("boolean", "boolean"), + ("integer", "integer"), + ("string", "string"), + ], + max_length=255, + ), + ), + ( + "pref_branch", + models.CharField( + choices=[("default", "default"), ("user", "user")], + max_length=255, + ), + ), + ("firefox_version", models.CharField(max_length=255)), + ( + "firefox_channel", + models.CharField( + choices=[ + ("Nightly", "Nightly"), + ("Beta", "Beta"), + ("Release", "Release"), + ], + default="Nightly", + max_length=255, + ), + ), + ("client_matching", models.TextField(default="")), + ("name", models.CharField(max_length=255, unique=True)), + ("slug", models.SlugField(max_length=255, unique=True)), + ("objectives", models.TextField(default="")), + ( + "analysis", + models.TextField(blank=True, default="", null=True), + ), + ("dashboard_url", models.URLField(blank=True, null=True)), + ( + "dashboard_image_url", + models.URLField(blank=True, null=True), + ), + ( + "population_percent", + models.DecimalField( + decimal_places=4, default="0", max_digits=7 + ), + ), + ( + "project", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="experiments", + to="projects.Project", + ), + ), ], options={ - 'verbose_name': 'Experiment', - 'verbose_name_plural': 'Experiments', + "verbose_name": "Experiment", + "verbose_name_plural": "Experiments", }, ), migrations.CreateModel( - name='ExperimentChangeLog', + name="ExperimentChangeLog", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('changed_on', models.DateTimeField(auto_now_add=True)), - ('old_status', models.CharField(blank=True, choices=[('Created', 'Created'), ('Pending', 'Pending'), ('Accepted', 'Accepted'), ('Launched', 'Launched'), ('Complete', 'Complete'), ('Rejected', 'Rejected')], max_length=255, null=True)), - ('new_status', models.CharField(choices=[('Created', 'Created'), ('Pending', 'Pending'), ('Accepted', 'Accepted'), ('Launched', 'Launched'), ('Complete', 'Complete'), ('Rejected', 'Rejected')], max_length=255)), - ('message', models.TextField(blank=True, null=True)), - ('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('experiment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='changes', to='experiments.Experiment')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("changed_on", models.DateTimeField(auto_now_add=True)), + ( + "old_status", + models.CharField( + blank=True, + choices=[ + ("Created", "Created"), + ("Pending", "Pending"), + ("Accepted", "Accepted"), + ("Launched", "Launched"), + ("Complete", "Complete"), + ("Rejected", "Rejected"), + ], + max_length=255, + null=True, + ), + ), + ( + "new_status", + models.CharField( + choices=[ + ("Created", "Created"), + ("Pending", "Pending"), + ("Accepted", "Accepted"), + ("Launched", "Launched"), + ("Complete", "Complete"), + ("Rejected", "Rejected"), + ], + max_length=255, + ), + ), + ("message", models.TextField(blank=True, null=True)), + ( + "changed_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "experiment", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="changes", + to="experiments.Experiment", + ), + ), ], options={ - 'verbose_name': 'Experiment Change Log', - 'verbose_name_plural': 'Experiment Change Logs', - 'ordering': ('changed_on',), + "verbose_name": "Experiment Change Log", + "verbose_name_plural": "Experiment Change Logs", + "ordering": ("changed_on",), }, ), migrations.CreateModel( - name='ExperimentVariant', + name="ExperimentVariant", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=255)), - ('slug', models.SlugField(max_length=255)), - ('is_control', models.BooleanField(default=False)), - ('description', models.TextField(default='')), - ('ratio', models.PositiveIntegerField(default=1)), - ('value', django.contrib.postgres.fields.jsonb.JSONField(default=False)), - ('experiment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variants', to='experiments.Experiment')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ("slug", models.SlugField(max_length=255)), + ("is_control", models.BooleanField(default=False)), + ("description", models.TextField(default="")), + ("ratio", models.PositiveIntegerField(default=1)), + ( + "value", + django.contrib.postgres.fields.jsonb.JSONField( + default=False + ), + ), + ( + "experiment", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="variants", + to="experiments.Experiment", + ), + ), ], options={ - 'verbose_name': 'Experiment Variant', - 'verbose_name_plural': 'Experiment Variants', + "verbose_name": "Experiment Variant", + "verbose_name_plural": "Experiment Variants", }, ), migrations.AlterUniqueTogether( - name='experimentvariant', - unique_together=set([('is_control', 'experiment'), ('slug', 'experiment')]), + name="experimentvariant", + unique_together=set( + [("is_control", "experiment"), ("slug", "experiment")] + ), ), ] diff --git a/app/experimenter/experiments/migrations/0003_auto_20171120_2205.py b/app/experimenter/experiments/migrations/0003_auto_20171120_2205.py index 53dfaa2fd0..48df2e2a95 100644 --- a/app/experimenter/experiments/migrations/0003_auto_20171120_2205.py +++ b/app/experimenter/experiments/migrations/0003_auto_20171120_2205.py @@ -7,19 +7,21 @@ class Migration(migrations.Migration): - dependencies = [ - ('experiments', '0002_auto_20171115_1918'), - ] + dependencies = [("experiments", "0002_auto_20171115_1918")] operations = [ migrations.AlterField( - model_name='experiment', - name='client_matching', - field=models.TextField(blank=True, default=''), + model_name="experiment", + name="client_matching", + field=models.TextField(blank=True, default=""), ), migrations.AlterField( - model_name='experiment', - name='pref_branch', - field=models.CharField(choices=[('default', 'default'), ('user', 'user')], default='default', max_length=255), + model_name="experiment", + name="pref_branch", + field=models.CharField( + choices=[("default", "default"), ("user", "user")], + default="default", + max_length=255, + ), ), ] diff --git a/app/experimenter/experiments/migrations/0004_auto_20171205_2015.py b/app/experimenter/experiments/migrations/0004_auto_20171205_2015.py index 9c9bcbc833..6292a33276 100644 --- a/app/experimenter/experiments/migrations/0004_auto_20171205_2015.py +++ b/app/experimenter/experiments/migrations/0004_auto_20171205_2015.py @@ -8,24 +8,24 @@ class Migration(migrations.Migration): - dependencies = [ - ('experiments', '0003_auto_20171120_2205'), - ] + dependencies = [("experiments", "0003_auto_20171120_2205")] operations = [ migrations.AddField( - model_name='experiment', - name='enrollment_dashboard_url', + model_name="experiment", + name="enrollment_dashboard_url", field=models.URLField(blank=True, null=True), ), migrations.AddField( - model_name='experiment', - name='total_users', + model_name="experiment", + name="total_users", field=models.PositiveIntegerField(default=0), ), migrations.AlterField( - model_name='experimentchangelog', - name='changed_on', - field=models.DateTimeField(default=experimenter.experiments.models.ExperimentChangeLog.current_datetime), + model_name="experimentchangelog", + name="changed_on", + field=models.DateTimeField( + default=experimenter.experiments.models.ExperimentChangeLog.current_datetime + ), ), ] diff --git a/app/experimenter/experiments/migrations/0005_experiment_short_description.py b/app/experimenter/experiments/migrations/0005_experiment_short_description.py index b2df2f495b..4e7307a5a9 100644 --- a/app/experimenter/experiments/migrations/0005_experiment_short_description.py +++ b/app/experimenter/experiments/migrations/0005_experiment_short_description.py @@ -7,14 +7,12 @@ class Migration(migrations.Migration): - dependencies = [ - ('experiments', '0004_auto_20171205_2015'), - ] + dependencies = [("experiments", "0004_auto_20171205_2015")] operations = [ migrations.AddField( - model_name='experiment', - name='short_description', - field=models.TextField(blank=True, default='', null=True), - ), + model_name="experiment", + name="short_description", + field=models.TextField(blank=True, default="", null=True), + ) ] diff --git a/app/experimenter/experiments/migrations/0006_auto_20180221_1939.py b/app/experimenter/experiments/migrations/0006_auto_20180221_1939.py index 878e411b02..ba517fe3c4 100644 --- a/app/experimenter/experiments/migrations/0006_auto_20180221_1939.py +++ b/app/experimenter/experiments/migrations/0006_auto_20180221_1939.py @@ -5,62 +5,84 @@ from django.db import migrations, models -def update_dates(apps, schema_editor): # pragma: no cover - Experiment = apps.get_model('experiments', 'Experiment') - ExperimentChangeLog = apps.get_model('experiments', 'ExperimentChangeLog') +def update_dates(apps, schema_editor): # pragma: no cover + Experiment = apps.get_model("experiments", "Experiment") + ExperimentChangeLog = apps.get_model("experiments", "ExperimentChangeLog") for experiment in Experiment.objects.all(): try: - start_date = ExperimentChangeLog.objects.get(experiment=experiment, old_status='Accepted', new_status='Launched').changed_on + start_date = ExperimentChangeLog.objects.get( + experiment=experiment, + old_status="Accepted", + new_status="Launched", + ).changed_on experiment.proposed_start_date = start_date except: pass try: - end_date = ExperimentChangeLog.objects.get(experiment=experiment, old_status='Launched', new_status='Complete').changed_on + end_date = ExperimentChangeLog.objects.get( + experiment=experiment, + old_status="Launched", + new_status="Complete", + ).changed_on experiment.proposed_end_date = end_date except: pass experiment.save() -def update_versions(apps, schema_editor): # pragma: no cover - Experiment = apps.get_model('experiments', 'Experiment') +def update_versions(apps, schema_editor): # pragma: no cover + Experiment = apps.get_model("experiments", "Experiment") for experiment in Experiment.objects.all(): - experiment.firefox_version = '{}.0'.format(experiment.firefox_version) + experiment.firefox_version = "{}.0".format(experiment.firefox_version) experiment.save() -def add_project_to_name(apps, schema_editor): #pragma: no cover - Experiment = apps.get_model('experiments', 'Experiment') +def add_project_to_name(apps, schema_editor): # pragma: no cover + Experiment = apps.get_model("experiments", "Experiment") for experiment in Experiment.objects.all(): - experiment.name = '{} {}'.format(experiment.project.name, experiment.name) + experiment.name = "{} {}".format( + experiment.project.name, experiment.name + ) experiment.save() class Migration(migrations.Migration): - dependencies = [ - ('experiments', '0005_experiment_short_description'), - ] + dependencies = [("experiments", "0005_experiment_short_description")] operations = [ migrations.AddField( - model_name='experiment', - name='proposed_end_date', + model_name="experiment", + name="proposed_end_date", field=models.DateField(blank=True, null=True), ), migrations.AddField( - model_name='experiment', - name='proposed_start_date', + model_name="experiment", + name="proposed_start_date", field=models.DateField(blank=True, null=True), ), migrations.AlterField( - model_name='experiment', - name='firefox_version', - field=models.CharField(choices=[('55.0', '55.0'), ('56.0', '56.0'), ('57.0', '57.0'), ('58.0', '58.0'), ('59.0', '59.0'), ('60.0', '60.0'), ('61.0', '61.0'), ('62.0', '62.0'), ('63.0', '63.0'), ('64.0', '64.0')], max_length=255), + model_name="experiment", + name="firefox_version", + field=models.CharField( + choices=[ + ("55.0", "55.0"), + ("56.0", "56.0"), + ("57.0", "57.0"), + ("58.0", "58.0"), + ("59.0", "59.0"), + ("60.0", "60.0"), + ("61.0", "61.0"), + ("62.0", "62.0"), + ("63.0", "63.0"), + ("64.0", "64.0"), + ], + max_length=255, + ), ), migrations.RunPython(update_dates), migrations.RunPython(update_versions), diff --git a/app/experimenter/experiments/migrations/0007_auto_20180424_2039.py b/app/experimenter/experiments/migrations/0007_auto_20180424_2039.py index cd55b9e7f2..1906fae242 100644 --- a/app/experimenter/experiments/migrations/0007_auto_20180424_2039.py +++ b/app/experimenter/experiments/migrations/0007_auto_20180424_2039.py @@ -8,79 +8,141 @@ class Migration(migrations.Migration): - dependencies = [ - ('experiments', '0006_auto_20180221_1939'), - ] + dependencies = [("experiments", "0006_auto_20180221_1939")] operations = [ migrations.AddField( - model_name='experiment', - name='risk_brand', + model_name="experiment", + name="risk_brand", field=models.NullBooleanField(default=None), ), migrations.AddField( - model_name='experiment', - name='risk_confidential', + model_name="experiment", + name="risk_confidential", field=models.NullBooleanField(default=None), ), migrations.AddField( - model_name='experiment', - name='risk_fast_shipped', + model_name="experiment", + name="risk_fast_shipped", field=models.NullBooleanField(default=None), ), migrations.AddField( - model_name='experiment', - name='risk_partner_related', + model_name="experiment", + name="risk_partner_related", field=models.NullBooleanField(default=None), ), migrations.AddField( - model_name='experiment', - name='risk_release_population', + model_name="experiment", + name="risk_release_population", field=models.NullBooleanField(default=None), ), migrations.AddField( - model_name='experiment', - name='risks', - field=models.TextField(blank=True, default='If you answered yes to any of the above, your study is considered\n"High Risk" and will require an executive sponsor to sign off\nexplicitly in the bug and state the known risk.\n\nFor a high risk study, each of the following\nmust be provided and accounted for:\n\n\nFinal Experiment Design\nResponsible: Experiment owner\nAccountable: Shield Team\n\n\nPopulation Size\nResponsible: Experiment owner\nAccountable: SHIELD Team\n\n\nData Analysis\nResponsible: Assigned analyst\nAccountable: SHIELD Team\n\n\nLegal Sign-Off\nResponsible: Experiment owner\nAccountable: SHIELD\n\n\nShipping\nResponsible: Release Management\nAccountable: Shield Team\n\n\nRisk Matrix\nResponsible: Experiment owner\nAccountable: Shield Team\n ', null=True), + model_name="experiment", + name="risks", + field=models.TextField( + blank=True, + default='If you answered yes to any of the above, your study is considered\n"High Risk" and will require an executive sponsor to sign off\nexplicitly in the bug and state the known risk.\n\nFor a high risk study, each of the following\nmust be provided and accounted for:\n\n\nFinal Experiment Design\nResponsible: Experiment owner\nAccountable: Shield Team\n\n\nPopulation Size\nResponsible: Experiment owner\nAccountable: SHIELD Team\n\n\nData Analysis\nResponsible: Assigned analyst\nAccountable: SHIELD Team\n\n\nLegal Sign-Off\nResponsible: Experiment owner\nAccountable: SHIELD\n\n\nShipping\nResponsible: Release Management\nAccountable: Shield Team\n\n\nRisk Matrix\nResponsible: Experiment owner\nAccountable: Shield Team\n ', + null=True, + ), ), migrations.AddField( - model_name='experiment', - name='testing', - field=models.TextField(blank=True, default='QA Status of your code: Green, yellow, red.\n\n\nIf additional QA is required, provide a plan for\ntesting each branch of this study:\n ', null=True), + model_name="experiment", + name="testing", + field=models.TextField( + blank=True, + default="QA Status of your code: Green, yellow, red.\n\n\nIf additional QA is required, provide a plan for\ntesting each branch of this study:\n ", + null=True, + ), ), migrations.AlterField( - model_name='experiment', - name='analysis', - field=models.TextField(blank=True, default='What is the main effect you are looking for and what data will\nyou use to make these decisions? What metrics are you using to measure success\n\n\nWho is the owner of the data analysis for this study?\n\n\nDo you plan on surveying users at the end of the study? Yes/No.\nStrategy and Insights can help create surveys if needed\n ', null=True), + model_name="experiment", + name="analysis", + field=models.TextField( + blank=True, + default="What is the main effect you are looking for and what data will\nyou use to make these decisions? What metrics are you using to measure success\n\n\nWho is the owner of the data analysis for this study?\n\n\nDo you plan on surveying users at the end of the study? Yes/No.\nStrategy and Insights can help create surveys if needed\n ", + null=True, + ), ), migrations.AlterField( - model_name='experiment', - name='firefox_channel', - field=models.CharField(choices=[(None, 'Firefox Channel'), ('Nightly', 'Nightly'), ('Beta', 'Beta'), ('Release', 'Release')], max_length=255), + model_name="experiment", + name="firefox_channel", + field=models.CharField( + choices=[ + (None, "Firefox Channel"), + ("Nightly", "Nightly"), + ("Beta", "Beta"), + ("Release", "Release"), + ], + max_length=255, + ), ), migrations.AlterField( - model_name='experiment', - name='firefox_version', - field=models.CharField(choices=[(None, 'Firefox Version'), ('55.0', 'Firefox 55.0'), ('56.0', 'Firefox 56.0'), ('57.0', 'Firefox 57.0'), ('58.0', 'Firefox 58.0'), ('59.0', 'Firefox 59.0'), ('60.0', 'Firefox 60.0'), ('61.0', 'Firefox 61.0'), ('62.0', 'Firefox 62.0'), ('63.0', 'Firefox 63.0'), ('64.0', 'Firefox 64.0')], max_length=255), + model_name="experiment", + name="firefox_version", + field=models.CharField( + choices=[ + (None, "Firefox Version"), + ("55.0", "Firefox 55.0"), + ("56.0", "Firefox 56.0"), + ("57.0", "Firefox 57.0"), + ("58.0", "Firefox 58.0"), + ("59.0", "Firefox 59.0"), + ("60.0", "Firefox 60.0"), + ("61.0", "Firefox 61.0"), + ("62.0", "Firefox 62.0"), + ("63.0", "Firefox 63.0"), + ("64.0", "Firefox 64.0"), + ], + max_length=255, + ), ), migrations.AlterField( - model_name='experiment', - name='objectives', - field=models.TextField(blank=True, default='What is the objective of this study? Explain in detail.', null=True), + model_name="experiment", + name="objectives", + field=models.TextField( + blank=True, + default="What is the objective of this study? Explain in detail.", + null=True, + ), ), migrations.AlterField( - model_name='experiment', - name='pref_branch', - field=models.CharField(blank=True, choices=[(None, 'Firefox Pref Branch'), ('default', 'default'), ('user', 'user')], max_length=255, null=True), + model_name="experiment", + name="pref_branch", + field=models.CharField( + blank=True, + choices=[ + (None, "Firefox Pref Branch"), + ("default", "default"), + ("user", "user"), + ], + max_length=255, + null=True, + ), ), migrations.AlterField( - model_name='experiment', - name='pref_type', - field=models.CharField(blank=True, choices=[(None, 'Firefox Pref Type'), ('boolean', 'boolean'), ('integer', 'integer'), ('string', 'string')], max_length=255, null=True), + model_name="experiment", + name="pref_type", + field=models.CharField( + blank=True, + choices=[ + (None, "Firefox Pref Type"), + ("boolean", "boolean"), + ("integer", "integer"), + ("string", "string"), + ], + max_length=255, + null=True, + ), ), migrations.AlterField( - model_name='experiment', - name='project', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='experiments', to='projects.Project'), + model_name="experiment", + name="project", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="experiments", + to="projects.Project", + ), ), ] diff --git a/app/experimenter/experiments/migrations/0008_auto_20180503_1747.py b/app/experimenter/experiments/migrations/0008_auto_20180503_1747.py index 800a9b2e63..8eda5cee5c 100644 --- a/app/experimenter/experiments/migrations/0008_auto_20180503_1747.py +++ b/app/experimenter/experiments/migrations/0008_auto_20180503_1747.py @@ -6,43 +6,86 @@ def update_statuses(apps, schema_editor): - Experiment = apps.get_model('experiments', 'Experiment') - ExperimentChangeLog = apps.get_model('experiments', 'ExperimentChangeLog') + Experiment = apps.get_model("experiments", "Experiment") + ExperimentChangeLog = apps.get_model("experiments", "ExperimentChangeLog") - Experiment.objects.filter(status='Created').update(status='Draft') - ExperimentChangeLog.objects.filter(old_status='Created').update(old_status='Draft') - ExperimentChangeLog.objects.filter(new_status='Created').update(new_status='Draft') + Experiment.objects.filter(status="Created").update(status="Draft") + ExperimentChangeLog.objects.filter(old_status="Created").update( + old_status="Draft" + ) + ExperimentChangeLog.objects.filter(new_status="Created").update( + new_status="Draft" + ) - Experiment.objects.filter(status='Pending').update(status='Review') - ExperimentChangeLog.objects.filter(old_status='Pending').update(old_status='Review') - ExperimentChangeLog.objects.filter(new_status='Pending').update(new_status='Review') + Experiment.objects.filter(status="Pending").update(status="Review") + ExperimentChangeLog.objects.filter(old_status="Pending").update( + old_status="Review" + ) + ExperimentChangeLog.objects.filter(new_status="Pending").update( + new_status="Review" + ) - Experiment.objects.filter(status='Launched').update(status='Live') - ExperimentChangeLog.objects.filter(old_status='Launched').update(old_status='Live') - ExperimentChangeLog.objects.filter(new_status='Launched').update(new_status='Live') + Experiment.objects.filter(status="Launched").update(status="Live") + ExperimentChangeLog.objects.filter(old_status="Launched").update( + old_status="Live" + ) + ExperimentChangeLog.objects.filter(new_status="Launched").update( + new_status="Live" + ) class Migration(migrations.Migration): - dependencies = [ - ('experiments', '0007_auto_20180424_2039'), - ] + dependencies = [("experiments", "0007_auto_20180424_2039")] operations = [ migrations.AlterField( - model_name='experiment', - name='status', - field=models.CharField(choices=[('Draft', 'Draft'), ('Review', 'Review'), ('Accepted', 'Accepted'), ('Live', 'Live'), ('Complete', 'Complete'), ('Rejected', 'Rejected')], default='Draft', max_length=255), + model_name="experiment", + name="status", + field=models.CharField( + choices=[ + ("Draft", "Draft"), + ("Review", "Review"), + ("Accepted", "Accepted"), + ("Live", "Live"), + ("Complete", "Complete"), + ("Rejected", "Rejected"), + ], + default="Draft", + max_length=255, + ), ), migrations.AlterField( - model_name='experimentchangelog', - name='new_status', - field=models.CharField(choices=[('Draft', 'Draft'), ('Review', 'Review'), ('Accepted', 'Accepted'), ('Live', 'Live'), ('Complete', 'Complete'), ('Rejected', 'Rejected')], max_length=255), + model_name="experimentchangelog", + name="new_status", + field=models.CharField( + choices=[ + ("Draft", "Draft"), + ("Review", "Review"), + ("Accepted", "Accepted"), + ("Live", "Live"), + ("Complete", "Complete"), + ("Rejected", "Rejected"), + ], + max_length=255, + ), ), migrations.AlterField( - model_name='experimentchangelog', - name='old_status', - field=models.CharField(blank=True, choices=[('Draft', 'Draft'), ('Review', 'Review'), ('Accepted', 'Accepted'), ('Live', 'Live'), ('Complete', 'Complete'), ('Rejected', 'Rejected')], max_length=255, null=True), + model_name="experimentchangelog", + name="old_status", + field=models.CharField( + blank=True, + choices=[ + ("Draft", "Draft"), + ("Review", "Review"), + ("Accepted", "Accepted"), + ("Live", "Live"), + ("Complete", "Complete"), + ("Rejected", "Rejected"), + ], + max_length=255, + null=True, + ), ), migrations.RunPython(update_statuses), ] diff --git a/app/experimenter/experiments/migrations/0009_experiment_owner.py b/app/experimenter/experiments/migrations/0009_experiment_owner.py index 64fbe3f0b7..ad0373f12d 100644 --- a/app/experimenter/experiments/migrations/0009_experiment_owner.py +++ b/app/experimenter/experiments/migrations/0009_experiment_owner.py @@ -11,13 +11,18 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('experiments', '0008_auto_20180503_1747'), + ("experiments", "0008_auto_20180503_1747"), ] operations = [ migrations.AddField( - model_name='experiment', - name='owner', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), + model_name="experiment", + name="owner", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ) ] diff --git a/app/experimenter/experiments/migrations/0010_experiment_archived.py b/app/experimenter/experiments/migrations/0010_experiment_archived.py index 160848d8f7..ab3e58695b 100644 --- a/app/experimenter/experiments/migrations/0010_experiment_archived.py +++ b/app/experimenter/experiments/migrations/0010_experiment_archived.py @@ -7,14 +7,12 @@ class Migration(migrations.Migration): - dependencies = [ - ('experiments', '0009_experiment_owner'), - ] + dependencies = [("experiments", "0009_experiment_owner")] operations = [ migrations.AddField( - model_name='experiment', - name='archived', + model_name="experiment", + name="archived", field=models.BooleanField(default=False), - ), + ) ] diff --git a/app/experimenter/experiments/models.py b/app/experimenter/experiments/models.py index 60fd687ef9..3f899fdc7d 100644 --- a/app/experimenter/experiments/models.py +++ b/app/experimenter/experiments/models.py @@ -16,18 +16,16 @@ class ExperimentManager(models.Manager): def get_queryset(self): return ( - super().get_queryset() - .annotate(latest_change=Max('changes__changed_on')) + super() + .get_queryset() + .annotate(latest_change=Max("changes__changed_on")) ) class Experiment(ExperimentConstants, models.Model): owner = models.ForeignKey(get_user_model(), blank=True, null=True) project = models.ForeignKey( - 'projects.Project', - blank=True, - null=True, - related_name='experiments', + "projects.Project", blank=True, null=True, related_name="experiments" ) status = models.CharField( max_length=255, @@ -36,10 +34,12 @@ class Experiment(ExperimentConstants, models.Model): ) archived = models.BooleanField(default=False) name = models.CharField( - max_length=255, unique=True, blank=False, null=False) + max_length=255, unique=True, blank=False, null=False + ) slug = models.SlugField( - max_length=255, unique=True, blank=False, null=False) - short_description = models.TextField(default='', blank=True, null=True) + max_length=255, unique=True, blank=False, null=False + ) + short_description = models.TextField(default="", blank=True, null=True) proposed_start_date = models.DateField(blank=True, null=True) proposed_end_date = models.DateField(blank=True, null=True) pref_key = models.CharField(max_length=255, blank=True, null=True) @@ -56,30 +56,40 @@ class Experiment(ExperimentConstants, models.Model): null=True, ) population_percent = models.DecimalField( - max_digits=7, decimal_places=4, default='0') + max_digits=7, decimal_places=4, default="0" + ) firefox_version = models.CharField( - max_length=255, choices=ExperimentConstants.VERSION_CHOICES) + max_length=255, choices=ExperimentConstants.VERSION_CHOICES + ) firefox_channel = models.CharField( - max_length=255, choices=ExperimentConstants.CHANNEL_CHOICES) - client_matching = models.TextField(default='', blank=True) + max_length=255, choices=ExperimentConstants.CHANNEL_CHOICES + ) + client_matching = models.TextField(default="", blank=True) objectives = models.TextField( - default=ExperimentConstants.OBJECTIVES_DEFAULT, blank=True, null=True) + default=ExperimentConstants.OBJECTIVES_DEFAULT, blank=True, null=True + ) analysis = models.TextField( - default=ExperimentConstants.ANALYSIS_DEFAULT, blank=True, null=True) + default=ExperimentConstants.ANALYSIS_DEFAULT, blank=True, null=True + ) risk_partner_related = models.NullBooleanField( - default=None, blank=True, null=True) - risk_brand = models.NullBooleanField( - default=None, blank=True, null=True) + default=None, blank=True, null=True + ) + risk_brand = models.NullBooleanField(default=None, blank=True, null=True) risk_fast_shipped = models.NullBooleanField( - default=None, blank=True, null=True) + default=None, blank=True, null=True + ) risk_confidential = models.NullBooleanField( - default=None, blank=True, null=True) + default=None, blank=True, null=True + ) risk_release_population = models.NullBooleanField( - default=None, blank=True, null=True) + default=None, blank=True, null=True + ) risks = models.TextField( - default=ExperimentConstants.RISKS_DEFAULT, blank=True, null=True) + default=ExperimentConstants.RISKS_DEFAULT, blank=True, null=True + ) testing = models.TextField( - default=ExperimentConstants.TESTING_DEFAULT, blank=True, null=True) + default=ExperimentConstants.TESTING_DEFAULT, blank=True, null=True + ) total_users = models.PositiveIntegerField(default=0) enrollment_dashboard_url = models.URLField(blank=True, null=True) dashboard_url = models.URLField(blank=True, null=True) @@ -91,8 +101,8 @@ def __str__(self): # pragma: no cover return self.name class Meta: - verbose_name = 'Experiment' - verbose_name_plural = 'Experiments' + verbose_name = "Experiment" + verbose_name_plural = "Experiments" @cached_property def control(self): @@ -120,8 +130,7 @@ def is_begun(self): def _transition_date(self, start_state, end_state): change = self.changes.filter( - old_status=start_state, - new_status=end_state, + old_status=start_state, new_status=end_state ) if change.count() == 1: @@ -129,7 +138,7 @@ def _transition_date(self, start_state, end_state): @property def population(self): - return '{percent:g}% of {channel} Firefox {version}'.format( + return "{percent:g}% of {channel} Firefox {version}".format( percent=float(self.population_percent), version=self.firefox_version, channel=self.firefox_channel, @@ -137,50 +146,43 @@ def population(self): @property def start_date(self): - return self._transition_date( - self.STATUS_ACCEPTED, - self.STATUS_LIVE, - ) + return self._transition_date(self.STATUS_ACCEPTED, self.STATUS_LIVE) @property def end_date(self): - return self._transition_date( - self.STATUS_LIVE, - self.STATUS_COMPLETE, - ) + return self._transition_date(self.STATUS_LIVE, self.STATUS_COMPLETE) @property def experiment_slug(self): - return 'pref-flip-{project_slug}-{experiment_slug}'.format( - project_slug=self.project.slug, - experiment_slug=self.slug, + return "pref-flip-{project_slug}-{experiment_slug}".format( + project_slug=self.project.slug, experiment_slug=self.slug ) @property def experiment_url(self): return urljoin( - 'https://{host}'.format(host=settings.HOSTNAME), - reverse('experiments-detail', args=[self.slug]) + "https://{host}".format(host=settings.HOSTNAME), + reverse("experiments-detail", args=[self.slug]), ) @property def accept_url(self): return urljoin( - 'https://{host}'.format(host=settings.HOSTNAME), - reverse('experiments-api-accept', kwargs={'slug': self.slug}) + "https://{host}".format(host=settings.HOSTNAME), + reverse("experiments-api-accept", kwargs={"slug": self.slug}), ) @property def reject_url(self): return urljoin( - 'https://{host}'.format(host=settings.HOSTNAME), - reverse('experiments-api-reject', kwargs={'slug': self.slug}) + "https://{host}".format(host=settings.HOSTNAME), + reverse("experiments-api-reject", kwargs={"slug": self.slug}), ) @property def test_tube_url(self): return ( - 'https://firefox-test-tube.herokuapp.com/experiments/{slug}/' + "https://firefox-test-tube.herokuapp.com/experiments/{slug}/" ).format(slug=self.slug) @property @@ -208,36 +210,35 @@ def completed_variants(self): @property def completed_objectives(self): return ( - self.objectives != self.OBJECTIVES_DEFAULT and - self.analysis != self.ANALYSIS_DEFAULT + self.objectives != self.OBJECTIVES_DEFAULT + and self.analysis != self.ANALYSIS_DEFAULT ) @property def completed_risks(self): return ( - None not in self._risk_questions and - self.testing != self.TESTING_DEFAULT + None not in self._risk_questions + and self.testing != self.TESTING_DEFAULT ) @property def is_ready_for_review(self): return ( - self.completed_overview and - self.completed_variants and - self.completed_objectives and - self.completed_risks + self.completed_overview + and self.completed_variants + and self.completed_objectives + and self.completed_risks ) class ExperimentVariant(models.Model): experiment = models.ForeignKey( - Experiment, blank=False, null=False, related_name='variants') - name = models.CharField( - max_length=255, blank=False, null=False) - slug = models.SlugField( - max_length=255, blank=False, null=False) + Experiment, blank=False, null=False, related_name="variants" + ) + name = models.CharField(max_length=255, blank=False, null=False) + slug = models.SlugField(max_length=255, blank=False, null=False) is_control = models.BooleanField(default=False) - description = models.TextField(default='') + description = models.TextField(default="") ratio = models.PositiveIntegerField(default=1) value = JSONField(default=False) @@ -245,34 +246,32 @@ def __str__(self): # pragma: no cover return self.name class Meta: - verbose_name = 'Experiment Variant' - verbose_name_plural = 'Experiment Variants' + verbose_name = "Experiment Variant" + verbose_name_plural = "Experiment Variants" unique_together = ( - ('slug', 'experiment'), - ('is_control', 'experiment'), + ("slug", "experiment"), + ("is_control", "experiment"), ) class ExperimentChangeLogManager(models.Manager): def latest(self): - return self.all().order_by('-changed_on').first() + return self.all().order_by("-changed_on").first() class ExperimentChangeLog(models.Model): - STATUS_NONE_DRAFT = 'Created Draft' - STATUS_DRAFT_DRAFT = 'Edited Draft' - STATUS_DRAFT_REVIEW = 'Submitted for Review' - STATUS_REVIEW_DRAFT = 'Cancelled Review Request' - STATUS_REVIEW_ACCEPTED = 'Review Approved' - STATUS_REVIEW_REJECTED = 'Review Rejected' - STATUS_ACCEPTED_LIVE = 'Launched' - STATUS_LIVE_COMPLETE = 'Complete' + STATUS_NONE_DRAFT = "Created Draft" + STATUS_DRAFT_DRAFT = "Edited Draft" + STATUS_DRAFT_REVIEW = "Submitted for Review" + STATUS_REVIEW_DRAFT = "Cancelled Review Request" + STATUS_REVIEW_ACCEPTED = "Review Approved" + STATUS_REVIEW_REJECTED = "Review Rejected" + STATUS_ACCEPTED_LIVE = "Launched" + STATUS_LIVE_COMPLETE = "Complete" PRETTY_STATUS_LABELS = { - None: { - Experiment.STATUS_DRAFT: STATUS_NONE_DRAFT, - }, + None: {Experiment.STATUS_DRAFT: STATUS_NONE_DRAFT}, Experiment.STATUS_DRAFT: { Experiment.STATUS_DRAFT: STATUS_DRAFT_DRAFT, Experiment.STATUS_REVIEW: STATUS_DRAFT_REVIEW, @@ -283,10 +282,10 @@ class ExperimentChangeLog(models.Model): Experiment.STATUS_REJECTED: STATUS_REVIEW_REJECTED, }, Experiment.STATUS_ACCEPTED: { - Experiment.STATUS_LIVE: STATUS_ACCEPTED_LIVE, + Experiment.STATUS_LIVE: STATUS_ACCEPTED_LIVE }, Experiment.STATUS_LIVE: { - Experiment.STATUS_COMPLETE: STATUS_LIVE_COMPLETE, + Experiment.STATUS_COMPLETE: STATUS_LIVE_COMPLETE }, } @@ -294,7 +293,8 @@ def current_datetime(): return timezone.now() experiment = models.ForeignKey( - Experiment, blank=False, null=False, related_name='changes') + Experiment, blank=False, null=False, related_name="changes" + ) changed_on = models.DateTimeField(default=current_datetime) changed_by = models.ForeignKey(get_user_model()) old_status = models.CharField( @@ -314,18 +314,19 @@ def current_datetime(): objects = ExperimentChangeLogManager() class Meta: - verbose_name = 'Experiment Change Log' - verbose_name_plural = 'Experiment Change Logs' - ordering = ('changed_on',) + verbose_name = "Experiment Change Log" + verbose_name_plural = "Experiment Change Logs" + ordering = ("changed_on",) def __str__(self): # pragma: no cover - return '{status} by {updater} on {datetime}'.format( - status=self.new_status, - updater=self.changed_by, - datetime=self.changed_on.date(), + return "{status} by {updater} on {datetime}".format( + status=self.new_status, + updater=self.changed_by, + datetime=self.changed_on.date(), ) @property def pretty_status(self): - return self.PRETTY_STATUS_LABELS.get( - self.old_status, {}).get(self.new_status, '') + return self.PRETTY_STATUS_LABELS.get(self.old_status, {}).get( + self.new_status, "" + ) diff --git a/app/experimenter/experiments/serializers.py b/app/experimenter/experiments/serializers.py index 58c5cdedcc..fd241ac4a6 100644 --- a/app/experimenter/experiments/serializers.py +++ b/app/experimenter/experiments/serializers.py @@ -2,10 +2,7 @@ from rest_framework import serializers -from experimenter.experiments.models import ( - Experiment, - ExperimentVariant, -) +from experimenter.experiments.models import Experiment, ExperimentVariant class JSTimestampField(serializers.Field): @@ -25,20 +22,14 @@ class ExperimentVariantSerializer(serializers.ModelSerializer): class Meta: model = ExperimentVariant - fields = ( - 'description', - 'name', - 'ratio', - 'slug', - 'value', - ) + fields = ("description", "name", "ratio", "slug", "value") class ExperimentSerializer(serializers.ModelSerializer): control = ExperimentVariantSerializer() end_date = JSTimestampField() - project_name = serializers.ReadOnlyField(source='project.name') - project_slug = serializers.ReadOnlyField(source='project.slug') + project_name = serializers.ReadOnlyField(source="project.name") + project_slug = serializers.ReadOnlyField(source="project.slug") proposed_end_date = JSTimestampField() proposed_start_date = JSTimestampField() start_date = JSTimestampField() @@ -47,29 +38,29 @@ class ExperimentSerializer(serializers.ModelSerializer): class Meta: model = Experiment fields = ( - 'accept_url', - 'client_matching', - 'control', - 'end_date', - 'experiment_slug', - 'experiment_url', - 'firefox_channel', - 'firefox_version', - 'name', - 'objectives', - 'population', - 'population_percent', - 'pref_branch', - 'pref_branch', - 'pref_key', - 'pref_type', - 'project_name', - 'project_slug', - 'proposed_end_date', - 'proposed_start_date', - 'reject_url', - 'short_description', - 'slug', - 'start_date', - 'variant', + "accept_url", + "client_matching", + "control", + "end_date", + "experiment_slug", + "experiment_url", + "firefox_channel", + "firefox_version", + "name", + "objectives", + "population", + "population_percent", + "pref_branch", + "pref_branch", + "pref_key", + "pref_type", + "project_name", + "project_slug", + "proposed_end_date", + "proposed_start_date", + "reject_url", + "short_description", + "slug", + "start_date", + "variant", ) diff --git a/app/experimenter/experiments/tests/factories.py b/app/experimenter/experiments/tests/factories.py index 0f343a2cae..461bd2d903 100644 --- a/app/experimenter/experiments/tests/factories.py +++ b/app/experimenter/experiments/tests/factories.py @@ -9,7 +9,10 @@ from experimenter.openidc.tests.factories import UserFactory from experimenter.experiments.models import ( - Experiment, ExperimentVariant, ExperimentChangeLog) + Experiment, + ExperimentChangeLog, + ExperimentVariant, +) from experimenter.projects.tests.factories import ProjectFactory faker = FakerFactory.create() @@ -22,50 +25,65 @@ class ExperimentFactory(factory.django.DjangoModelFactory): slug = factory.LazyAttribute(lambda o: slugify(o.name)) archived = False short_description = factory.LazyAttribute( - lambda o: faker.text(random.randint(100, 500))) + lambda o: faker.text(random.randint(100, 500)) + ) proposed_start_date = factory.LazyAttribute( lambda o: ( - datetime.date.today() + - datetime.timedelta(days=random.randint(1, 10)) - )) + datetime.date.today() + + datetime.timedelta(days=random.randint(1, 10)) + ) + ) proposed_end_date = factory.LazyAttribute( lambda o: ( - o.proposed_start_date + - datetime.timedelta(days=random.randint(1, 10)) - )) + o.proposed_start_date + + datetime.timedelta(days=random.randint(1, 10)) + ) + ) pref_key = factory.LazyAttribute( - lambda o: 'browser.{pref}.enabled'.format( - pref=faker.catch_phrase().replace(' ', '.').lower())) + lambda o: "browser.{pref}.enabled".format( + pref=faker.catch_phrase().replace(" ", ".").lower() + ) + ) pref_type = factory.LazyAttribute( - lambda o: random.choice(Experiment.PREF_TYPE_CHOICES[1:])[0]) + lambda o: random.choice(Experiment.PREF_TYPE_CHOICES[1:])[0] + ) pref_branch = factory.LazyAttribute( - lambda o: random.choice(Experiment.PREF_BRANCH_CHOICES[1:])[0]) + lambda o: random.choice(Experiment.PREF_BRANCH_CHOICES[1:])[0] + ) firefox_version = factory.LazyAttribute( - lambda o: random.choice(Experiment.VERSION_CHOICES[1:])[0]) + lambda o: random.choice(Experiment.VERSION_CHOICES[1:])[0] + ) firefox_channel = factory.LazyAttribute( - lambda o: random.choice(Experiment.CHANNEL_CHOICES[1:])[0]) + lambda o: random.choice(Experiment.CHANNEL_CHOICES[1:])[0] + ) objectives = factory.LazyAttribute( - lambda o: faker.text(random.randint(500, 5000))) + lambda o: faker.text(random.randint(500, 5000)) + ) analysis = factory.LazyAttribute( - lambda o: faker.text(random.randint(500, 5000))) + lambda o: faker.text(random.randint(500, 5000)) + ) testing = factory.LazyAttribute( - lambda o: faker.text(random.randint(500, 5000))) + lambda o: faker.text(random.randint(500, 5000)) + ) risks = factory.LazyAttribute( - lambda o: faker.text(random.randint(500, 5000))) + lambda o: faker.text(random.randint(500, 5000)) + ) total_users = factory.LazyAttribute( - lambda o: random.randint(100000, 1000000)) + lambda o: random.randint(100000, 1000000) + ) risk_partner_related = False risk_brand = False risk_fast_shipped = False risk_confidential = False risk_release_population = False - enrollment_dashboard_url = 'http://www.example.com/enrollment' - dashboard_url = 'http://www.example.com/dashboard' - dashboard_image_url = 'http://www.example.com/dashboard.png' + enrollment_dashboard_url = "http://www.example.com/enrollment" + dashboard_url = "http://www.example.com/dashboard" + dashboard_image_url = "http://www.example.com/dashboard.png" population_percent = factory.LazyAttribute( - lambda o: decimal.Decimal(random.randint(1, 10) * 10)) + lambda o: decimal.Decimal(random.randint(1, 10) * 10) + ) client_matching = ( - 'Locales: en-US, en-CA, en-GB\nGeos: US, CA, GB\n' + "Locales: en-US, en-CA, en-GB\nGeos: US, CA, GB\n" 'Some "additional" filtering' ) @@ -83,9 +101,8 @@ def create_with_variants(cls, *args, **kwargs): def create_with_status(cls, target_status, *args, **kwargs): experiment = cls.create_with_variants(*args, **kwargs) - now = ( - datetime.datetime.now() - - datetime.timedelta(days=random.randint(100, 200)) + now = datetime.datetime.now() - datetime.timedelta( + days=random.randint(100, 200) ) old_status = None @@ -111,10 +128,10 @@ def create_with_status(cls, target_status, *args, **kwargs): class BaseExperimentVariantFactory(factory.django.DjangoModelFactory): + description = factory.LazyAttribute(lambda o: faker.text()) experiment = factory.SubFactory(ExperimentFactory) name = factory.LazyAttribute(lambda o: faker.catch_phrase()) slug = factory.LazyAttribute(lambda o: slugify(o.name)) - description = factory.LazyAttribute(lambda o: faker.text()) class Meta: model = ExperimentVariant @@ -141,10 +158,14 @@ class ExperimentControlFactory(ExperimentVariantFactory): class ExperimentChangeLogFactory(factory.django.DjangoModelFactory): experiment = factory.SubFactory(ExperimentFactory) changed_by = factory.SubFactory(UserFactory) - old_status = factory.LazyAttribute(lambda o: random.choice( - Experiment.STATUS_CHOICES)[0]) - new_status = factory.LazyAttribute(lambda o: random.choice( - Experiment.STATUS_TRANSITIONS[o.old_status] or [o.old_status])) + old_status = factory.LazyAttribute( + lambda o: random.choice(Experiment.STATUS_CHOICES)[0] + ) + new_status = factory.LazyAttribute( + lambda o: random.choice( + Experiment.STATUS_TRANSITIONS[o.old_status] or [o.old_status] + ) + ) message = factory.LazyAttribute(lambda o: faker.text()) class Meta: diff --git a/app/experimenter/experiments/tests/test_admin.py b/app/experimenter/experiments/tests/test_admin.py index 470154778e..8e2c3496cc 100644 --- a/app/experimenter/experiments/tests/test_admin.py +++ b/app/experimenter/experiments/tests/test_admin.py @@ -29,8 +29,8 @@ def test_save_sets_is_control_to_True(self): experiment = ExperimentFactory.create() variant_data = ExperimentControlFactory.attributes() - variant_data['experiment'] = experiment.id - variant_data['is_control'] = False + variant_data["experiment"] = experiment.id + variant_data["is_control"] = False form = ControlVariantModelForm(data=variant_data) @@ -86,5 +86,6 @@ def test_show_dashboard_url_returns_link(self): self.assertEqual( experiment_admin.show_dashboard_url(experiment), '{url}'.format( - url=experiment.dashboard_url) + url=experiment.dashboard_url + ), ) diff --git a/app/experimenter/experiments/tests/test_api_views.py b/app/experimenter/experiments/tests/test_api_views.py index 59a41ece3d..d24aa5a448 100644 --- a/app/experimenter/experiments/tests/test_api_views.py +++ b/app/experimenter/experiments/tests/test_api_views.py @@ -19,13 +19,14 @@ def test_list_view_serializes_experiments(self): experiment = ExperimentFactory.create_with_variants() experiments.append(experiment) - response = self.client.get(reverse('experiments-api-list')) + response = self.client.get(reverse("experiments-api-list")) self.assertEqual(response.status_code, 200) json_data = json.loads(response.content) serialized_experiments = ExperimentSerializer( - Experiment.objects.all(), many=True).data + Experiment.objects.all(), many=True + ).data self.assertEqual(serialized_experiments, json_data) @@ -40,17 +41,20 @@ def test_list_view_filters_by_project_slug(self): # started project experiments should be included for i in range(3): experiment = ExperimentFactory.create_with_variants( - project=project) + project=project + ) project_experiments.append(experiment) response = self.client.get( - reverse('experiments-api-list'), {'project__slug': project.slug}) + reverse("experiments-api-list"), {"project__slug": project.slug} + ) self.assertEqual(response.status_code, 200) json_data = json.loads(response.content) serialized_experiments = ExperimentSerializer( - project.experiments.all(), many=True).data + project.experiments.all(), many=True + ).data self.assertEqual(serialized_experiments, json_data) @@ -69,16 +73,17 @@ def test_list_view_filters_by_status(self): pending_experiments.append(experiment) response = self.client.get( - reverse('experiments-api-list'), - {'status': Experiment.STATUS_REVIEW}, + reverse("experiments-api-list"), + {"status": Experiment.STATUS_REVIEW}, ) self.assertEqual(response.status_code, 200) json_data = json.loads(response.content) serialized_experiments = ExperimentSerializer( - Experiment.objects.filter( - status=Experiment.STATUS_REVIEW), many=True).data + Experiment.objects.filter(status=Experiment.STATUS_REVIEW), + many=True, + ).data self.assertEqual(serialized_experiments, json_data) @@ -86,7 +91,7 @@ def test_list_view_filters_by_status(self): class TestExperimentAcceptView(TestCase): def test_post_to_accept_view_sets_status_accepted(self): - user_email = 'user@example.com' + user_email = "user@example.com" experiment = ExperimentFactory.create_with_variants() experiment.status = experiment.STATUS_REVIEW @@ -94,7 +99,8 @@ def test_post_to_accept_view_sets_status_accepted(self): response = self.client.patch( reverse( - 'experiments-api-accept', kwargs={'slug': experiment.slug}), + "experiments-api-accept", kwargs={"slug": experiment.slug} + ), **{settings.OPENIDC_EMAIL_HEADER: user_email}, ) @@ -113,8 +119,9 @@ def test_post_to_accept_raises_404_for_non_pending_experiment(self): response = self.client.patch( reverse( - 'experiments-api-accept', kwargs={'slug': experiment.slug}), - **{settings.OPENIDC_EMAIL_HEADER: 'user@example.com'}, + "experiments-api-accept", kwargs={"slug": experiment.slug} + ), + **{settings.OPENIDC_EMAIL_HEADER: "user@example.com"}, ) self.assertEqual(response.status_code, 404) @@ -123,8 +130,8 @@ def test_post_to_accept_raises_404_for_non_pending_experiment(self): class TestExperimentRejectView(TestCase): def test_post_to_reject_view_sets_status_rejected(self): - user_email = 'user@example.com' - rejection_message = 'This experiment was rejected for reasons.' + user_email = "user@example.com" + rejection_message = "This experiment was rejected for reasons." experiment = ExperimentFactory.create_with_variants() experiment.status = experiment.STATUS_REVIEW @@ -132,9 +139,10 @@ def test_post_to_reject_view_sets_status_rejected(self): response = self.client.patch( reverse( - 'experiments-api-reject', kwargs={'slug': experiment.slug}), - data=json.dumps({'message': rejection_message}), - content_type='application/json', + "experiments-api-reject", kwargs={"slug": experiment.slug} + ), + data=json.dumps({"message": rejection_message}), + content_type="application/json", **{settings.OPENIDC_EMAIL_HEADER: user_email}, ) @@ -154,8 +162,9 @@ def test_post_to_reject_raises_404_for_non_pending_experiment(self): response = self.client.patch( reverse( - 'experiments-api-reject', kwargs={'slug': experiment.slug}), - **{settings.OPENIDC_EMAIL_HEADER: 'user@example.com'}, + "experiments-api-reject", kwargs={"slug": experiment.slug} + ), + **{settings.OPENIDC_EMAIL_HEADER: "user@example.com"}, ) self.assertEqual(response.status_code, 404) diff --git a/app/experimenter/experiments/tests/test_forms.py b/app/experimenter/experiments/tests/test_forms.py index bb35e71b73..513059dd0f 100644 --- a/app/experimenter/experiments/tests/test_forms.py +++ b/app/experimenter/experiments/tests/test_forms.py @@ -7,25 +7,20 @@ from django.core.exceptions import ValidationError from django.test import TestCase -from experimenter.experiments.models import ( - Experiment, - ExperimentVariant, -) from experimenter.experiments.forms import ( ChangeLogMixin, ControlVariantForm, - ExperimentStatusForm, ExperimentObjectivesForm, ExperimentOverviewForm, ExperimentRisksForm, + ExperimentStatusForm, ExperimentVariantsForm, ExperimentalVariantForm, JSONField, NameSlugMixin, ) -from experimenter.experiments.tests.factories import ( - ExperimentFactory, -) +from experimenter.experiments.models import Experiment, ExperimentVariant +from experimenter.experiments.tests.factories import ExperimentFactory from experimenter.openidc.tests.factories import UserFactory from experimenter.projects.tests.factories import ProjectFactory @@ -33,13 +28,13 @@ class TestJSONField(TestCase): def test_jsonfield_accepts_valid_json(self): - valid_json = json.dumps({'a': True, 2: ['b', 3, 4.0]}) + valid_json = json.dumps({"a": True, 2: ["b", 3, 4.0]}) field = JSONField() cleaned = field.clean(valid_json) self.assertEqual(cleaned, valid_json) def test_jsonfield_rejects_invalid_json(self): - invalid_json = '{this isnt valid' + invalid_json = "{this isnt valid" field = JSONField() with self.assertRaises(ValidationError): @@ -49,17 +44,18 @@ def test_jsonfield_rejects_invalid_json(self): class TestNameSlugMixin(TestCase): def test_name_slug_mixin_creates_slug_from_name(self): + class TestForm(NameSlugMixin, forms.Form): name = forms.CharField() slug = forms.CharField(required=False) - name = 'A Name' - expected_slug = 'a-name' + name = "A Name" + expected_slug = "a-name" - form = TestForm({'name': name}) + form = TestForm({"name": name}) self.assertTrue(form.is_valid()) - self.assertEqual(form.cleaned_data['name'], name) - self.assertEqual(form.cleaned_data['slug'], expected_slug) + self.assertEqual(form.cleaned_data["name"], name) + self.assertEqual(form.cleaned_data["slug"], expected_slug) class TestControlVariantForm(TestCase): @@ -68,15 +64,15 @@ def test_form_creates_control_variant(self): experiment = ExperimentFactory.create() data = { - 'experiment': experiment.id, - 'name': 'The Control Variant', - 'description': 'Its the control! So controlly.', - 'ratio': 50, - 'value': 'true', + "experiment": experiment.id, + "name": "The Control Variant", + "description": "Its the control! So controlly.", + "ratio": 50, + "value": "true", } prefixed_data = { - '{}-{}'.format(ControlVariantForm.prefix, key): value + "{}-{}".format(ControlVariantForm.prefix, key): value for key, value in data.items() } @@ -89,11 +85,11 @@ def test_form_creates_control_variant(self): self.assertEqual(variant.experiment.id, experiment.id) self.assertTrue(variant.is_control) - self.assertEqual(variant.name, data['name']) - self.assertEqual(variant.description, data['description']) - self.assertEqual(variant.slug, 'the-control-variant') - self.assertEqual(variant.ratio, data['ratio']) - self.assertEqual(variant.value, 'true') + self.assertEqual(variant.name, data["name"]) + self.assertEqual(variant.description, data["description"]) + self.assertEqual(variant.slug, "the-control-variant") + self.assertEqual(variant.ratio, data["ratio"]) + self.assertEqual(variant.value, "true") class TestExperimentalVariantForm(TestCase): @@ -102,15 +98,15 @@ def test_form_creates_experimental_variant(self): experiment = ExperimentFactory.create() data = { - 'experiment': experiment.id, - 'name': 'The Experimental Variant', - 'description': 'Its the experimental! So experimentally.', - 'ratio': 50, - 'value': 'false', + "experiment": experiment.id, + "name": "The Experimental Variant", + "description": "Its the experimental! So experimentally.", + "ratio": 50, + "value": "false", } prefixed_data = { - '{}-{}'.format(ExperimentalVariantForm.prefix, key): value + "{}-{}".format(ExperimentalVariantForm.prefix, key): value for key, value in data.items() } @@ -123,11 +119,11 @@ def test_form_creates_experimental_variant(self): self.assertEqual(variant.experiment.id, experiment.id) self.assertFalse(variant.is_control) - self.assertEqual(variant.name, data['name']) - self.assertEqual(variant.description, data['description']) - self.assertEqual(variant.slug, 'the-experimental-variant') - self.assertEqual(variant.ratio, data['ratio']) - self.assertEqual(variant.value, 'false') + self.assertEqual(variant.name, data["name"]) + self.assertEqual(variant.description, data["description"]) + self.assertEqual(variant.slug, "the-experimental-variant") + self.assertEqual(variant.ratio, data["ratio"]) + self.assertEqual(variant.value, "false") class MockRequestMixin(object): @@ -143,11 +139,12 @@ def setUp(self): class TestChangeLogMixin(MockRequestMixin, TestCase): def test_mixin_creates_change_log_with_request_user_on_save(self): + class TestForm(ChangeLogMixin, forms.ModelForm): class Meta: model = Experiment - fields = ('name',) + fields = ("name",) data = ExperimentFactory.attributes() form = TestForm(request=self.request, data=data) @@ -171,11 +168,11 @@ class TestForm(ChangeLogMixin, forms.ModelForm): class Meta: model = Experiment - fields = ('status',) + fields = ("status",) form = TestForm( request=self.request, - data={'status': new_status}, + data={"status": new_status}, instance=experiment, ) self.assertTrue(form.is_valid()) @@ -198,17 +195,18 @@ def setUp(self): self.project = ProjectFactory.create() self.data = { - 'owner': self.user.id, - 'project': self.project.id, - 'name': 'A new experiment!', - 'short_description': 'Let us learn new things', - 'population_percent': '10', - 'firefox_version': Experiment.VERSION_CHOICES[-1][0], - 'firefox_channel': Experiment.CHANNEL_NIGHTLY, - 'client_matching': 'en-us only please', - 'proposed_start_date': datetime.date.today(), - 'proposed_end_date': ( - datetime.date.today() + datetime.timedelta(days=1)), + "owner": self.user.id, + "project": self.project.id, + "name": "A new experiment!", + "short_description": "Let us learn new things", + "population_percent": "10", + "firefox_version": Experiment.VERSION_CHOICES[-1][0], + "firefox_channel": Experiment.CHANNEL_NIGHTLY, + "client_matching": "en-us only please", + "proposed_start_date": datetime.date.today(), + "proposed_end_date": ( + datetime.date.today() + datetime.timedelta(days=1) + ), } def test_form_creates_experiment(self): @@ -219,22 +217,29 @@ def test_form_creates_experiment(self): self.assertEqual(experiment.owner, self.user) self.assertEqual(experiment.project, self.project) self.assertEqual(experiment.status, experiment.STATUS_DRAFT) - self.assertEqual(experiment.name, self.data['name']) - self.assertEqual(experiment.slug, 'a-new-experiment') + self.assertEqual(experiment.name, self.data["name"]) + self.assertEqual(experiment.slug, "a-new-experiment") self.assertEqual( - experiment.short_description, self.data['short_description']) + experiment.short_description, self.data["short_description"] + ) self.assertEqual( - experiment.population_percent, decimal.Decimal('10.000')) + experiment.population_percent, decimal.Decimal("10.000") + ) self.assertEqual( - experiment.firefox_version, self.data['firefox_version']) + experiment.firefox_version, self.data["firefox_version"] + ) self.assertEqual( - experiment.firefox_channel, self.data['firefox_channel']) + experiment.firefox_channel, self.data["firefox_channel"] + ) self.assertEqual( - experiment.client_matching, self.data['client_matching']) + experiment.client_matching, self.data["client_matching"] + ) self.assertEqual( - experiment.proposed_start_date, self.data['proposed_start_date']) + experiment.proposed_start_date, self.data["proposed_start_date"] + ) self.assertEqual( - experiment.proposed_end_date, self.data['proposed_end_date']) + experiment.proposed_end_date, self.data["proposed_end_date"] + ) self.assertEqual(experiment.changes.count(), 1) change = experiment.changes.get() @@ -243,22 +248,22 @@ def test_form_creates_experiment(self): self.assertEqual(change.changed_by, self.request.user) def test_form_is_invalid_if_population_percent_is_0(self): - self.data['population_percent'] = '0' + self.data["population_percent"] = "0" form = ExperimentOverviewForm(request=self.request, data=self.data) self.assertFalse(form.is_valid()) - self.assertIn('population_percent', form.errors) + self.assertIn("population_percent", form.errors) def test_form_is_invalid_if_population_percent_below_0(self): - self.data['population_percent'] = '-1' + self.data["population_percent"] = "-1" form = ExperimentOverviewForm(request=self.request, data=self.data) self.assertFalse(form.is_valid()) - self.assertIn('population_percent', form.errors) + self.assertIn("population_percent", form.errors) def test_form_is_invalid_if_population_percent_above_100(self): - self.data['population_percent'] = '101' + self.data["population_percent"] = "101" form = ExperimentOverviewForm(request=self.request, data=self.data) self.assertFalse(form.is_valid()) - self.assertIn('population_percent', form.errors) + self.assertIn("population_percent", form.errors) class TestExperimentVariantsForm(MockRequestMixin, TestCase): @@ -267,63 +272,74 @@ def setUp(self): super().setUp() self.data = { - 'pref_key': 'browser.testing.tests-enabled', - 'pref_type': Experiment.PREF_TYPE_BOOL, - 'pref_branch': Experiment.PREF_BRANCH_DEFAULT, - 'control-name': 'The Control Variant', - 'control-description': 'Its the control! So controlly.', - 'control-ratio': 60, - 'control-value': 'false', - 'experimental-name': 'The Experimental Variant', - 'experimental-description': ( - 'Its the experimental! So experimentally.'), - 'experimental-ratio': 40, - 'experimental-value': 'true', + "pref_key": "browser.testing.tests-enabled", + "pref_type": Experiment.PREF_TYPE_BOOL, + "pref_branch": Experiment.PREF_BRANCH_DEFAULT, + "control-name": "The Control Variant", + "control-description": "Its the control! So controlly.", + "control-ratio": 60, + "control-value": "false", + "experimental-name": "The Experimental Variant", + "experimental-description": ( + "Its the experimental! So experimentally." + ), + "experimental-ratio": 40, + "experimental-value": "true", } def test_form_saves_variants(self): created_experiment = ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT) + Experiment.STATUS_DRAFT + ) form = ExperimentVariantsForm( - request=self.request, data=self.data, instance=created_experiment) + request=self.request, data=self.data, instance=created_experiment + ) self.assertTrue(form.is_valid()) experiment = form.save() - self.assertEqual(experiment.pref_key, self.data['pref_key']) - self.assertEqual(experiment.pref_type, self.data['pref_type']) - self.assertEqual(experiment.pref_branch, self.data['pref_branch']) - self.assertEqual(experiment.control.name, self.data['control-name']) + self.assertEqual(experiment.pref_key, self.data["pref_key"]) + self.assertEqual(experiment.pref_type, self.data["pref_type"]) + self.assertEqual(experiment.pref_branch, self.data["pref_branch"]) + self.assertEqual(experiment.control.name, self.data["control-name"]) self.assertEqual( - experiment.control.description, self.data['control-description']) - self.assertEqual(experiment.control.ratio, self.data['control-ratio']) - self.assertEqual(experiment.control.value, self.data['control-value']) + experiment.control.description, self.data["control-description"] + ) + self.assertEqual(experiment.control.ratio, self.data["control-ratio"]) + self.assertEqual(experiment.control.value, self.data["control-value"]) self.assertEqual( - experiment.variant.name, self.data['experimental-name']) + experiment.variant.name, self.data["experimental-name"] + ) self.assertEqual( experiment.variant.description, - self.data['experimental-description'], + self.data["experimental-description"], ) self.assertEqual( - experiment.variant.ratio, self.data['experimental-ratio']) + experiment.variant.ratio, self.data["experimental-ratio"] + ) self.assertEqual( - experiment.variant.value, self.data['experimental-value']) + experiment.variant.value, self.data["experimental-value"] + ) def test_form_is_invalid_if_control_is_invalid(self): created_experiment = ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT) - self.data['control-ratio'] = 'invalid' + Experiment.STATUS_DRAFT + ) + self.data["control-ratio"] = "invalid" form = ExperimentVariantsForm( - request=self.request, data=self.data, instance=created_experiment) + request=self.request, data=self.data, instance=created_experiment + ) self.assertFalse(form.is_valid()) def test_form_is_invalid_if_experimental_is_invalid(self): created_experiment = ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT) - self.data['experimental-ratio'] = 'invalid' + Experiment.STATUS_DRAFT + ) + self.data["experimental-ratio"] = "invalid" form = ExperimentVariantsForm( - request=self.request, data=self.data, instance=created_experiment) + request=self.request, data=self.data, instance=created_experiment + ) self.assertFalse(form.is_valid()) @@ -331,70 +347,77 @@ class TestExperimentObjectivesForm(MockRequestMixin, TestCase): def test_form_saves_objectives(self): created_experiment = ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT) + Experiment.STATUS_DRAFT + ) data = { - 'objectives': 'The objective is to experiment!', - 'analysis': 'Lets analyze the results!', + "objectives": "The objective is to experiment!", + "analysis": "Lets analyze the results!", } form = ExperimentObjectivesForm( - request=self.request, data=data, instance=created_experiment) + request=self.request, data=data, instance=created_experiment + ) self.assertTrue(form.is_valid()) experiment = form.save() - self.assertEqual(experiment.objectives, data['objectives']) - self.assertEqual(experiment.analysis, data['analysis']) + self.assertEqual(experiment.objectives, data["objectives"]) + self.assertEqual(experiment.analysis, data["analysis"]) class TestExperimentRisksForm(MockRequestMixin, TestCase): def test_form_saves_risks(self): created_experiment = ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT) + Experiment.STATUS_DRAFT + ) data = { - 'risk_partner_related': False, - 'risk_brand': True, - 'risk_fast_shipped': False, - 'risk_confidential': True, - 'risk_release_population': False, - 'risks': 'There are some risks', - 'testing': 'Always be sure to test!', + "risk_partner_related": False, + "risk_brand": True, + "risk_fast_shipped": False, + "risk_confidential": True, + "risk_release_population": False, + "risks": "There are some risks", + "testing": "Always be sure to test!", } form = ExperimentRisksForm( - request=self.request, data=data, instance=created_experiment) + request=self.request, data=data, instance=created_experiment + ) self.assertTrue(form.is_valid()) experiment = form.save() self.assertEqual( - experiment.risk_partner_related, data['risk_partner_related']) - self.assertEqual(experiment.risk_brand, data['risk_brand']) + experiment.risk_partner_related, data["risk_partner_related"] + ) + self.assertEqual(experiment.risk_brand, data["risk_brand"]) self.assertEqual( - experiment.risk_fast_shipped, data['risk_fast_shipped']) + experiment.risk_fast_shipped, data["risk_fast_shipped"] + ) self.assertEqual( - experiment.risk_confidential, data['risk_confidential']) + experiment.risk_confidential, data["risk_confidential"] + ) self.assertEqual( - experiment.risk_release_population, - data['risk_release_population'], + experiment.risk_release_population, data["risk_release_population"] ) - self.assertEqual(experiment.risks, data['risks']) - self.assertEqual(experiment.testing, data['testing']) + self.assertEqual(experiment.risks, data["risks"]) + self.assertEqual(experiment.testing, data["testing"]) class TestExperimentStatusForm(MockRequestMixin, TestCase): def test_form_allows_valid_state_transition_and_creates_changelog(self): experiment = ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT) + Experiment.STATUS_DRAFT + ) form = ExperimentStatusForm( request=self.request, - data={'status': experiment.STATUS_REVIEW}, + data={"status": experiment.STATUS_REVIEW}, instance=experiment, ) self.assertTrue(form.is_valid()) @@ -406,10 +429,11 @@ def test_form_allows_valid_state_transition_and_creates_changelog(self): def test_form_rejects_invalid_state_transitions(self): experiment = ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT) + Experiment.STATUS_DRAFT + ) form = ExperimentStatusForm( request=self.request, - data={'status': experiment.STATUS_LIVE}, + data={"status": experiment.STATUS_LIVE}, instance=experiment, ) self.assertFalse(form.is_valid()) diff --git a/app/experimenter/experiments/tests/test_generate_dashboards.py b/app/experimenter/experiments/tests/test_generate_dashboards.py index 77c9956266..1b1aa92b81 100644 --- a/app/experimenter/experiments/tests/test_generate_dashboards.py +++ b/app/experimenter/experiments/tests/test_generate_dashboards.py @@ -9,85 +9,107 @@ from experimenter.experiments.models import Experiment from experimenter.experiments.tests.factories import ExperimentFactory from experimenter.experiments.management.commands.generate_dashboards import ( - DASHBOARD_TAG_NAME, sanitize_name) + DASHBOARD_TAG_NAME, + sanitize_name, +) class GenerateDashboardsTest(TestCase): def setUp(self): self.ORIGINAL_EXTERNAL_API_EXCEPTION = ( - ExperimentDashboard.ExternalAPIError) - - dashboard_patcher = mock.patch(( - 'experimenter.experiments.management.commands.' - 'generate_dashboards.ExperimentDashboard')) - logging_patcher = mock.patch(( - 'experimenter.experiments.management.commands.' - 'generate_dashboards.logging')) + ExperimentDashboard.ExternalAPIError + ) + + dashboard_patcher = mock.patch( + ( + "experimenter.experiments.management.commands." + "generate_dashboards.ExperimentDashboard" + ) + ) + logging_patcher = mock.patch( + ( + "experimenter.experiments.management.commands." + "generate_dashboards.logging" + ) + ) self.mock_logger = logging_patcher.start() - self.MockExperimentDashboard = dashboard_patcher.start() - self.MockExperimentDashboard.ExternalAPIError = ( - self.ORIGINAL_EXTERNAL_API_EXCEPTION) + self.MockDashboard = dashboard_patcher.start() + self.MockDashboard.ExternalAPIError = ( + self.ORIGINAL_EXTERNAL_API_EXCEPTION + ) self.addCleanup(logging_patcher.stop) self.addCleanup(dashboard_patcher.stop) # A launched experiment self.experiment_launched = ExperimentFactory.create_with_status( - Experiment.STATUS_LIVE) + Experiment.STATUS_LIVE + ) # A recently complete experiment self.experiment_complete = ExperimentFactory.create_with_status( - Experiment.STATUS_COMPLETE) + Experiment.STATUS_COMPLETE + ) relevant_change = self.experiment_complete.changes.filter( old_status=Experiment.STATUS_LIVE, - new_status=Experiment.STATUS_COMPLETE).get() - relevant_change.changed_on = ( - datetime.now(timezone.utc) - timedelta(days=1)) + new_status=Experiment.STATUS_COMPLETE, + ).get() + relevant_change.changed_on = datetime.now(timezone.utc) - timedelta( + days=1 + ) relevant_change.save() # An experiment with a missing dashboard self.experiment_missing_dash = ExperimentFactory.create_with_status( - Experiment.STATUS_COMPLETE) + Experiment.STATUS_COMPLETE + ) self.experiment_missing_dash.dashboard_url = None self.experiment_missing_dash.save() - self.experiments = [self.experiment_launched, - self.experiment_complete, - self.experiment_missing_dash] + self.experiments = [ + self.experiment_launched, + self.experiment_complete, + self.experiment_missing_dash, + ] def test_dashboard_object_generated(self): - expected_call_args = [( - settings.REDASH_API_KEY, - DASHBOARD_TAG_NAME, - sanitize_name(experiment.name), - experiment.slug, - experiment.start_date.strftime("%Y-%m-%d"), - (None - if not experiment.end_date - else experiment.end_date.strftime("%Y-%m-%d"))) - for experiment in self.experiments] - - DEFAULT_PUBLIC_URL = 'http://www.example.com/dashboard' - NEW_PUBLIC_URL = 'www.example.com/some_dashboard_url' - - mock_instance = self.MockExperimentDashboard.return_value + expected_call_args = [ + ( + settings.REDASH_API_KEY, + DASHBOARD_TAG_NAME, + sanitize_name(experiment.name), + experiment.slug, + experiment.start_date.strftime("%Y-%m-%d"), + ( + None + if not experiment.end_date + else experiment.end_date.strftime("%Y-%m-%d") + ), + ) + for experiment in self.experiments + ] + + DEFAULT_PUBLIC_URL = "http://www.example.com/dashboard" + NEW_PUBLIC_URL = "www.example.com/some_dashboard_url" + + mock_instance = self.MockDashboard.return_value mock_instance.public_url = NEW_PUBLIC_URL mock_instance.UT_HOURLY_EVENTS = [1, 2, 3, 4] mock_instance.MAPPED_UT_EVENTS = [1, 2, 3] mock_instance.get_update_range.return_value = { "min": datetime.now() - timedelta(days=2) } - mock_instance.get_query_ids_and_names.return_value = ( - [i for i in range(3)]) + mock_instance.get_query_ids_and_names.return_value = [ + i for i in range(3) + ] with self.settings(DASHBOARD_RATE_LIMIT=len(self.experiments)): - call_command('generate_dashboards') + call_command("generate_dashboards") - self.assertTrue(self.MockExperimentDashboard.called) - self.assertEqual(self.MockExperimentDashboard.call_count, 3) + self.assertTrue(self.MockDashboard.called) + self.assertEqual(self.MockDashboard.call_count, 3) - for idx, call_args in enumerate( - self.MockExperimentDashboard.call_args_list): + for idx, call_args in enumerate(self.MockDashboard.call_args_list): args, kwargs = call_args self.assertEqual(args, expected_call_args[idx]) @@ -95,12 +117,13 @@ def test_dashboard_object_generated(self): for experiment in self.experiments: experiment_obj = Experiment.objects.get(pk=experiment.pk) self.assertTrue( - experiment_obj.dashboard_url in [None, DEFAULT_PUBLIC_URL]) + experiment_obj.dashboard_url in [None, DEFAULT_PUBLIC_URL] + ) - self.assertEqual(len( - mock_instance.add_graph_templates.mock_calls), 15) - self.assertEqual(len( - mock_instance.get_update_range.mock_calls), 3) + self.assertEqual( + len(mock_instance.add_graph_templates.mock_calls), 15 + ) + self.assertEqual(len(mock_instance.get_update_range.mock_calls), 3) self.call_count = 0 @@ -114,73 +137,77 @@ def get_widgets(): mock_instance.get_query_ids_and_names.side_effect = get_widgets - call_command('generate_dashboards') + call_command("generate_dashboards") # The dashboards are now complete, so dashboard_url is set - self.assertEqual(len( - mock_instance.add_graph_templates.mock_calls), 30) + self.assertEqual( + len(mock_instance.add_graph_templates.mock_calls), 30 + ) for experiment in self.experiments: experiment_obj = Experiment.objects.get(pk=experiment.pk) self.assertEqual(experiment_obj.dashboard_url, NEW_PUBLIC_URL) def test_recently_updated_dashboard_is_ignored(self): - mock_instance = self.MockExperimentDashboard.return_value - mock_instance.public_url = 'www.example.com/dashboard' + mock_instance = self.MockDashboard.return_value + mock_instance.public_url = "www.example.com/dashboard" mock_instance.get_update_range.return_value = { "min": datetime.now(timezone.utc) } - mock_instance.get_query_ids_and_names.return_value = ( - [i for i in range(int(15 / 2))]) + mock_instance.get_query_ids_and_names.return_value = [ + i for i in range(int(15 / 2)) + ] with self.settings(DASHBOARD_RATE_LIMIT=len(self.experiments)): - call_command('generate_dashboards') + call_command("generate_dashboards") - self.assertTrue(self.MockExperimentDashboard.called) - self.assertEqual(self.MockExperimentDashboard.call_count, 3) + self.assertTrue(self.MockDashboard.called) + self.assertEqual(self.MockDashboard.call_count, 3) - self.assertEqual(len( - mock_instance.add_graph_templates.mock_calls), 0) + self.assertEqual( + len(mock_instance.add_graph_templates.mock_calls), 0 + ) def test_dashboards_are_rate_limited(self): - mock_instance = self.MockExperimentDashboard.return_value - mock_instance.public_url = 'www.example.com/dashboard' + mock_instance = self.MockDashboard.return_value + mock_instance.public_url = "www.example.com/dashboard" mock_instance.get_update_range.return_value = { "min": datetime.now(timezone.utc) } - mock_instance.get_query_ids_and_names.return_value = ( - [i for i in range(int(15 / 2))]) + mock_instance.get_query_ids_and_names.return_value = [ + i for i in range(int(15 / 2)) + ] rate_limit = len(self.experiments) - 1 with self.settings(DASHBOARD_RATE_LIMIT=rate_limit): - call_command('generate_dashboards') + call_command("generate_dashboards") - self.assertTrue(self.MockExperimentDashboard.called) - self.assertEqual( - self.MockExperimentDashboard.call_count, rate_limit) + self.assertTrue(self.MockDashboard.called) + self.assertEqual(self.MockDashboard.call_count, rate_limit) def test_external_api_error_is_caught(self): - ERROR_MESSAGE = 'Unable to communicate with Redash' + ERROR_MESSAGE = "Unable to communicate with Redash" - self.MockExperimentDashboard.side_effect = ( - ExperimentDashboard.ExternalAPIError(( - ERROR_MESSAGE))) + self.MockDashboard.side_effect = ExperimentDashboard.ExternalAPIError( + (ERROR_MESSAGE) + ) - call_command('generate_dashboards') + call_command("generate_dashboards") - self.mock_logger.error.assert_any_call(( - 'ExternalAPIError ' - 'for {exp}: {err}').format( - exp=self.experiment_complete, err=ERROR_MESSAGE)) + self.mock_logger.error.assert_any_call( + ("ExternalAPIError " "for {exp}: {err}").format( + exp=self.experiment_complete, err=ERROR_MESSAGE + ) + ) def test_value_error_is_caught(self): - ERROR_MESSAGE = 'column_mapping value required' + ERROR_MESSAGE = "column_mapping value required" - self.MockExperimentDashboard.side_effect = ( - ValueError(ERROR_MESSAGE)) + self.MockDashboard.side_effect = ValueError(ERROR_MESSAGE) - call_command('generate_dashboards') + call_command("generate_dashboards") - self.mock_logger.error.assert_any_call(( - 'ExperimentDashboard Value Error ' - 'for {exp}: {err}').format( - exp=self.experiment_complete, err=ERROR_MESSAGE)) + self.mock_logger.error.assert_any_call( + ("ExperimentDashboard Value Error " "for {exp}: {err}").format( + exp=self.experiment_complete, err=ERROR_MESSAGE + ) + ) diff --git a/app/experimenter/experiments/tests/test_models.py b/app/experimenter/experiments/tests/test_models.py index 24780166d6..b3125591d0 100644 --- a/app/experimenter/experiments/tests/test_models.py +++ b/app/experimenter/experiments/tests/test_models.py @@ -3,9 +3,14 @@ from django.test import TestCase from experimenter.experiments.models import ( - Experiment, ExperimentVariant, ExperimentChangeLog) + Experiment, + ExperimentVariant, + ExperimentChangeLog, +) from experimenter.experiments.tests.factories import ( - ExperimentFactory, ExperimentChangeLogFactory) + ExperimentFactory, + ExperimentChangeLogFactory, +) class TestExperimentManager(TestCase): @@ -30,7 +35,7 @@ def test_queryset_annotated_with_latest_change(self): ) self.assertEqual( - list(Experiment.objects.order_by('-latest_change')), + list(Experiment.objects.order_by("-latest_change")), [experiment2, experiment1], ) @@ -41,7 +46,7 @@ def test_queryset_annotated_with_latest_change(self): ) self.assertEqual( - list(Experiment.objects.order_by('-latest_change')), + list(Experiment.objects.order_by("-latest_change")), [experiment1, experiment2], ) @@ -73,13 +78,15 @@ def test_end_date_returns_datetime_if_change_exists(self): def test_control_property_returns_experiment_control(self): experiment = ExperimentFactory.create_with_variants() control = ExperimentVariant.objects.get( - experiment=experiment, is_control=True) + experiment=experiment, is_control=True + ) self.assertEqual(experiment.control, control) def test_variant_property_returns_experiment_variant(self): experiment = ExperimentFactory.create_with_variants() variant = ExperimentVariant.objects.get( - experiment=experiment, is_control=False) + experiment=experiment, is_control=False + ) self.assertEqual(experiment.variant, variant) def test_experiment_with_created_status_is_not_readonly(self): @@ -133,8 +140,7 @@ def test_objectives_is_not_complete_with_still_default(self): def test_objectives_is_complete_with_non_defaults(self): experiment = ExperimentFactory.create( - objectives='Some objectives!', - analysis='Some analysis!', + objectives="Some objectives!", analysis="Some analysis!" ) self.assertTrue(experiment.completed_objectives) @@ -147,8 +153,7 @@ def test_risk_questions_returns_a_tuple(self): risk_release_population=False, ) self.assertEqual( - experiment._risk_questions, - (False, True, False, True, False), + experiment._risk_questions, (False, True, False, True, False) ) def test_risk_not_completed_when_risk_questions_not_answered(self): @@ -158,7 +163,7 @@ def test_risk_not_completed_when_risk_questions_not_answered(self): risk_fast_shipped=None, risk_confidential=None, risk_release_population=None, - testing='A test plan!', + testing="A test plan!", ) self.assertFalse(experiment.completed_risks) @@ -180,7 +185,7 @@ def test_risk_completed_when_risk_questions_and_testing_completed(self): risk_fast_shipped=False, risk_confidential=True, risk_release_population=False, - testing='A test plan!', + testing="A test plan!", ) self.assertTrue(experiment.completed_risks) @@ -204,11 +209,11 @@ def test_is_not_high_risk_if_no_risk_questions_are_true(self): def test_is_high_risk_if_any_risk_questions_are_true(self): risk_fields = ( - 'risk_partner_related', - 'risk_brand', - 'risk_fast_shipped', - 'risk_confidential', - 'risk_release_population', + "risk_partner_related", + "risk_brand", + "risk_fast_shipped", + "risk_confidential", + "risk_release_population", ) for true_risk_field in risk_fields: @@ -220,34 +225,31 @@ def test_is_high_risk_if_any_risk_questions_are_true(self): def test_experiment_population_returns_correct_string(self): experiment = ExperimentFactory( - population_percent='0.5', - firefox_version='57.0', - firefox_channel='Nightly', - ) - self.assertEqual( - experiment.population, - '0.5% of Nightly Firefox 57.0' + population_percent="0.5", + firefox_version="57.0", + firefox_channel="Nightly", ) + self.assertEqual(experiment.population, "0.5% of Nightly Firefox 57.0") def test_test_tube_link_is_correct(self): - experiment = ExperimentFactory.create(slug='experiment') + experiment = ExperimentFactory.create(slug="experiment") self.assertEqual( experiment.test_tube_url, - 'https://firefox-test-tube.herokuapp.com/experiments/experiment/', + "https://firefox-test-tube.herokuapp.com/experiments/experiment/", ) def test_accept_url_is_correct(self): - experiment = ExperimentFactory.create(slug='experiment') + experiment = ExperimentFactory.create(slug="experiment") self.assertEqual( experiment.accept_url, - 'https://localhost/api/v1/experiments/experiment/accept/', + "https://localhost/api/v1/experiments/experiment/accept/", ) def test_reject_url_is_correct(self): - experiment = ExperimentFactory.create(slug='experiment') + experiment = ExperimentFactory.create(slug="experiment") self.assertEqual( experiment.reject_url, - 'https://localhost/api/v1/experiments/experiment/reject/', + "https://localhost/api/v1/experiments/experiment/reject/", ) @@ -280,14 +282,15 @@ def test_pretty_status_created_draft(self): for old_status in ExperimentChangeLog.PRETTY_STATUS_LABELS.keys(): for new_status in ExperimentChangeLog.PRETTY_STATUS_LABELS[ - old_status].keys(): + old_status + ].keys(): expected_label = ExperimentChangeLog.PRETTY_STATUS_LABELS[ - old_status][new_status] + old_status + ][new_status] changelog = ExperimentChangeLogFactory.create( experiment=experiment, old_status=old_status, new_status=new_status, ) - self.assertEqual( - changelog.pretty_status, expected_label) + self.assertEqual(changelog.pretty_status, expected_label) diff --git a/app/experimenter/experiments/tests/test_serializers.py b/app/experimenter/experiments/tests/test_serializers.py index fc18f14ee2..8295b285f3 100644 --- a/app/experimenter/experiments/tests/test_serializers.py +++ b/app/experimenter/experiments/tests/test_serializers.py @@ -20,7 +20,8 @@ def test_field_serializes_to_js_time_format(self): field = JSTimestampField() example_datetime = datetime.datetime(2000, 1, 1, 1, 1, 1, 1) self.assertEqual( - field.to_representation(example_datetime), 946688461000.0) + field.to_representation(example_datetime), 946688461000.0 + ) def test_field_returns_none_if_no_datetime_passed_in(self): field = JSTimestampField() @@ -32,53 +33,63 @@ class TestExperimentVariantSerializer(TestCase): def test_serializer_outputs_expected_schema(self): variant = ExperimentVariantFactory.create() serialized = ExperimentVariantSerializer(variant) - self.assertEqual(serialized.data, { - 'description': variant.description, - 'name': variant.name, - 'ratio': variant.ratio, - 'slug': variant.slug, - 'value': variant.value, - }) + self.assertEqual( + serialized.data, + { + "description": variant.description, + "name": variant.name, + "ratio": variant.ratio, + "slug": variant.slug, + "value": variant.value, + }, + ) class TestExperimentSerializer(TestCase): def test_serializer_outputs_expected_schema(self): experiment = ExperimentFactory.create_with_status( - Experiment.STATUS_COMPLETE) + Experiment.STATUS_COMPLETE + ) serialized = ExperimentSerializer(experiment) expected_data = { - 'accept_url': experiment.accept_url, - 'client_matching': experiment.client_matching, - 'control': ExperimentVariantSerializer(experiment.control).data, - 'end_date': JSTimestampField().to_representation( - experiment.end_date), - 'experiment_slug': experiment.experiment_slug, - 'experiment_url': experiment.experiment_url, - 'firefox_channel': experiment.firefox_channel, - 'firefox_version': experiment.firefox_version, - 'name': experiment.name, - 'objectives': experiment.objectives, - 'population': experiment.population, - 'population_percent': '{0:.4f}'.format( - experiment.population_percent), - 'pref_branch': experiment.pref_branch, - 'pref_key': experiment.pref_key, - 'pref_type': experiment.pref_type, - 'project_name': experiment.project.name, - 'project_slug': experiment.project.slug, - 'proposed_end_date': JSTimestampField().to_representation( - experiment.proposed_end_date), - 'proposed_start_date': JSTimestampField().to_representation( - experiment.proposed_start_date), - 'reject_url': experiment.reject_url, - 'short_description': experiment.short_description, - 'slug': experiment.slug, - 'start_date': JSTimestampField().to_representation( - experiment.start_date), - 'variant': ExperimentVariantSerializer(experiment.variant).data, + "accept_url": experiment.accept_url, + "client_matching": experiment.client_matching, + "control": ExperimentVariantSerializer(experiment.control).data, + "end_date": JSTimestampField().to_representation( + experiment.end_date + ), + "experiment_slug": experiment.experiment_slug, + "experiment_url": experiment.experiment_url, + "firefox_channel": experiment.firefox_channel, + "firefox_version": experiment.firefox_version, + "name": experiment.name, + "objectives": experiment.objectives, + "population": experiment.population, + "population_percent": "{0:.4f}".format( + experiment.population_percent + ), + "pref_branch": experiment.pref_branch, + "pref_key": experiment.pref_key, + "pref_type": experiment.pref_type, + "project_name": experiment.project.name, + "project_slug": experiment.project.slug, + "proposed_end_date": JSTimestampField().to_representation( + experiment.proposed_end_date + ), + "proposed_start_date": JSTimestampField().to_representation( + experiment.proposed_start_date + ), + "reject_url": experiment.reject_url, + "short_description": experiment.short_description, + "slug": experiment.slug, + "start_date": JSTimestampField().to_representation( + experiment.start_date + ), + "variant": ExperimentVariantSerializer(experiment.variant).data, } self.assertEqual( - set(serialized.data.keys()), set(expected_data.keys())) + set(serialized.data.keys()), set(expected_data.keys()) + ) self.assertEqual(serialized.data, expected_data) diff --git a/app/experimenter/experiments/tests/test_views.py b/app/experimenter/experiments/tests/test_views.py index 7c9854e6f1..f61ace533d 100644 --- a/app/experimenter/experiments/tests/test_views.py +++ b/app/experimenter/experiments/tests/test_views.py @@ -25,57 +25,54 @@ class TestExperimentFilterset(TestCase): def test_filters_out_archived_by_default(self): for i in range(3): ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT, archived=False) + Experiment.STATUS_DRAFT, archived=False + ) for i in range(3): ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT, archived=True) + Experiment.STATUS_DRAFT, archived=True + ) filter = ExperimentFilterset( - data={}, - queryset=Experiment.objects.all(), + data={}, queryset=Experiment.objects.all() ) self.assertEqual( - set(filter.qs), - set(Experiment.objects.filter(archived=False)), + set(filter.qs), set(Experiment.objects.filter(archived=False)) ) def test_allows_archived_if_True(self): for i in range(3): ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT, archived=False) + Experiment.STATUS_DRAFT, archived=False + ) for i in range(3): ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT, archived=True) + Experiment.STATUS_DRAFT, archived=True + ) filter = ExperimentFilterset( - data={'archived': True}, - queryset=Experiment.objects.all(), + data={"archived": True}, queryset=Experiment.objects.all() ) - self.assertEqual( - set(filter.qs), - set(Experiment.objects.all()), - ) + self.assertEqual(set(filter.qs), set(Experiment.objects.all())) def test_filters_by_project(self): project = ProjectFactory.create() for i in range(3): ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT, project=project) + Experiment.STATUS_DRAFT, project=project + ) ExperimentFactory.create_with_status(Experiment.STATUS_DRAFT) filter = ExperimentFilterset( - {'project': project.id}, - queryset=Experiment.objects.all(), + {"project": project.id}, queryset=Experiment.objects.all() ) self.assertEqual( - set(filter.qs), - set(Experiment.objects.filter(project=project)), + set(filter.qs), set(Experiment.objects.filter(project=project)) ) def test_filters_by_owner(self): @@ -83,17 +80,16 @@ def test_filters_by_owner(self): for i in range(3): ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT, owner=owner) + Experiment.STATUS_DRAFT, owner=owner + ) ExperimentFactory.create_with_status(Experiment.STATUS_DRAFT) filter = ExperimentFilterset( - {'owner': owner.id}, - queryset=Experiment.objects.all(), + {"owner": owner.id}, queryset=Experiment.objects.all() ) self.assertEqual( - set(filter.qs), - set(Experiment.objects.filter(owner=owner)), + set(filter.qs), set(Experiment.objects.filter(owner=owner)) ) def test_filters_by_status(self): @@ -102,7 +98,7 @@ def test_filters_by_status(self): ExperimentFactory.create_with_status(Experiment.STATUS_REVIEW) filter = ExperimentFilterset( - {'status': Experiment.STATUS_DRAFT}, + {"status": Experiment.STATUS_DRAFT}, queryset=Experiment.objects.all(), ) @@ -117,12 +113,14 @@ def test_filters_by_firefox_version(self): for i in range(3): ExperimentFactory.create_with_variants( - firefox_version=include_version) + firefox_version=include_version + ) ExperimentFactory.create_with_variants( - firefox_version=exclude_version) + firefox_version=exclude_version + ) filter = ExperimentFilterset( - {'firefox_version': include_version}, + {"firefox_version": include_version}, queryset=Experiment.objects.all(), ) self.assertEqual( @@ -136,12 +134,14 @@ def test_filters_by_firefox_channel(self): for i in range(3): ExperimentFactory.create_with_variants( - firefox_channel=include_channel) + firefox_channel=include_channel + ) ExperimentFactory.create_with_variants( - firefox_channel=exclude_channel) + firefox_channel=exclude_channel + ) filter = ExperimentFilterset( - {'firefox_channel': include_channel}, + {"firefox_channel": include_channel}, queryset=Experiment.objects.all(), ) self.assertEqual( @@ -154,70 +154,73 @@ class TestExperimengOrderingForm(TestCase): def test_accepts_valid_ordering(self): ordering = ExperimentOrderingForm.ORDERING_CHOICES[1][0] - form = ExperimentOrderingForm({'ordering': ordering}) + form = ExperimentOrderingForm({"ordering": ordering}) self.assertTrue(form.is_valid()) def test_rejects_invalid_ordering(self): - form = ExperimentOrderingForm({'ordering': 'invalid ordering'}) + form = ExperimentOrderingForm({"ordering": "invalid ordering"}) self.assertFalse(form.is_valid()) class TestExperimentListView(TestCase): def test_list_view_lists_experiments_with_default_order_no_archived(self): - user_email = 'user@example.com' + user_email = "user@example.com" # Archived experiment is ommitted ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT, archived=True) + Experiment.STATUS_DRAFT, archived=True + ) for i in range(3): ExperimentFactory.create_with_status( - random.choice(Experiment.STATUS_CHOICES)[0]) + random.choice(Experiment.STATUS_CHOICES)[0] + ) - experiments = Experiment.objects.all().filter(archived=False).order_by( - ExperimentOrderingForm.ORDERING_CHOICES[0][0]) + experiments = ( + Experiment.objects.all() + .filter(archived=False) + .order_by(ExperimentOrderingForm.ORDERING_CHOICES[0][0]) + ) response = self.client.get( - reverse('home'), - **{settings.OPENIDC_EMAIL_HEADER: user_email}, + reverse("home"), **{settings.OPENIDC_EMAIL_HEADER: user_email} ) context = response.context[0] self.assertEqual(response.status_code, 200) - self.assertEqual(list(context['experiments']), list(experiments)) + self.assertEqual(list(context["experiments"]), list(experiments)) def test_list_view_shows_all_including_archived(self): - user_email = 'user@example.com' + user_email = "user@example.com" # Archived experiment is included ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT, archived=True) + Experiment.STATUS_DRAFT, archived=True + ) for i in range(3): ExperimentFactory.create_with_status( - random.choice(Experiment.STATUS_CHOICES)[0]) + random.choice(Experiment.STATUS_CHOICES)[0] + ) experiments = Experiment.objects.all() response = self.client.get( - '{url}?{params}'.format( - url=reverse('home'), - params=urlencode({ - 'archived': True, - }), + "{url}?{params}".format( + url=reverse("home"), params=urlencode({"archived": True}) ), **{settings.OPENIDC_EMAIL_HEADER: user_email}, ) context = response.context[0] self.assertEqual(response.status_code, 200) - self.assertEqual(set(context['experiments']), set(experiments)) + self.assertEqual(set(context["experiments"]), set(experiments)) def test_list_view_filters_and_orders_experiments(self): - user_email = 'user@example.com' + user_email = "user@example.com" - ordering = 'latest_change' + ordering = "latest_change" filtered_channel = Experiment.CHANNEL_CHOICES[1][0] filtered_owner = UserFactory.create() filtered_project = ProjectFactory.create() @@ -235,7 +238,8 @@ def test_list_view_filters_and_orders_experiments(self): for i in range(10): ExperimentFactory.create_with_status( - random.choice(Experiment.STATUS_CHOICES)[0]) + random.choice(Experiment.STATUS_CHOICES)[0] + ) filtered_ordered_experiments = Experiment.objects.filter( firefox_channel=filtered_channel, @@ -246,16 +250,18 @@ def test_list_view_filters_and_orders_experiments(self): ).order_by(ordering) response = self.client.get( - '{url}?{params}'.format( - url=reverse('home'), - params=urlencode({ - 'firefox_channel': filtered_channel, - 'firefox_version': filtered_version, - 'ordering': ordering, - 'owner': filtered_owner.id, - 'project': filtered_project.id, - 'status': filtered_status, - }), + "{url}?{params}".format( + url=reverse("home"), + params=urlencode( + { + "firefox_channel": filtered_channel, + "firefox_version": filtered_version, + "ordering": ordering, + "owner": filtered_owner.id, + "project": filtered_project.id, + "status": filtered_status, + } + ), ), **{settings.OPENIDC_EMAIL_HEADER: user_email}, ) @@ -263,14 +269,14 @@ def test_list_view_filters_and_orders_experiments(self): context = response.context[0] self.assertEqual(response.status_code, 200) self.assertEqual( - list(context['experiments']), - list(filtered_ordered_experiments), + list(context["experiments"]), list(filtered_ordered_experiments) ) class TestExperimentFormMixin(TestCase): def test_get_form_kwargs_adds_request(self): + class BaseTestView(object): def __init__(self, request): @@ -285,11 +291,13 @@ class TestView(ExperimentFormMixin, BaseTestView): request = mock.Mock() view = TestView(request=request) form_kwargs = view.get_form_kwargs() - self.assertEqual(form_kwargs['request'], request) + self.assertEqual(form_kwargs["request"], request) - @mock.patch('experimenter.experiments.views.reverse') + @mock.patch("experimenter.experiments.views.reverse") def test_get_success_url_returns_next_url_if_action_is_continue( - self, mock_reverse): + self, mock_reverse + ): + class BaseTestView(object): def __init__(self, request, instance): @@ -297,7 +305,7 @@ def __init__(self, request, instance): self.object = instance class TestView(ExperimentFormMixin, BaseTestView): - next_view_name = 'next-test-view' + next_view_name = "next-test-view" def mock_reverser(url_name, *args, **kwargs): return url_name @@ -305,20 +313,23 @@ def mock_reverser(url_name, *args, **kwargs): mock_reverse.side_effect = mock_reverser instance = mock.Mock() - instance.slug = 'slug' + instance.slug = "slug" request = mock.Mock() - request.POST = {'action': 'continue'} + request.POST = {"action": "continue"} view = TestView(request, instance) redirect = view.get_success_url() self.assertEqual(redirect, TestView.next_view_name) mock_reverse.assert_called_with( - TestView.next_view_name, kwargs={'slug': instance.slug}) + TestView.next_view_name, kwargs={"slug": instance.slug} + ) - @mock.patch('experimenter.experiments.views.reverse') + @mock.patch("experimenter.experiments.views.reverse") def test_get_success_url_returns_detail_url_if_action_is_empty( - self, mock_reverse): + self, mock_reverse + ): + class BaseTestView(object): def __init__(self, request, instance): @@ -326,7 +337,7 @@ def __init__(self, request, instance): self.object = instance class TestView(ExperimentFormMixin, BaseTestView): - next_view_name = 'next-test-view' + next_view_name = "next-test-view" def mock_reverser(url_name, *args, **kwargs): return url_name @@ -334,39 +345,41 @@ def mock_reverser(url_name, *args, **kwargs): mock_reverse.side_effect = mock_reverser instance = mock.Mock() - instance.slug = 'slug' + instance.slug = "slug" request = mock.Mock() request.POST = {} view = TestView(request, instance) redirect = view.get_success_url() - self.assertEqual(redirect, 'experiments-detail') + self.assertEqual(redirect, "experiments-detail") mock_reverse.assert_called_with( - 'experiments-detail', kwargs={'slug': instance.slug}) + "experiments-detail", kwargs={"slug": instance.slug} + ) class TestExperimentCreateView(TestCase): def test_view_creates_experiment(self): - user_email = 'user@example.com' + user_email = "user@example.com" project = ProjectFactory.create() data = { - 'project': project.id, - 'name': 'A new experiment!', - 'short_description': 'Let us learn new things', - 'population_percent': '10', - 'firefox_version': Experiment.VERSION_CHOICES[-1][0], - 'firefox_channel': Experiment.CHANNEL_NIGHTLY, - 'client_matching': 'en-us only please', - 'proposed_start_date': datetime.date.today(), - 'proposed_end_date': ( - datetime.date.today() + datetime.timedelta(days=1)), + "project": project.id, + "name": "A new experiment!", + "short_description": "Let us learn new things", + "population_percent": "10", + "firefox_version": Experiment.VERSION_CHOICES[-1][0], + "firefox_channel": Experiment.CHANNEL_NIGHTLY, + "client_matching": "en-us only please", + "proposed_start_date": datetime.date.today(), + "proposed_end_date": ( + datetime.date.today() + datetime.timedelta(days=1) + ), } response = self.client.post( - reverse('experiments-create'), + reverse("experiments-create"), data, **{settings.OPENIDC_EMAIL_HEADER: user_email}, ) @@ -375,7 +388,7 @@ def test_view_creates_experiment(self): experiment = Experiment.objects.get() self.assertEqual(experiment.status, experiment.STATUS_DRAFT) self.assertEqual(experiment.project, project) - self.assertEqual(experiment.name, data['name']) + self.assertEqual(experiment.name, data["name"]) self.assertEqual(experiment.changes.count(), 1) @@ -389,45 +402,43 @@ def test_view_finds_project_id_in_get_args(self): project = ProjectFactory.create() request = mock.Mock() - request.GET = {'project': project.id} + request.GET = {"project": project.id} view = ExperimentCreateView(request=request) initial = view.get_initial() - self.assertEqual(initial['project'], project.id) + self.assertEqual(initial["project"], project.id) class TestExperimentOverviewUpdateView(TestCase): def test_view_saves_experiment(self): - user_email = 'user@example.com' + user_email = "user@example.com" experiment = ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT) + Experiment.STATUS_DRAFT + ) - new_start_date = ( - datetime.date.today() + - datetime.timedelta(days=random.randint(1, 100)) + new_start_date = datetime.date.today() + datetime.timedelta( + days=random.randint(1, 100) ) - new_end_date = ( - new_start_date + - datetime.timedelta(days=random.randint(1, 100)) + new_end_date = new_start_date + datetime.timedelta( + days=random.randint(1, 100) ) data = { - 'name': 'A new name!', - 'short_description': 'A new description!', - 'population_percent': '11', - 'firefox_version': Experiment.VERSION_CHOICES[-1][0], - 'firefox_channel': Experiment.CHANNEL_NIGHTLY, - 'client_matching': 'New matching!', - 'proposed_start_date': new_start_date, - 'proposed_end_date': new_end_date, + "name": "A new name!", + "short_description": "A new description!", + "population_percent": "11", + "firefox_version": Experiment.VERSION_CHOICES[-1][0], + "firefox_channel": Experiment.CHANNEL_NIGHTLY, + "client_matching": "New matching!", + "proposed_start_date": new_start_date, + "proposed_end_date": new_end_date, } response = self.client.post( reverse( - 'experiments-overview-update', - kwargs={'slug': experiment.slug}, + "experiments-overview-update", kwargs={"slug": experiment.slug} ), data, **{settings.OPENIDC_EMAIL_HEADER: user_email}, @@ -435,15 +446,16 @@ def test_view_saves_experiment(self): self.assertEqual(response.status_code, 302) experiment = Experiment.objects.get() - self.assertEqual(experiment.name, data['name']) + self.assertEqual(experiment.name, data["name"]) self.assertEqual( - experiment.short_description, data['short_description']) + experiment.short_description, data["short_description"] + ) self.assertEqual( experiment.population_percent, - decimal.Decimal(data['population_percent']), + decimal.Decimal(data["population_percent"]), ) - self.assertEqual(experiment.firefox_version, data['firefox_version']) - self.assertEqual(experiment.firefox_channel, data['firefox_channel']) + self.assertEqual(experiment.firefox_version, data["firefox_version"]) + self.assertEqual(experiment.firefox_channel, data["firefox_channel"]) self.assertEqual(experiment.proposed_start_date, new_start_date) self.assertEqual(experiment.proposed_end_date, new_end_date) @@ -459,29 +471,30 @@ def test_view_saves_experiment(self): class TestExperimentVariantsUpdateView(TestCase): def test_view_saves_experiment(self): - user_email = 'user@example.com' + user_email = "user@example.com" experiment = ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT) + Experiment.STATUS_DRAFT + ) data = { - 'pref_key': 'browser.testing.tests-enabled', - 'pref_type': Experiment.PREF_TYPE_BOOL, - 'pref_branch': Experiment.PREF_BRANCH_DEFAULT, - 'control-name': 'The Control Variant', - 'control-description': 'Its the control! So controlly.', - 'control-ratio': 60, - 'control-value': 'false', - 'experimental-name': 'The Experimental Variant', - 'experimental-description': ( - 'Its the experimental! So experimentally.'), - 'experimental-ratio': 40, - 'experimental-value': 'true', + "pref_key": "browser.testing.tests-enabled", + "pref_type": Experiment.PREF_TYPE_BOOL, + "pref_branch": Experiment.PREF_BRANCH_DEFAULT, + "control-name": "The Control Variant", + "control-description": "Its the control! So controlly.", + "control-ratio": 60, + "control-value": "false", + "experimental-name": "The Experimental Variant", + "experimental-description": ( + "Its the experimental! So experimentally." + ), + "experimental-ratio": 40, + "experimental-value": "true", } response = self.client.post( reverse( - 'experiments-variants-update', - kwargs={'slug': experiment.slug}, + "experiments-variants-update", kwargs={"slug": experiment.slug} ), data, **{settings.OPENIDC_EMAIL_HEADER: user_email}, @@ -490,24 +503,21 @@ def test_view_saves_experiment(self): experiment = Experiment.objects.get() - self.assertEqual(experiment.pref_key, data['pref_key']) - self.assertEqual(experiment.pref_type, data['pref_type']) - self.assertEqual(experiment.pref_branch, data['pref_branch']) - self.assertEqual(experiment.control.name, data['control-name']) - self.assertEqual( - experiment.control.description, data['control-description']) - self.assertEqual(experiment.control.ratio, data['control-ratio']) - self.assertEqual(experiment.control.value, data['control-value']) - self.assertEqual( - experiment.variant.name, data['experimental-name']) + self.assertEqual(experiment.pref_key, data["pref_key"]) + self.assertEqual(experiment.pref_type, data["pref_type"]) + self.assertEqual(experiment.pref_branch, data["pref_branch"]) + self.assertEqual(experiment.control.name, data["control-name"]) self.assertEqual( - experiment.variant.description, - data['experimental-description'], + experiment.control.description, data["control-description"] ) + self.assertEqual(experiment.control.ratio, data["control-ratio"]) + self.assertEqual(experiment.control.value, data["control-value"]) + self.assertEqual(experiment.variant.name, data["experimental-name"]) self.assertEqual( - experiment.variant.ratio, data['experimental-ratio']) - self.assertEqual( - experiment.variant.value, data['experimental-value']) + experiment.variant.description, data["experimental-description"] + ) + self.assertEqual(experiment.variant.ratio, data["experimental-ratio"]) + self.assertEqual(experiment.variant.value, data["experimental-value"]) self.assertEqual(experiment.changes.count(), 2) @@ -521,19 +531,20 @@ def test_view_saves_experiment(self): class TestExperimentObjectivesUpdateView(TestCase): def test_view_saves_experiment(self): - user_email = 'user@example.com' + user_email = "user@example.com" experiment = ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT) + Experiment.STATUS_DRAFT + ) data = { - 'objectives': 'Some new objectives!', - 'analysis': 'Some new analysis!', + "objectives": "Some new objectives!", + "analysis": "Some new analysis!", } response = self.client.post( reverse( - 'experiments-objectives-update', - kwargs={'slug': experiment.slug}, + "experiments-objectives-update", + kwargs={"slug": experiment.slug}, ), data, **{settings.OPENIDC_EMAIL_HEADER: user_email}, @@ -541,8 +552,8 @@ def test_view_saves_experiment(self): self.assertEqual(response.status_code, 302) experiment = Experiment.objects.get() - self.assertEqual(experiment.objectives, data['objectives']) - self.assertEqual(experiment.analysis, data['analysis']) + self.assertEqual(experiment.objectives, data["objectives"]) + self.assertEqual(experiment.analysis, data["analysis"]) self.assertEqual(experiment.changes.count(), 2) @@ -556,24 +567,24 @@ def test_view_saves_experiment(self): class TestExperimentRisksUpdateView(TestCase): def test_view_saves_experiment(self): - user_email = 'user@example.com' + user_email = "user@example.com" experiment = ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT) + Experiment.STATUS_DRAFT + ) data = { - 'risk_partner_related': False, - 'risk_brand': True, - 'risk_fast_shipped': False, - 'risk_confidential': True, - 'risk_release_population': False, - 'risks': 'There are some risks', - 'testing': 'Always be sure to test!', + "risk_partner_related": False, + "risk_brand": True, + "risk_fast_shipped": False, + "risk_confidential": True, + "risk_release_population": False, + "risks": "There are some risks", + "testing": "Always be sure to test!", } response = self.client.post( reverse( - 'experiments-risks-update', - kwargs={'slug': experiment.slug}, + "experiments-risks-update", kwargs={"slug": experiment.slug} ), data, **{settings.OPENIDC_EMAIL_HEADER: user_email}, @@ -583,18 +594,20 @@ def test_view_saves_experiment(self): experiment = Experiment.objects.get() self.assertEqual( - experiment.risk_partner_related, data['risk_partner_related']) - self.assertEqual(experiment.risk_brand, data['risk_brand']) + experiment.risk_partner_related, data["risk_partner_related"] + ) + self.assertEqual(experiment.risk_brand, data["risk_brand"]) self.assertEqual( - experiment.risk_fast_shipped, data['risk_fast_shipped']) + experiment.risk_fast_shipped, data["risk_fast_shipped"] + ) self.assertEqual( - experiment.risk_confidential, data['risk_confidential']) + experiment.risk_confidential, data["risk_confidential"] + ) self.assertEqual( - experiment.risk_release_population, - data['risk_release_population'], + experiment.risk_release_population, data["risk_release_population"] ) - self.assertEqual(experiment.risks, data['risks']) - self.assertEqual(experiment.testing, data['testing']) + self.assertEqual(experiment.risks, data["risks"]) + self.assertEqual(experiment.testing, data["testing"]) self.assertEqual(experiment.changes.count(), 2) @@ -608,12 +621,13 @@ def test_view_saves_experiment(self): class TestExperimentDetailView(TestCase): def test_view_renders_correctly(self): - user_email = 'user@example.com' + user_email = "user@example.com" experiment = ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT) + Experiment.STATUS_DRAFT + ) response = self.client.get( - reverse('experiments-detail', kwargs={'slug': experiment.slug}), + reverse("experiments-detail", kwargs={"slug": experiment.slug}), **{settings.OPENIDC_EMAIL_HEADER: user_email}, ) @@ -623,43 +637,45 @@ def test_view_renders_correctly(self): class TestExperimentStatusUpdateView(TestCase): def test_view_updates_status_and_redirects(self): - user_email = 'user@example.com' + user_email = "user@example.com" experiment = ExperimentFactory.create_with_status( - Experiment.STATUS_DRAFT) + Experiment.STATUS_DRAFT + ) new_status = experiment.STATUS_REVIEW response = self.client.post( reverse( - 'experiments-status-update', kwargs={'slug': experiment.slug}), - {'status': new_status}, + "experiments-status-update", kwargs={"slug": experiment.slug} + ), + {"status": new_status}, **{settings.OPENIDC_EMAIL_HEADER: user_email}, ) self.assertRedirects( response, - reverse('experiments-detail', kwargs={'slug': experiment.slug}), + reverse("experiments-detail", kwargs={"slug": experiment.slug}), fetch_redirect_response=False, ) updated_experiment = Experiment.objects.get(slug=experiment.slug) self.assertEqual(updated_experiment.status, new_status) def test_view_redirects_on_failure(self): - user_email = 'user@example.com' + user_email = "user@example.com" original_status = Experiment.STATUS_DRAFT - experiment = ExperimentFactory.create_with_status( - original_status) + experiment = ExperimentFactory.create_with_status(original_status) response = self.client.post( reverse( - 'experiments-status-update', kwargs={'slug': experiment.slug}), - {'status': Experiment.STATUS_COMPLETE}, + "experiments-status-update", kwargs={"slug": experiment.slug} + ), + {"status": Experiment.STATUS_COMPLETE}, **{settings.OPENIDC_EMAIL_HEADER: user_email}, ) self.assertRedirects( response, - reverse('experiments-detail', kwargs={'slug': experiment.slug}), + reverse("experiments-detail", kwargs={"slug": experiment.slug}), fetch_redirect_response=False, ) updated_experiment = Experiment.objects.get(slug=experiment.slug) diff --git a/app/experimenter/experiments/views.py b/app/experimenter/experiments/views.py index 185c1fb3b3..69109a4083 100644 --- a/app/experimenter/experiments/views.py +++ b/app/experimenter/experiments/views.py @@ -8,10 +8,10 @@ from experimenter.projects.models import Project from experimenter.experiments.forms import ( - ExperimentStatusForm, ExperimentObjectivesForm, ExperimentOverviewForm, ExperimentRisksForm, + ExperimentStatusForm, ExperimentVariantsForm, ) from experimenter.experiments.models import Experiment @@ -21,12 +21,10 @@ class ExperimentFiltersetForm(forms.ModelForm): class Meta: model = Experiment - fields = ( - 'archived', - ) + fields = ("archived",) def clean_archived(self): - allow_archived = self.cleaned_data.get('archived', False) + allow_archived = self.cleaned_data.get("archived", False) # If we pass in archived=True what we actually mean is # don't filter on archived at all, ie show all experiments @@ -39,69 +37,68 @@ def clean_archived(self): class ExperimentFilterset(filters.FilterSet): archived = filters.BooleanFilter( - label='Show archived experiments', - widget=forms.CheckboxInput(), + label="Show archived experiments", widget=forms.CheckboxInput() ) project = filters.ModelChoiceFilter( - empty_label='All Projects', + empty_label="All Projects", queryset=Project.objects.all(), - widget=forms.Select(attrs={'class': 'form-control'}), + widget=forms.Select(attrs={"class": "form-control"}), ) owner = filters.ModelChoiceFilter( - empty_label='All Owners', + empty_label="All Owners", queryset=get_user_model().objects.all(), - widget=forms.Select(attrs={'class': 'form-control'}), + widget=forms.Select(attrs={"class": "form-control"}), ) status = filters.ChoiceFilter( - empty_label='All Statuses', + empty_label="All Statuses", choices=Experiment.STATUS_CHOICES, - widget=forms.Select(attrs={'class': 'form-control'}), + widget=forms.Select(attrs={"class": "form-control"}), ) firefox_channel = filters.ChoiceFilter( - empty_label='All Channels', + empty_label="All Channels", choices=Experiment.CHANNEL_CHOICES[1:], - widget=forms.Select(attrs={'class': 'form-control'}), + widget=forms.Select(attrs={"class": "form-control"}), ) firefox_version = filters.ChoiceFilter( - empty_label='All Versions', + empty_label="All Versions", choices=Experiment.VERSION_CHOICES[1:], - widget=forms.Select(attrs={'class': 'form-control'}), + widget=forms.Select(attrs={"class": "form-control"}), ) class Meta: model = Experiment form = ExperimentFiltersetForm fields = ( - 'archived', - 'firefox_channel', - 'firefox_version', - 'owner', - 'project', - 'status', + "archived", + "firefox_channel", + "firefox_version", + "owner", + "project", + "status", ) class ExperimentOrderingForm(forms.Form): ORDERING_CHOICES = ( - ('-latest_change', 'Most Recently Updated'), - ('latest_change', 'Least Recently Updated'), - ('firefox_version', 'Firefox Version Ascending'), - ('-firefox_version', 'Firefox Version Descending'), - ('firefox_channel', 'Firefox Channel Ascending'), - ('-firefox_channel', 'Firefox Channel Descending'), + ("-latest_change", "Most Recently Updated"), + ("latest_change", "Least Recently Updated"), + ("firefox_version", "Firefox Version Ascending"), + ("-firefox_version", "Firefox Version Descending"), + ("firefox_channel", "Firefox Channel Ascending"), + ("-firefox_channel", "Firefox Channel Descending"), ) ordering = forms.ChoiceField( choices=ORDERING_CHOICES, - widget=forms.Select(attrs={'class': 'form-control'}), + widget=forms.Select(attrs={"class": "form-control"}), ) class ExperimentListView(FilterView): - model = Experiment - context_object_name = 'experiments' - template_name = 'experiments/list.html' + context_object_name = "experiments" filterset_class = ExperimentFilterset + model = Experiment + template_name = "experiments/list.html" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -113,20 +110,21 @@ def get_filterset_kwargs(self, *args, **kwargs): # Always pass in request.GET otherwise the # filterset form will be unbound and our custom # validation won't kick in - kwargs['data'] = self.request.GET + kwargs["data"] = self.request.GET return kwargs def get_ordering(self): self.ordering_form = ExperimentOrderingForm(self.request.GET) if self.ordering_form.is_valid(): - return self.ordering_form.cleaned_data['ordering'] + return self.ordering_form.cleaned_data["ordering"] return self.ordering_form.ORDERING_CHOICES[0][0] def get_context_data(self, *args, **kwargs): return super().get_context_data( - ordering_form=self.ordering_form, *args, **kwargs) + ordering_form=self.ordering_form, *args, **kwargs + ) class ExperimentFormMixin(object): @@ -134,63 +132,64 @@ class ExperimentFormMixin(object): def get_form_kwargs(self): kwargs = super().get_form_kwargs() - kwargs['request'] = self.request + kwargs["request"] = self.request return kwargs def get_success_url(self): if ( - 'action' in self.request.POST and - self.request.POST['action'] == 'continue' + "action" in self.request.POST + and self.request.POST["action"] == "continue" ): return reverse( - self.next_view_name, kwargs={'slug': self.object.slug}) + self.next_view_name, kwargs={"slug": self.object.slug} + ) - return reverse('experiments-detail', kwargs={'slug': self.object.slug}) + return reverse("experiments-detail", kwargs={"slug": self.object.slug}) class ExperimentCreateView(ExperimentFormMixin, CreateView): form_class = ExperimentOverviewForm - template_name = 'experiments/edit_overview.html' - next_view_name = 'experiments-variants-update' + next_view_name = "experiments-variants-update" + template_name = "experiments/edit_overview.html" def get_initial(self): initial = super().get_initial() - if 'project' in self.request.GET: - initial['project'] = self.request.GET['project'] + if "project" in self.request.GET: + initial["project"] = self.request.GET["project"] - initial['owner'] = self.request.user.id + initial["owner"] = self.request.user.id return initial class ExperimentOverviewUpdateView(ExperimentFormMixin, UpdateView): form_class = ExperimentOverviewForm - template_name = 'experiments/edit_overview.html' - next_view_name = 'experiments-variants-update' + next_view_name = "experiments-variants-update" + template_name = "experiments/edit_overview.html" class ExperimentVariantsUpdateView(ExperimentFormMixin, UpdateView): form_class = ExperimentVariantsForm - template_name = 'experiments/edit_variants.html' - next_view_name = 'experiments-objectives-update' + next_view_name = "experiments-objectives-update" + template_name = "experiments/edit_variants.html" class ExperimentObjectivesUpdateView(ExperimentFormMixin, UpdateView): form_class = ExperimentObjectivesForm - template_name = 'experiments/edit_objectives.html' - next_view_name = 'experiments-risks-update' + next_view_name = "experiments-risks-update" + template_name = "experiments/edit_objectives.html" class ExperimentRisksUpdateView(ExperimentFormMixin, UpdateView): form_class = ExperimentRisksForm - template_name = 'experiments/edit_risks.html' - next_view_name = 'experiments-detail' + next_view_name = "experiments-detail" + template_name = "experiments/edit_risks.html" class ExperimentDetailView(DetailView): model = Experiment - template_name = 'experiments/detail.html' + template_name = "experiments/detail.html" class ExperimentStatusUpdateView(ExperimentFormMixin, UpdateView): @@ -198,8 +197,9 @@ class ExperimentStatusUpdateView(ExperimentFormMixin, UpdateView): model = Experiment def get_success_url(self): - return reverse('experiments-detail', kwargs={'slug': self.object.slug}) + return reverse("experiments-detail", kwargs={"slug": self.object.slug}) def form_invalid(self, form): return redirect( - reverse('experiments-detail', kwargs={'slug': self.object.slug})) + reverse("experiments-detail", kwargs={"slug": self.object.slug}) + ) diff --git a/app/experimenter/experiments/web_urls.py b/app/experimenter/experiments/web_urls.py index ad4657b920..177830eb8d 100644 --- a/app/experimenter/experiments/web_urls.py +++ b/app/experimenter/experiments/web_urls.py @@ -1,50 +1,46 @@ from django.conf.urls import url from experimenter.experiments.views import ( - ExperimentStatusUpdateView, ExperimentCreateView, ExperimentDetailView, ExperimentObjectivesUpdateView, ExperimentOverviewUpdateView, ExperimentRisksUpdateView, + ExperimentStatusUpdateView, ExperimentVariantsUpdateView, ) urlpatterns = [ + url(r"^new/$", ExperimentCreateView.as_view(), name="experiments-create"), url( - r'^new/$', - ExperimentCreateView.as_view(), - name='experiments-create', - ), - url( - r'^(?P