From 7a5623042535f36896e24bb5e917df10a9715d6a Mon Sep 17 00:00:00 2001 From: Tif Tran Date: Fri, 17 Jul 2020 14:51:35 -0700 Subject: [PATCH] v4 public api fixes #3010 (#3048) * v4 public api fixes #3010 * docs * change to expose recipe only * docs * formatting * v4 tests * renaming * unused imports Co-authored-by: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> --- .../base/management/commands/generate_docs.py | 4 + app/experimenter/docs/openapi-schema.json | 249 ++++++++++++++++++ app/experimenter/docs/swagger-ui.html | 249 ++++++++++++++++++ .../experiments/api/v1/serializers.py | 71 ----- .../experiments/api/v4/__init__.py | 0 .../experiments/api/v4/serializers.py | 79 ++++++ app/experimenter/experiments/api/v4/urls.py | 7 + app/experimenter/experiments/api/v4/views.py | 12 + .../tests/api/v1/test_serializers.py | 74 ------ .../experiments/tests/api/v4/__init__.py | 0 .../api/{v1 => v4}/experimentRecipe.json | 0 .../tests/api/v4/test_serializers.py | 83 ++++++ .../experiments/tests/api/v4/test_views.py | 55 ++++ app/experimenter/kinto/tasks.py | 2 +- app/experimenter/kinto/tests/test_tasks.py | 2 +- app/experimenter/urls.py | 1 + 16 files changed, 741 insertions(+), 147 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 create mode 100644 app/experimenter/experiments/tests/api/v4/__init__.py rename app/experimenter/experiments/tests/api/{v1 => v4}/experimentRecipe.json (100%) create mode 100644 app/experimenter/experiments/tests/api/v4/test_serializers.py create mode 100644 app/experimenter/experiments/tests/api/v4/test_views.py 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 587c365e3c..b4d5ea62a1 100644 --- a/app/experimenter/docs/openapi-schema.json +++ b/app/experimenter/docs/openapi-schema.json @@ -8520,6 +8520,255 @@ ] } }, + "/api/v4/experiments/": { + "get": { + "operationId": "listExperiments", + "description": "", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "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/v4/experiments/{slug}/": { + "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 ffad55e210..9421f3f7a2 100644 --- a/app/experimenter/docs/swagger-ui.html +++ b/app/experimenter/docs/swagger-ui.html @@ -8532,6 +8532,255 @@ ] } }, + "/api/v4/experiments/": { + "get": { + "operationId": "listExperiments", + "description": "", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "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/v4/experiments/{slug}/": { + "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 5176aecc59..c883d3a797 100644 --- a/app/experimenter/experiments/api/v1/serializers.py +++ b/app/experimenter/experiments/api/v1/serializers.py @@ -146,74 +146,3 @@ class Meta: def get_results(self, obj): return ResultsSerializer(obj).data - - -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..377f4bb4d8 --- /dev/null +++ b/app/experimenter/experiments/api/v4/serializers.py @@ -0,0 +1,79 @@ +from rest_framework import serializers + +from experimenter.experiments.models import ( + Experiment, + ExperimentVariant, +) + + +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): + if obj.variants.count(): + control_branch = obj.variants.get(is_control=True) + return control_branch.slug + + def get_startDate(self, obj): + # placeholder value + if obj.start_date: + 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..12432b4cf3 --- /dev/null +++ b/app/experimenter/experiments/api/v4/urls.py @@ -0,0 +1,7 @@ +from rest_framework.routers import SimpleRouter +from experimenter.experiments.api.v4.views import ExperimentRapidViewSet + + +router = SimpleRouter() +router.register(r"experiments", ExperimentRapidViewSet, "experiment-rapid-recipe") +urlpatterns = router.urls diff --git a/app/experimenter/experiments/api/v4/views.py b/app/experimenter/experiments/api/v4/views.py new file mode 100644 index 0000000000..1e4c29d7bd --- /dev/null +++ b/app/experimenter/experiments/api/v4/views.py @@ -0,0 +1,12 @@ +from rest_framework import viewsets, mixins + +from experimenter.experiments.models import Experiment +from experimenter.experiments.api.v4.serializers import ExperimentRapidRecipeSerializer + + +class ExperimentRapidViewSet( + mixins.RetrieveModelMixin, mixins.ListModelMixin, viewsets.GenericViewSet, +): + lookup_field = "slug" + queryset = Experiment.objects.get_prefetched().filter(type=Experiment.TYPE_RAPID) + serializer_class = ExperimentRapidRecipeSerializer diff --git a/app/experimenter/experiments/tests/api/v1/test_serializers.py b/app/experimenter/experiments/tests/api/v1/test_serializers.py index 7389726724..b095c46b73 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 @@ -14,7 +11,6 @@ ) from experimenter.experiments.api.v1.serializers import ( ExperimentChangeLogSerializer, - ExperimentRapidRecipeSerializer, ExperimentSerializer, ExperimentVariantSerializer, JSTimestampField, @@ -188,73 +184,3 @@ def test_serializer_outputs_expected_schema(self): ) serializer = ExperimentChangeLogSerializer(change_log) self.assertEqual(serializer.data["changed_on"], change_log.changed_on) - - -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/experiments/tests/api/v4/__init__.py b/app/experimenter/experiments/tests/api/v4/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/experimenter/experiments/tests/api/v1/experimentRecipe.json b/app/experimenter/experiments/tests/api/v4/experimentRecipe.json similarity index 100% rename from app/experimenter/experiments/tests/api/v1/experimentRecipe.json rename to app/experimenter/experiments/tests/api/v4/experimentRecipe.json diff --git a/app/experimenter/experiments/tests/api/v4/test_serializers.py b/app/experimenter/experiments/tests/api/v4/test_serializers.py new file mode 100644 index 0000000000..c44a234bc7 --- /dev/null +++ b/app/experimenter/experiments/tests/api/v4/test_serializers.py @@ -0,0 +1,83 @@ +import datetime +import json +import os +from jsonschema import validate + +from django.test import TestCase + +from experimenter.experiments.models import Experiment +from experimenter.experiments.tests.factories import ( + ExperimentFactory, + ExperimentVariantFactory, +) +from experimenter.experiments.api.v4.serializers import ExperimentRapidRecipeSerializer + + +class TestExperimentRapidRecipeSerializer(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/experiments/tests/api/v4/test_views.py b/app/experimenter/experiments/tests/api/v4/test_views.py new file mode 100644 index 0000000000..cb0b6659fa --- /dev/null +++ b/app/experimenter/experiments/tests/api/v4/test_views.py @@ -0,0 +1,55 @@ +import json + +from django.conf import settings +from django.test import TestCase +from django.urls import reverse + +from experimenter.experiments.models import Experiment +from experimenter.experiments.constants import ExperimentConstants +from experimenter.experiments.api.v4.serializers import ExperimentRapidRecipeSerializer +from experimenter.experiments.tests.factories import ExperimentFactory + + +class TestExperimentListView(TestCase): + def test_list_view_serializes_experiments(self): + experiments = [] + user_email = "user@example.com" + + for i in range(3): + experiment = ExperimentFactory.create_with_variants( + type=ExperimentConstants.TYPE_RAPID, + objectives="gotta go fast", + audience="AUDIENCE 1", + features=["FEATURE 1"], + ) + experiments.append(experiment) + + response = self.client.get( + reverse("experiment-rapid-recipe-list"), + **{settings.OPENIDC_EMAIL_HEADER: user_email}, + ) + self.assertEqual(response.status_code, 200) + + json_data = json.loads(response.content) + + serialized_experiments = ExperimentRapidRecipeSerializer( + Experiment.objects.get_prefetched(), many=True + ).data + + self.assertEqual(serialized_experiments, json_data) + + +class TestExperimentRapidRecipeView(TestCase): + def test_get_rapid_experiment_recipe_returns_recipe_info_for_experiment(self): + user_email = "user@example.com" + experiment = ExperimentFactory.create(type=ExperimentConstants.TYPE_RAPID) + + response = self.client.get( + reverse("experiment-rapid-recipe-detail", kwargs={"slug": experiment.slug}), + **{settings.OPENIDC_EMAIL_HEADER: user_email}, + ) + + self.assertEqual(response.status_code, 200) + json_data = json.loads(response.content) + serialized_experiment = ExperimentRapidRecipeSerializer(experiment).data + self.assertEqual(serialized_experiment, json_data) 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 a71fc2d09c..39bf5d1d1d 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.urls")), re_path(r"^$", ExperimentListView.as_view(), name="home"),