Коммит
e37b91abbd
|
@ -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"
|
||||
|
|
Загрузка…
Ссылка в новой задаче