From 77469cc86f4b15394d5cd32622938435b1434946 Mon Sep 17 00:00:00 2001 From: tiftran Date: Thu, 16 Jul 2020 00:24:31 -0700 Subject: [PATCH] v4 public api fixes #3010 --- .../base/management/commands/generate_docs.py | 4 + app/experimenter/docs/openapi-schema.json | 240 ++++++++++++++++++ app/experimenter/docs/swagger-ui.html | 240 ++++++++++++++++++ .../experiments/api/v1/serializers.py | 71 ------ .../experiments/api/v4/__init__.py | 0 .../experiments/api/v4/serializers.py | 97 +++++++ app/experimenter/experiments/api/v4/urls.py | 23 ++ app/experimenter/experiments/api/v4/views.py | 35 +++ .../tests/api/v1/experimentRecipe.json | 163 ------------ .../tests/api/v1/test_serializers.py | 74 ------ app/experimenter/kinto/tasks.py | 2 +- app/experimenter/kinto/tests/test_tasks.py | 2 +- app/experimenter/urls.py | 1 + 13 files changed, 642 insertions(+), 310 deletions(-) create mode 100644 app/experimenter/experiments/api/v4/__init__.py create mode 100644 app/experimenter/experiments/api/v4/serializers.py create mode 100644 app/experimenter/experiments/api/v4/urls.py create mode 100644 app/experimenter/experiments/api/v4/views.py delete mode 100644 app/experimenter/experiments/tests/api/v1/experimentRecipe.json diff --git a/app/experimenter/base/management/commands/generate_docs.py b/app/experimenter/base/management/commands/generate_docs.py index 62fb87fc86..b810572fad 100644 --- a/app/experimenter/base/management/commands/generate_docs.py +++ b/app/experimenter/base/management/commands/generate_docs.py @@ -36,6 +36,10 @@ def generateSchema(): elif "/api/v3/" in path: for method in paths[path]: paths[path][method]["tags"] = ["Rapid: Private"] + elif "/api/v4/" in path: + for method in paths[path]: + paths[path][method]["tags"] = ["Rapid: Public"] + return json.dumps(schema, indent=2) @staticmethod diff --git a/app/experimenter/docs/openapi-schema.json b/app/experimenter/docs/openapi-schema.json index b861ee3107..ab0507c47d 100644 --- a/app/experimenter/docs/openapi-schema.json +++ b/app/experimenter/docs/openapi-schema.json @@ -8064,6 +8064,246 @@ ] } }, + "/api/v4/experiments": { + "get": { + "operationId": "listExperiments", + "description": "", + "parameters": [ + { + "name": "status", + "required": false, + "in": "query", + "description": "status", + "schema": { + "type": "string", + "enum": [ + "Draft", + "Review", + "Ship", + "Accepted", + "Live", + "Complete" + ] + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "properties": { + "audience": { + "enum": [ + "AUDIENCE 1", + "AUDIENCE 2" + ], + "nullable": true + }, + "bugzilla_url": { + "type": "string", + "readOnly": true + }, + "features": { + "type": "array", + "items": { + "type": null + } + }, + "name": { + "type": "string", + "maxLength": 255 + }, + "objectives": { + "type": "string", + "nullable": true + }, + "owner": { + "type": "integer", + "nullable": true + }, + "public_description": { + "type": "string", + "nullable": true + }, + "rapid_type": { + "enum": [ + "cfr a/a" + ], + "nullable": true + }, + "slug": { + "type": "string", + "maxLength": 255, + "pattern": "^[-a-zA-Z0-9_]+$" + }, + "status": { + "enum": [ + "Draft", + "Review", + "Ship", + "Accepted", + "Live", + "Complete" + ] + }, + "type": { + "enum": [ + "pref", + "addon", + "generic", + "rollout", + "message", + "rapid" + ] + } + }, + "required": [ + "name", + "slug" + ] + } + } + } + }, + "description": "" + } + }, + "tags": [ + "Rapid: Public" + ] + } + }, + "/api/v4/{slug}/recipe/": { + "get": { + "operationId": "RetrieveExperiment", + "description": "", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "description": "", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "string", + "readOnly": true + }, + "arguments": { + "properties": { + "slug": { + "type": "string", + "readOnly": true + }, + "userFacingName": { + "type": "string", + "readOnly": true + }, + "userFacingDescription": { + "type": "string", + "readOnly": true + }, + "active": { + "type": "string", + "readOnly": true, + "default": true + }, + "isEnrollmentPaused": { + "type": "string", + "readOnly": true + }, + "features": { + "type": "array", + "items": { + "type": null + } + }, + "proposedEnrollment": { + "type": "string", + "readOnly": true + }, + "bucketConfig": { + "type": "string", + "readOnly": true + }, + "startDate": { + "type": "string", + "readOnly": true + }, + "endDate": { + "type": "string", + "readOnly": true + }, + "branches": { + "type": "array", + "items": { + "properties": { + "slug": { + "type": "string", + "maxLength": 255, + "pattern": "^[-a-zA-Z0-9_]+$" + }, + "ratio": { + "type": "integer", + "maximum": 2147483647, + "minimum": 0 + }, + "value": { + "type": "string", + "readOnly": true + } + }, + "required": [ + "slug" + ] + } + }, + "referenceBranch": { + "type": "string", + "readOnly": true + } + }, + "required": [ + "branches" + ], + "type": "object" + }, + "filter_expression": { + "type": "string", + "readOnly": true + }, + "enabled": { + "type": "string", + "readOnly": true, + "default": true + } + }, + "required": [ + "arguments" + ] + } + } + }, + "description": "" + } + }, + "tags": [ + "Rapid: Public" + ] + } + }, "/api/v3/experiments/": { "post": { "operationId": "createExperiment", diff --git a/app/experimenter/docs/swagger-ui.html b/app/experimenter/docs/swagger-ui.html index b515182c26..0b6aaff8f0 100644 --- a/app/experimenter/docs/swagger-ui.html +++ b/app/experimenter/docs/swagger-ui.html @@ -8076,6 +8076,246 @@ ] } }, + "/api/v4/experiments": { + "get": { + "operationId": "listExperiments", + "description": "", + "parameters": [ + { + "name": "status", + "required": false, + "in": "query", + "description": "status", + "schema": { + "type": "string", + "enum": [ + "Draft", + "Review", + "Ship", + "Accepted", + "Live", + "Complete" + ] + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "properties": { + "audience": { + "enum": [ + "AUDIENCE 1", + "AUDIENCE 2" + ], + "nullable": true + }, + "bugzilla_url": { + "type": "string", + "readOnly": true + }, + "features": { + "type": "array", + "items": { + "type": null + } + }, + "name": { + "type": "string", + "maxLength": 255 + }, + "objectives": { + "type": "string", + "nullable": true + }, + "owner": { + "type": "integer", + "nullable": true + }, + "public_description": { + "type": "string", + "nullable": true + }, + "rapid_type": { + "enum": [ + "cfr a/a" + ], + "nullable": true + }, + "slug": { + "type": "string", + "maxLength": 255, + "pattern": "^[-a-zA-Z0-9_]+$" + }, + "status": { + "enum": [ + "Draft", + "Review", + "Ship", + "Accepted", + "Live", + "Complete" + ] + }, + "type": { + "enum": [ + "pref", + "addon", + "generic", + "rollout", + "message", + "rapid" + ] + } + }, + "required": [ + "name", + "slug" + ] + } + } + } + }, + "description": "" + } + }, + "tags": [ + "Rapid: Public" + ] + } + }, + "/api/v4/{slug}/recipe/": { + "get": { + "operationId": "RetrieveExperiment", + "description": "", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "description": "", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "id": { + "type": "string", + "readOnly": true + }, + "arguments": { + "properties": { + "slug": { + "type": "string", + "readOnly": true + }, + "userFacingName": { + "type": "string", + "readOnly": true + }, + "userFacingDescription": { + "type": "string", + "readOnly": true + }, + "active": { + "type": "string", + "readOnly": true, + "default": true + }, + "isEnrollmentPaused": { + "type": "string", + "readOnly": true + }, + "features": { + "type": "array", + "items": { + "type": null + } + }, + "proposedEnrollment": { + "type": "string", + "readOnly": true + }, + "bucketConfig": { + "type": "string", + "readOnly": true + }, + "startDate": { + "type": "string", + "readOnly": true + }, + "endDate": { + "type": "string", + "readOnly": true + }, + "branches": { + "type": "array", + "items": { + "properties": { + "slug": { + "type": "string", + "maxLength": 255, + "pattern": "^[-a-zA-Z0-9_]+$" + }, + "ratio": { + "type": "integer", + "maximum": 2147483647, + "minimum": 0 + }, + "value": { + "type": "string", + "readOnly": true + } + }, + "required": [ + "slug" + ] + } + }, + "referenceBranch": { + "type": "string", + "readOnly": true + } + }, + "required": [ + "branches" + ], + "type": "object" + }, + "filter_expression": { + "type": "string", + "readOnly": true + }, + "enabled": { + "type": "string", + "readOnly": true, + "default": true + } + }, + "required": [ + "arguments" + ] + } + } + }, + "description": "" + } + }, + "tags": [ + "Rapid: Public" + ] + } + }, "/api/v3/experiments/": { "post": { "operationId": "createExperiment", diff --git a/app/experimenter/experiments/api/v1/serializers.py b/app/experimenter/experiments/api/v1/serializers.py index b2f01b7319..fdcc2409ae 100644 --- a/app/experimenter/experiments/api/v1/serializers.py +++ b/app/experimenter/experiments/api/v1/serializers.py @@ -187,74 +187,3 @@ def get_projects(self, obj): def get_related_to(self, obj): return ", ".join([e.experiment_url for e in obj.related_to.order_by("slug")]) - - -class ExperimentRapidBranchesSerializer(serializers.ModelSerializer): - value = serializers.SerializerMethodField() - - class Meta: - model = ExperimentVariant - fields = ("slug", "ratio", "value") - - def get_value(self, obj): - # placeholder value - return None - - -class ExperimentRapidArgumentSerializer(serializers.ModelSerializer): - slug = serializers.ReadOnlyField(source="normandy_slug") - userFacingName = serializers.ReadOnlyField(source="name") - userFacingDescription = serializers.ReadOnlyField(source="public_description") - active = serializers.ReadOnlyField(default=True) - isEnrollmentPaused = serializers.ReadOnlyField(default=False) - proposedEnrollment = serializers.ReadOnlyField(source="proposed_enrollment") - bucketConfig = serializers.SerializerMethodField() - startDate = serializers.SerializerMethodField() - endDate = serializers.ReadOnlyField(default=None) - branches = ExperimentRapidBranchesSerializer(many=True, source="variants") - referenceBranch = serializers.SerializerMethodField() - - class Meta: - model = Experiment - fields = ( - "slug", - "userFacingName", - "userFacingDescription", - "active", - "isEnrollmentPaused", - "features", - "proposedEnrollment", - "bucketConfig", - "startDate", - "endDate", - "branches", - "referenceBranch", - ) - - def get_bucketConfig(self, obj): - return { - "randomizationUnit": "normandy_id", - "namespace": "", - "start": 0, - "count": 0, - "total": 10000, - } - - def get_referenceBranch(self, obj): - control_branch = obj.variants.get(is_control=True) - return control_branch.slug - - def get_startDate(self, obj): - # placeholder value - return obj.start_date.isoformat() - - -class ExperimentRapidRecipeSerializer(serializers.ModelSerializer): - id = serializers.ReadOnlyField(source="normandy_slug") - arguments = ExperimentRapidArgumentSerializer(source="*") - filter_expression = serializers.ReadOnlyField(source="audience") - enabled = serializers.ReadOnlyField(default=True) - - class Meta: - model = Experiment - fields = ("id", "arguments", "filter_expression", "enabled") diff --git a/app/experimenter/experiments/api/v4/__init__.py b/app/experimenter/experiments/api/v4/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/experimenter/experiments/api/v4/serializers.py b/app/experimenter/experiments/api/v4/serializers.py new file mode 100644 index 0000000000..8d28595585 --- /dev/null +++ b/app/experimenter/experiments/api/v4/serializers.py @@ -0,0 +1,97 @@ +from rest_framework import serializers + +from experimenter.experiments.models import ( + Experiment, + ExperimentVariant, +) + + +class ExperimentRapidSerializer(serializers.ModelSerializer): + owner = serializers.ReadOnlyField(source="owner.email") + + class Meta: + model = Experiment + fields = ( + "audience", + "bugzilla_url", + "features", + "name", + "objectives", + "owner", + "public_description", + "rapid_type", + "slug", + "status", + "type", + ) + + +class ExperimentRapidBranchesSerializer(serializers.ModelSerializer): + value = serializers.SerializerMethodField() + + class Meta: + model = ExperimentVariant + fields = ("slug", "ratio", "value") + + def get_value(self, obj): + # placeholder value + return None + + +class ExperimentRapidArgumentSerializer(serializers.ModelSerializer): + slug = serializers.ReadOnlyField(source="normandy_slug") + userFacingName = serializers.ReadOnlyField(source="name") + userFacingDescription = serializers.ReadOnlyField(source="public_description") + active = serializers.ReadOnlyField(default=True) + isEnrollmentPaused = serializers.ReadOnlyField(default=False) + proposedEnrollment = serializers.ReadOnlyField(source="proposed_enrollment") + bucketConfig = serializers.SerializerMethodField() + startDate = serializers.SerializerMethodField() + endDate = serializers.ReadOnlyField(default=None) + branches = ExperimentRapidBranchesSerializer(many=True, source="variants") + referenceBranch = serializers.SerializerMethodField() + + class Meta: + model = Experiment + fields = ( + "slug", + "userFacingName", + "userFacingDescription", + "active", + "isEnrollmentPaused", + "features", + "proposedEnrollment", + "bucketConfig", + "startDate", + "endDate", + "branches", + "referenceBranch", + ) + + def get_bucketConfig(self, obj): + return { + "randomizationUnit": "normandy_id", + "namespace": "", + "start": 0, + "count": 0, + "total": 10000, + } + + def get_referenceBranch(self, obj): + control_branch = obj.variants.get(is_control=True) + return control_branch.slug + + def get_startDate(self, obj): + # placeholder value + return obj.start_date.isoformat() + + +class ExperimentRapidRecipeSerializer(serializers.ModelSerializer): + id = serializers.ReadOnlyField(source="normandy_slug") + arguments = ExperimentRapidArgumentSerializer(source="*") + filter_expression = serializers.ReadOnlyField(source="audience") + enabled = serializers.ReadOnlyField(default=True) + + class Meta: + model = Experiment + fields = ("id", "arguments", "filter_expression", "enabled") diff --git a/app/experimenter/experiments/api/v4/urls.py b/app/experimenter/experiments/api/v4/urls.py new file mode 100644 index 0000000000..b5ea92ff01 --- /dev/null +++ b/app/experimenter/experiments/api/v4/urls.py @@ -0,0 +1,23 @@ +from django.conf.urls import url +from experimenter.experiments.api.v4.views import ( + ExperimentListView, + ExperimentRapidDetailsView, + ExperimentRapidRecipeView, +) + + +urlpatterns = [ + url( + r"^experiments$", ExperimentListView.as_view(), name="experiments-rapid-api-list", + ), + url( + r"^experiments/(?P[\w-]+)$", + ExperimentRapidDetailsView.as_view(), + name="experiments-rapid-details-read", + ), + url( + r"^experiments/(?P[\w-]+)/recipe/$", + ExperimentRapidRecipeView.as_view(), + name="experiments-rapid-recipe", + ), +] diff --git a/app/experimenter/experiments/api/v4/views.py b/app/experimenter/experiments/api/v4/views.py new file mode 100644 index 0000000000..8967123762 --- /dev/null +++ b/app/experimenter/experiments/api/v4/views.py @@ -0,0 +1,35 @@ +from rest_framework.generics import ( + ListAPIView, + RetrieveAPIView, +) + +from experimenter.experiments.models import Experiment +from experimenter.experiments.api.v4.serializers import ( + ExperimentRapidSerializer, + ExperimentRapidRecipeSerializer, +) + + +class ExperimentListView(ListAPIView): + filter_fields = ("status",) + queryset = Experiment.objects.get_prefetched().filter(type=Experiment.TYPE_RAPID) + serializer_class = ExperimentRapidSerializer + + +class ExperimentRapidDetailsView(RetrieveAPIView): + lookup_field = "slug" + queryset = Experiment.objects.get_prefetched() + serializer_class = ExperimentRapidSerializer + + +class ExperimentRapidRecipeView(RetrieveAPIView): + lookup_field = "slug" + queryset = Experiment.objects.get_prefetched().filter( + status__in=( + Experiment.STATUS_SHIP, + Experiment.STATUS_ACCEPTED, + Experiment.STATUS_LIVE, + Experiment.STATUS_COMPLETE, + ) + ) + serializer_class = ExperimentRapidRecipeSerializer diff --git a/app/experimenter/experiments/tests/api/v1/experimentRecipe.json b/app/experimenter/experiments/tests/api/v1/experimentRecipe.json deleted file mode 100644 index adb52ef3b3..0000000000 --- a/app/experimenter/experiments/tests/api/v1/experimentRecipe.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/ExperimentRecipe", - "definitions": { - "ExperimentRecipe": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "A unique identifier for the Recipe" - }, - "filter_expression": { - "type": "string", - "description": "JEXL expression defined in an Audience" - }, - "targeting": { - "type": "string", - "description": "JEXL expression using messaging system environment" - }, - "enabled": { - "type": "boolean", - "description": "Is the experiment enabled?" - }, - "arguments": { - "$ref": "#/definitions/Experiment", - "description": "Experiment definition" - } - }, - "required": ["id", "filter_expression", "enabled", "arguments"], - "additionalProperties": false, - "description": "The experiment definition accessible to Firefox via Remote Settings.\nIt is compatible with ExperimentManager." - }, - "Experiment": { - "type": "object", - "properties": { - "slug": { - "type": "string", - "description": "Unique identifier for the experiment" - }, - "userFacingName": { - "type": "string", - "description": "Publically-accesible name of the experiment" - }, - "userFacingDescription": { - "type": "string", - "description": "Short public description of the experiment" - }, - "active": { - "type": "boolean", - "description": "Is the experiment currently live in production? i.e., published to remote settings?" - }, - "isEnrollmentPaused": { - "type": "boolean", - "description": "Are we continuing to enroll new users into the experiment?" - }, - "bucketConfig": { - "type": "object", - "properties": { - "randomizationUnit": { - "type": "string", - "enum": ["client_id", "normandy_id"], - "description": "The randomization unit. Note that client_id is not yet implemented.", - "default": "normandy_id" - }, - "namespace": { - "type": "string", - "description": "Additional inputs to the hashing function" - }, - "start": { - "type": "number", - "description": "Index of start of the range of buckets" - }, - "count": { - "type": "number", - "description": "Number of buckets to check" - }, - "total": { - "type": "number", - "description": "Total number of buckets", - "default": 10000 - } - }, - "required": [ - "randomizationUnit", - "namespace", - "start", - "count", - "total" - ], - "additionalProperties": false, - "description": "Bucketing configuration" - }, - "features": { - "type": "array", - "items": { "type": "string" }, - "description": "A list of features relevant to the experiment analysis" - }, - "branches": { - "type": "array", - "items": { - "type": "object", - "properties": { - "slug": { - "type": "string", - "description": "Identifier for the branch" - }, - "ratio": { - "type": "number", - "description": "Relative ratio of population for the branch (e.g. if branch A=1 and branch B=3,\nbranch A would get 25% of the population)", - "default": 1 - }, - "group": { - "type": "array", - "items": { "type": "string", "enum": ["cfr", "aboutwelcome"] }, - "description": "Used to indicate a type of branch value" - }, - "value": { - "anyOf": [{ "type": "object" }, { "type": "null" }], - "description": "The variant payload. TODO: This will be more strictly validated." - } - }, - "required": ["slug", "ratio", "value"], - "additionalProperties": false - }, - "description": "Branch configuration for the experiment" - }, - "startDate": { - "type": "string", - "description": "Actual publish date of the experiment", - "format": "date-time" - }, - "endDate": { - "type": ["string", "null"], - "description": "Actual end date of the experiment", - "format": "date-time" - }, - "proposedEnrollment": { - "type": "number", - "description": "Duration of enrollment from the start date in days" - }, - "referenceBranch": { - "type": ["string", "null"], - "description": "The slug of the reference branch" - } - }, - "required": [ - "slug", - "userFacingName", - "userFacingDescription", - "active", - "isEnrollmentPaused", - "bucketConfig", - "features", - "branches", - "startDate", - "endDate", - "proposedEnrollment", - "referenceBranch" - ], - "additionalProperties": false - } - } -} diff --git a/app/experimenter/experiments/tests/api/v1/test_serializers.py b/app/experimenter/experiments/tests/api/v1/test_serializers.py index c9d637f5f9..e6290caf57 100644 --- a/app/experimenter/experiments/tests/api/v1/test_serializers.py +++ b/app/experimenter/experiments/tests/api/v1/test_serializers.py @@ -1,7 +1,4 @@ import datetime -import json -import os -from jsonschema import validate from django.test import TestCase @@ -15,7 +12,6 @@ from experimenter.experiments.api.v1.serializers import ( ExperimentChangeLogSerializer, ExperimentCSVSerializer, - ExperimentRapidRecipeSerializer, ExperimentSerializer, ExperimentVariantSerializer, JSTimestampField, @@ -237,73 +233,3 @@ def test_serializer_outputs_expected_schema(self): "results_url": experiment.results_url, }, ) - - -class TestExperimentRapidSerializer(TestCase): - def test_serializer_outputs_expected_schema(self): - audience = Experiment.RAPID_AUDIENCE_CHOICES[0][1] - features = [feature[0] for feature in Experiment.RAPID_FEATURE_CHOICES] - normandy_slug = "experimenter-normandy-slug" - today = datetime.datetime.today() - experiment = ExperimentFactory.create( - type=Experiment.TYPE_RAPID, - rapid_type=Experiment.RAPID_AA_CFR, - audience=audience, - features=features, - normandy_slug=normandy_slug, - proposed_enrollment=9, - proposed_start_date=today, - ) - - ExperimentVariantFactory.create( - experiment=experiment, slug="control", is_control=True - ) - ExperimentVariantFactory.create(experiment=experiment, slug="variant-2") - - serializer = ExperimentRapidRecipeSerializer(experiment) - data = serializer.data - - fn = os.path.join(os.path.dirname(__file__), "experimentRecipe.json") - - with open(fn, "r") as f: - json_schema = json.load(f) - self.assertIsNone(validate(instance=data, schema=json_schema)) - - arguments = data.pop("arguments") - branches = arguments.pop("branches") - - self.assertDictEqual( - data, - {"id": normandy_slug, "filter_expression": "AUDIENCE 1", "enabled": True}, - ) - - self.assertDictEqual( - dict(arguments), - { - "userFacingName": experiment.name, - "userFacingDescription": experiment.public_description, - "slug": normandy_slug, - "active": True, - "isEnrollmentPaused": False, - "endDate": None, - "proposedEnrollment": experiment.proposed_enrollment, - "features": features, - "referenceBranch": "control", - "startDate": today.isoformat(), - "bucketConfig": { - "count": 0, - "namespace": "", - "randomizationUnit": "normandy_id", - "start": 0, - "total": 10000, - }, - }, - ) - converted_branches = [dict(branch) for branch in branches] - self.assertEqual( - converted_branches, - [ - {"ratio": 33, "slug": "variant-2", "value": None}, - {"ratio": 33, "slug": "control", "value": None}, - ], - ) diff --git a/app/experimenter/kinto/tasks.py b/app/experimenter/kinto/tasks.py index ad851490fe..490e2b7b7d 100644 --- a/app/experimenter/kinto/tasks.py +++ b/app/experimenter/kinto/tasks.py @@ -5,7 +5,7 @@ from django.conf import settings from experimenter.celery import app -from experimenter.experiments.api.v1.serializers import ExperimentRapidRecipeSerializer +from experimenter.experiments.api.v4.serializers import ExperimentRapidRecipeSerializer from experimenter.experiments.changelog_utils import update_experiment_with_change_log from experimenter.experiments.models import Experiment from experimenter.kinto import client diff --git a/app/experimenter/kinto/tests/test_tasks.py b/app/experimenter/kinto/tests/test_tasks.py index cce99d95c5..4b55aed05c 100644 --- a/app/experimenter/kinto/tests/test_tasks.py +++ b/app/experimenter/kinto/tests/test_tasks.py @@ -8,7 +8,7 @@ from experimenter.experiments.tests.factories import ExperimentFactory from experimenter.kinto.tests.mixins import MockKintoClientMixin from experimenter.kinto import tasks -from experimenter.experiments.api.v1.serializers import ExperimentRapidRecipeSerializer +from experimenter.experiments.api.v4.serializers import ExperimentRapidRecipeSerializer class TestPushExperimentToKintoTask(MockKintoClientMixin, TestCase): diff --git a/app/experimenter/urls.py b/app/experimenter/urls.py index 0197568494..efb33a6448 100644 --- a/app/experimenter/urls.py +++ b/app/experimenter/urls.py @@ -10,6 +10,7 @@ re_path(r"^api/v1/experiments/", include("experimenter.experiments.api.v1.urls")), re_path(r"^api/v2/experiments/", include("experimenter.experiments.api.v2.urls")), re_path(r"^api/v3/", include("experimenter.experiments.api.v3.urls")), + re_path(r"^api/v4/", include("experimenter.experiments.api.v4.urls")), re_path(r"^admin/", admin.site.urls), re_path(r"^experiments/", include("experimenter.experiments.web_urls")), re_path(r"^$", ExperimentListView.as_view(), name="home"),