Merge pull request #8 from mozilla/3

Add Experiments fixes #3
This commit is contained in:
Jared Kerim 2016-12-14 11:30:14 -05:00 коммит произвёл GitHub
Родитель 0dfd8e14e4 87cf11d575
Коммит e37b91abbd
14 изменённых файлов: 303 добавлений и 0 удалений

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

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

@ -0,0 +1,50 @@
from django.contrib import admin
from django import forms
from experimenter.experiments.models import Experiment, ExperimentVariant
class BaseVariantInlineAdmin(admin.StackedInline):
max_num = 1
model = ExperimentVariant
prepopulated_fields = {'slug': ('name',)}
def has_delete_permission(self, request, obj=None):
return False
class ControlVariantModelForm(forms.ModelForm):
def save(self, commit=True):
self.instance.is_control = True
return super().save(commit=commit)
class Meta:
model = ExperimentVariant
exclude = []
class ControlVariantInlineAdmin(BaseVariantInlineAdmin):
form = ControlVariantModelForm
verbose_name = 'Control Variant'
verbose_name_plural = 'Control Variant'
fields = ('name', 'slug', 'description', 'value')
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', 'threshold', 'description', 'value')
def get_queryset(self, request):
return super().get_queryset(request).filter(is_control=False)
class ExperimentAdmin(admin.ModelAdmin):
inlines = (ControlVariantInlineAdmin, ExperimentVariantInlineAdmin,)
prepopulated_fields = {'slug': ('name',)}
admin.site.register(Experiment, ExperimentAdmin)

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

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.5 on 2016-11-15 20:15
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
]
operations = [
]

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

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.3 on 2016-11-25 19:50
from __future__ import unicode_literals
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('projects', '0002_project'),
('experiments', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Experiment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('active', models.BooleanField(default=False)),
('name', models.CharField(max_length=255, unique=True)),
('slug', models.SlugField(max_length=255, unique=True)),
('objectives', models.TextField(default='')),
('success_criteria', models.TextField(default='')),
('start_date', models.DateTimeField(blank=True, null=True)),
('end_date', models.DateTimeField(blank=True, null=True)),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='projects.Project')),
],
options={
'verbose_name_plural': 'Experiments',
'verbose_name': 'Experiment',
},
),
migrations.CreateModel(
name='ExperimentVariant',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
('slug', models.SlugField(max_length=255, unique=True)),
('is_control', models.BooleanField(default=False)),
('description', models.TextField(default='')),
('threshold', models.PositiveIntegerField(default=0)),
('value', django.contrib.postgres.fields.jsonb.JSONField(default=False)),
('experiment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='experiments.Experiment')),
],
options={
'verbose_name_plural': 'Experiment Variants',
'verbose_name': 'Experiment Variant',
},
),
]

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

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

@ -0,0 +1,41 @@
from django.contrib.postgres.fields import JSONField
from django.db import models
class Experiment(models.Model):
active = models.BooleanField(default=False)
project = models.ForeignKey('projects.Project', blank=False, null=False)
name = models.CharField(
max_length=255, unique=True, blank=False, null=False)
slug = models.SlugField(
max_length=255, unique=True, blank=False, null=False)
objectives = models.TextField(default='')
success_criteria = models.TextField(default='')
start_date = models.DateTimeField(blank=True, null=True)
end_date = models.DateTimeField(blank=True, null=True)
def __str__(self): # pragma: no cover
return self.name
class Meta:
verbose_name = 'Experiment'
verbose_name_plural = 'Experiments'
class ExperimentVariant(models.Model):
experiment = models.ForeignKey(Experiment, blank=False, null=False)
name = models.CharField(
max_length=255, unique=True, blank=False, null=False)
slug = models.SlugField(
max_length=255, unique=True, blank=False, null=False)
is_control = models.BooleanField(default=False)
description = models.TextField(default='')
threshold = models.PositiveIntegerField(default=0)
value = JSONField(default=False)
def __str__(self): # pragma: no cover
return self.name
class Meta:
verbose_name = 'Experiment Variant'
verbose_name_plural = 'Experiment Variants'

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

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

