feat(experiments): added experiments business logic
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
# Generated by Django 5.2.11 on 2026-02-14 09:55
|
||||
|
||||
import django.core.validators
|
||||
import uuid
|
||||
from decimal import Decimal
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Approval',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('comment', models.TextField(blank=True, verbose_name='comment')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'approval',
|
||||
'verbose_name_plural': 'approvals',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Experiment',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=200, verbose_name='name')),
|
||||
('description', models.TextField(blank=True, verbose_name='description')),
|
||||
('hypothesis', models.TextField(blank=True, verbose_name='hypothesis')),
|
||||
('status', models.CharField(choices=[('draft', 'Draft'), ('in_review', 'In review'), ('approved', 'Approved'), ('running', 'Running'), ('paused', 'Paused'), ('completed', 'Completed'), ('archived', 'Archived'), ('rejected', 'Rejected')], db_index=True, default='draft', max_length=20, verbose_name='status')),
|
||||
('version', models.PositiveIntegerField(default=1, verbose_name='version')),
|
||||
('traffic_allocation', models.DecimalField(decimal_places=2, default=Decimal('100.00'), help_text='Percentage of eligible users included in experiment', max_digits=5, validators=[django.core.validators.MinValueValidator(Decimal('0.01')), django.core.validators.MaxValueValidator(Decimal('100.00'))], verbose_name='traffic allocation %')),
|
||||
('targeting_rules', models.TextField(blank=True, help_text='DSL expression for targeting', verbose_name='targeting rules')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'experiment',
|
||||
'verbose_name_plural': 'experiments',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ExperimentLog',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('log_type', models.CharField(choices=[('status_change', 'Status change'), ('approval', 'Approval'), ('rejection', 'Rejection'), ('review_requested', 'Review requested'), ('guardrail_triggered', 'Guardrail triggered'), ('completed', 'Completed'), ('comment', 'Comment')], max_length=30, verbose_name='log type')),
|
||||
('comment', models.TextField(blank=True, verbose_name='comment')),
|
||||
('metadata', models.JSONField(blank=True, default=dict, verbose_name='metadata')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'experiment log',
|
||||
'verbose_name_plural': 'experiment logs',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ExperimentOutcome',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('outcome', models.CharField(choices=[('rollout', 'Rollout winner'), ('rollback', 'Rollback'), ('no_effect', 'No effect')], max_length=20, verbose_name='outcome')),
|
||||
('rationale', models.TextField(verbose_name='rationale')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'experiment outcome',
|
||||
'verbose_name_plural': 'experiment outcomes',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Variant',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=100, verbose_name='name')),
|
||||
('value', models.CharField(help_text='Value returned when this variant is selected', max_length=500, verbose_name='value')),
|
||||
('weight', models.DecimalField(decimal_places=2, max_digits=5, validators=[django.core.validators.MinValueValidator(Decimal('0.00')), django.core.validators.MaxValueValidator(Decimal('100.00'))], verbose_name='weight %')),
|
||||
('is_control', models.BooleanField(default=False, verbose_name='is control')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'variant',
|
||||
'verbose_name_plural': 'variants',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,86 @@
|
||||
# Generated by Django 5.2.11 on 2026-02-14 09:55
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('experiments', '0001_initial'),
|
||||
('flags', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='approval',
|
||||
name='approver',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='experiment_approvals', to=settings.AUTH_USER_MODEL, verbose_name='approver'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='experiment',
|
||||
name='flag',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='experiments', to='flags.featureflag', verbose_name='feature flag'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='experiment',
|
||||
name='owner',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='owned_experiments', to=settings.AUTH_USER_MODEL, verbose_name='owner'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='experiment',
|
||||
name='previous_version',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='next_versions', to='experiments.experiment', verbose_name='previous version'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='approval',
|
||||
name='experiment',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='approvals', to='experiments.experiment', verbose_name='experiment'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='experimentlog',
|
||||
name='experiment',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='experiments.experiment', verbose_name='experiment'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='experimentlog',
|
||||
name='user',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='experiment_logs', to=settings.AUTH_USER_MODEL, verbose_name='user'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='experimentoutcome',
|
||||
name='decided_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='experiment_decisions', to=settings.AUTH_USER_MODEL, verbose_name='decided by'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='experimentoutcome',
|
||||
name='experiment',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='outcome', to='experiments.experiment', verbose_name='experiment'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='variant',
|
||||
name='experiment',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='variants', to='experiments.experiment', verbose_name='experiment'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='experimentoutcome',
|
||||
name='winning_variant',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wins', to='experiments.variant', verbose_name='winning variant'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='experiment',
|
||||
index=models.Index(fields=['flag', 'status'], name='idx_experiment_flag_status'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='experiment',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('status__in', ('running', 'paused'))), fields=('flag',), name='unique_active_experiment_per_flag'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='approval',
|
||||
constraint=models.UniqueConstraint(fields=('experiment', 'approver'), name='unique_approval_per_user'),
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user