@ -0,0 +1,54 @@
import datetime
import factory
from django.utils.text import slugify
from faker import Factory as FakerFactory
from experimenter.projects.tests.factories import ProjectFactory
from experimenter.experiments.models import Experiment, ExperimentVariant
faker = FakerFactory.create()
class ExperimentFactory(factory.django.DjangoModelFactory):
active = True
project = factory.SubFactory(ProjectFactory)
name = factory.LazyAttribute(lambda o: faker.catch_phrase())
slug = factory.LazyAttribute(lambda o: slugify(o.name))
objectives = factory.LazyAttribute(lambda o: faker.paragraphs())
success_criteria = factory.LazyAttribute(lambda o: faker.paragraphs())
start_date = factory.LazyAttribute(lambda o: datetime.datetime.now())
end_date = factory.LazyAttribute(
lambda o: o.start_date + datetime.timedelta(weeks=2))
class Meta:
model = Experiment
@classmethod
def create_with_variants(cls, *args, **kwargs):
experiment = cls.create(*args, **kwargs)
ControlVariantFactory.create(experiment=experiment)
ExperimentVariantFactory.create(experiment=experiment)
return experiment
class BaseExperimentVariantFactory(factory.django.DjangoModelFactory):
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.paragraphs())
class Meta:
model = ExperimentVariant
class ControlVariantFactory(BaseExperimentVariantFactory):
is_control = True
threshold = 0
value = 'false'
class ExperimentVariantFactory(BaseExperimentVariantFactory):
is_control = False
threshold = 10
value = 'true'

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

@ -0,0 +1,69 @@
import mock
from django.test import TestCase
from experimenter.experiments.models import ExperimentVariant
from experimenter.experiments.tests.factories import (
ControlVariantFactory,
ExperimentFactory,
)
from experimenter.experiments.admin import (
BaseVariantInlineAdmin,
ControlVariantInlineAdmin,
ExperimentVariantInlineAdmin,
ControlVariantModelForm,
)
class BaseVariantInlineAdminTest(TestCase):
def test_has_no_delete_permissions(self):
inline_admin = BaseVariantInlineAdmin(mock.Mock(), mock.Mock())
self.assertFalse(inline_admin.has_delete_permission(mock.Mock()))
class ControlVariantModelFormTests(TestCase):
def test_save_sets_is_control_to_True(self):
experiment = ExperimentFactory.create()
variant_data = ControlVariantFactory.attributes()
variant_data['experiment'] = experiment.id
variant_data['is_control'] = False
form = ControlVariantModelForm(data=variant_data)
self.assertTrue(form.is_valid())
self.assertFalse(form.instance.is_control)
instance = form.save()
self.assertTrue(instance.is_control)
class ControlVariantInlineAdminTest(TestCase):
def test_queryset_filters_is_control_True(self):
ExperimentFactory.create_with_variants()
self.assertEqual(ExperimentVariant.objects.all().count(), 2)
control_admin = ControlVariantInlineAdmin(mock.Mock(), mock.Mock())
variants = control_admin.get_queryset(mock.Mock())
self.assertEqual(variants.count(), 1)
self.assertEqual(variants.filter(is_control=True).count(), 1)
self.assertEqual(variants.filter(is_control=False).count(), 0)
class ExperimentVariantInlineAdminTest(TestCase):
def test_queryset_filters_is_control_True(self):
ExperimentFactory.create_with_variants()
self.assertEqual(ExperimentVariant.objects.all().count(), 2)
control_admin = ExperimentVariantInlineAdmin(mock.Mock(), mock.Mock())
variants = control_admin.get_queryset(mock.Mock())
self.assertEqual(variants.count(), 1)
self.assertEqual(variants.filter(is_control=False).count(), 1)
self.assertEqual(variants.filter(is_control=True).count(), 0)

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

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

@ -0,0 +1,15 @@
import factory
from django.utils.text import slugify
from faker import Factory as FakerFactory
from experimenter.projects.models import Project
faker = FakerFactory.create()
class ProjectFactory(factory.django.DjangoModelFactory):
name = factory.LazyAttribute(lambda o: faker.catch_phrase())
slug = factory.LazyAttribute(lambda o: slugify(o.name))
class Meta:
model = Project

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

@ -42,6 +42,7 @@ INSTALLED_APPS = [
'experimenter.home',
'experimenter.projects',
'experimenter.experiments',
]
MIDDLEWARE_CLASSES = [

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

@ -1,5 +1,7 @@
Django==1.10.3
coverage==4.2
factory-boy==2.7.0
flake8==3.0.4
ipdb==0.10.1
mock==2.0.0
psycopg2==2.6.1

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

@ -7,6 +7,8 @@ dependencies:
pre:
- sudo service postgresql stop
- sudo /etc/init.d/postgresql stop
- sudo apt-get remove -y postgresql-9.5
- sudo apt-get remove -y postgresql-9.6
cache_directories:
- "~/docker"