Repository: AlexsLemonade/refinebio

Issue:
Make the token endpoint more programmer-friendly
### Context

I was writing up an example of how to create a dataset using the API

### Problem or idea

Just like it should only take one request to create a dataset if you have everything ready, I don't see a good reason why we need two requests to create an API token.

### Solution or next step

Refactor the token routes to be like the refactored dataset viewset.


Discussion:

Resolving patch:
diff --git a/api/data_refinery_api/urls.py b/api/data_refinery_api/urls.py
--- a/api/data_refinery_api/urls.py
+++ b/api/data_refinery_api/urls.py
@@ -20,7 +20,6 @@
     ComputationalResultListView,
     ComputedFileDetailView,
     ComputedFileListView,
-    CreateAPITokenView,
     DatasetView,
     DownloaderJobDetailView,
     DownloaderJobListView,
@@ -145,8 +144,12 @@ class AccessUser:
                     DatasetView.as_view({"get": "retrieve", "put": "update"}),
                     name="dataset",
                 ),
-                url(r"^token/$", CreateAPITokenView.as_view(), name="token"),
-                url(r"^token/(?P<id>[0-9a-f-]+)/$", APITokenView.as_view(), name="token_id"),
+                url(r"^token/$", APITokenView.as_view({"post": "create"}), name="token"),
+                url(
+                    r"^token/(?P<id>[0-9a-f-]+)/$",
+                    APITokenView.as_view({"get": "retrieve", "put": "update"}),
+                    name="token_id",
+                ),
                 # Jobs
                 url(r"^jobs/survey/$", SurveyJobListView.as_view(), name="survey_jobs"),
                 url(
diff --git a/api/data_refinery_api/views/__init__.py b/api/data_refinery_api/views/__init__.py
--- a/api/data_refinery_api/views/__init__.py
+++ b/api/data_refinery_api/views/__init__.py
@@ -1,4 +1,4 @@
-from data_refinery_api.views.api_token import APITokenView, CreateAPITokenView
+from data_refinery_api.views.api_token import APITokenView
 from data_refinery_api.views.compendium_result import (
     CompendiumResultDetailView,
     CompendiumResultListView,
diff --git a/api/data_refinery_api/views/api_token.py b/api/data_refinery_api/views/api_token.py
--- a/api/data_refinery_api/views/api_token.py
+++ b/api/data_refinery_api/views/api_token.py
@@ -3,7 +3,7 @@
 ##
 
 from django.utils.decorators import method_decorator
-from rest_framework import generics, serializers
+from rest_framework import mixins, serializers, viewsets
 
 from drf_yasg.utils import swagger_auto_schema
 
@@ -21,10 +21,10 @@ class Meta:
         }
 
 
-class CreateAPITokenView(generics.CreateAPIView):
-    """
-    token_create
-
+@method_decorator(
+    name="create",
+    decorator=swagger_auto_schema(
+        operation_description="""
     This endpoint can be used to create and activate tokens. These tokens can be used
     in requests that provide urls to download computed files. Setting `is_activated` to
     true indicates agreement with refine.bio's [Terms of Use](https://www.refine.bio/terms)
@@ -45,25 +45,32 @@ class CreateAPITokenView(generics.CreateAPIView):
     - [https://github.com/AlexsLemonade/refinebio/issues/731]()
     - [https://github.com/AlexsLemonade/refinebio-frontend/issues/560]()
     """
-
-    model = APIToken
-    serializer_class = APITokenSerializer
-
-
-@method_decorator(name="patch", decorator=swagger_auto_schema(auto_schema=None))
-class APITokenView(generics.RetrieveUpdateAPIView):
-    """
-    Read and modify Api Tokens.
-
-    get:
-    Return details about a specific token.
-
-    put:
+    ),
+)
+@method_decorator(
+    name="retrieve",
+    decorator=swagger_auto_schema(operation_description="Return details about a specific token.",),
+)
+@method_decorator(
+    name="update",
+    decorator=swagger_auto_schema(
+        operation_description="""
     This can be used to activate a specific token by sending `is_activated: true`.
     Setting `is_activated` to true indicates agreement with refine.bio's
     [Terms of Use](https://www.refine.bio/terms) and
     [Privacy Policy](https://www.refine.bio/privacy).
     """
+    ),
+)
+class APITokenView(
+    mixins.CreateModelMixin,
+    mixins.UpdateModelMixin,
+    mixins.RetrieveModelMixin,
+    viewsets.GenericViewSet,
+):
+    """
+    Create, read, and modify Api Tokens.
+    """
 
     model = APIToken
     lookup_field = "id"


Test patch:
diff --git a/api/data_refinery_api/tests.py b/api/data_refinery_api/tests.py
--- a/api/data_refinery_api/tests.py
+++ b/api/data_refinery_api/tests.py
@@ -459,16 +459,11 @@ def test_compendia(self):
 
         # Create a token first
         response = self.client.post(
-            reverse("token", kwargs={"version": API_VERSION}), content_type="application/json"
-        )
-        token = response.json()
-        token["is_activated"] = True
-        token_id = token["id"]
-        response = self.client.put(
-            reverse("token_id", kwargs={"id": token_id, "version": API_VERSION}),
-            json.dumps(token),
+            reverse("token", kwargs={"version": API_VERSION}),
+            json.dumps({"is_activated": True}),
             content_type="application/json",
         )
+        token_id = response.json()["id"]
 
         response = self.client.get(
             reverse("computed_files", kwargs={"version": API_VERSION}),
@@ -482,27 +477,16 @@ def test_compendia(self):
     @patch("data_refinery_api.views.dataset.send_job", lambda *args: True)
     def test_create_dataset_fails_without_email(self):
 
-        # Get a token first
+        # Create a token first
         response = self.client.post(
-            reverse("token", kwargs={"version": API_VERSION}), content_type="application/json"
-        )
-        token = response.json()
-        token["is_activated"] = True
-        token_id = token["id"]
-        response = self.client.put(
-            reverse("token_id", kwargs={"id": token_id, "version": API_VERSION}),
-            json.dumps(token),
+            reverse("token", kwargs={"version": API_VERSION}),
+            json.dumps({"is_activated": True}),
             content_type="application/json",
         )
-
-        activated_token = response.json()
-        self.assertEqual(activated_token["id"], token_id)
-        self.assertEqual(activated_token["is_activated"], True)
+        token_id = response.json()["id"]
 
         # Good, except for missing email.
-        jdata = json.dumps(
-            {"start": True, "data": {"GSE123": ["789"]}, "token_id": activated_token["id"]}
-        )
+        jdata = json.dumps({"start": True, "data": {"GSE123": ["789"]}, "token_id": token_id})
         response = self.client.post(
             reverse("create_dataset", kwargs={"version": API_VERSION}),
             jdata,
@@ -521,9 +505,7 @@ def test_create_dataset_fails_without_email(self):
 
         self.assertEqual(response.status_code, 201)
 
-        jdata = json.dumps(
-            {"start": True, "data": {"GSE123": ["789"]}, "token_id": activated_token["id"]}
-        )
+        jdata = json.dumps({"start": True, "data": {"GSE123": ["789"]}, "token_id": token_id})
         response = self.client.put(
             reverse("dataset", kwargs={"id": response.json()["id"], "version": API_VERSION}),
             jdata,
@@ -545,22 +527,13 @@ def test_create_dataset_fails_without_email(self):
     @patch("data_refinery_api.views.dataset.send_job", lambda *args: True)
     def test_starting_dataset_fails_without_experiments(self):
 
-        # Get a token first
+        # Create a token first
         response = self.client.post(
-            reverse("token", kwargs={"version": API_VERSION}), content_type="application/json"
-        )
-        token = response.json()
-        token["is_activated"] = True
-        token_id = token["id"]
-        response = self.client.put(
-            reverse("token_id", kwargs={"id": token_id, "version": API_VERSION}),
-            json.dumps(token),
+            reverse("token", kwargs={"version": API_VERSION}),
+            json.dumps({"is_activated": True}),
             content_type="application/json",
         )
-
-        activated_token = response.json()
-        self.assertEqual(activated_token["id"], token_id)
-        self.assertEqual(activated_token["is_activated"], True)
+        token_id = response.json()["id"]
 
         # Good, except for having zero experiments
         data = {"email_address": "baz@gmail.com", "data": {}}
@@ -617,23 +590,6 @@ def test_dataset_adding_non_downloadable_samples_fails(self):
         experiment_sample_association.experiment = self.experiment
         experiment_sample_association.save()
 
-        # Get a token first
-        response = self.client.post(
-            reverse("token", kwargs={"version": API_VERSION}), content_type="application/json"
-        )
-        token = response.json()
-        token["is_activated"] = True
-        token_id = token["id"]
-        response = self.client.put(
-            reverse("token_id", kwargs={"id": token_id, "version": API_VERSION}),
-            json.dumps(token),
-            content_type="application/json",
-        )
-
-        activated_token = response.json()
-        self.assertEqual(activated_token["id"], token_id)
-        self.assertEqual(activated_token["is_activated"], True)
-
         # Bad, 456 is not processed
         jdata = json.dumps({"email_address": "baz@gmail.com", "data": {"GSE123": ["456"]}})
         response = self.client.post(
@@ -745,22 +701,14 @@ def test_dataset_adding_non_downloadable_samples_fails(self):
     @patch("data_refinery_api.views.dataset.send_job", lambda *args: True)
     def test_create_update_dataset(self):
 
-        # Get a token first
+        # Create a token first
         response = self.client.post(
-            reverse("token", kwargs={"version": API_VERSION}), content_type="application/json"
-        )
-        token = response.json()
-        token["is_activated"] = True
-        token_id = token["id"]
-        response = self.client.put(
-            reverse("token_id", kwargs={"id": token_id, "version": API_VERSION}),
-            json.dumps(token),
+            reverse("token", kwargs={"version": API_VERSION}),
+            json.dumps({"is_activated": True}),
             content_type="application/json",
         )
+        token_id = response.json()["id"]
 
-        activated_token = response.json()
-        self.assertEqual(activated_token["id"], token_id)
-        self.assertEqual(activated_token["is_activated"], True)
         # Good
         jdata = json.dumps({"email_address": "baz@gmail.com", "data": {"GSE123": ["789"]}})
         response = self.client.post(
@@ -1203,6 +1151,35 @@ def test_qn_endpoints(self):
         # there's another qn endpoint that is created in the setup method of this test case
         self.assertEqual(len(response.json()), 3)
 
+    def test_create_token(self):
+        # First, try activating right away
+        response = self.client.post(
+            reverse("token", kwargs={"version": API_VERSION}),
+            json.dumps({"is_activated": True}),
+            content_type="application/json",
+        )
+        self.assertEqual(response.status_code, 201)
+        token_id = response.json()["id"]
+
+        # Now activate using a second request
+        response = self.client.post(
+            reverse("token", kwargs={"version": API_VERSION}), content_type="application/json"
+        )
+        self.assertEqual(response.status_code, 201)
+        token = response.json()
+        token["is_activated"] = True
+        token_id = token["id"]
+        response = self.client.put(
+            reverse("token_id", kwargs={"id": token_id, "version": API_VERSION}),
+            json.dumps(token),
+            content_type="application/json",
+        )
+        self.assertEqual(response.status_code, 200)
+
+        activated_token = response.json()
+        self.assertEqual(activated_token["id"], token_id)
+        self.assertEqual(activated_token["is_activated"], True)
+
 
 MOCK_NOMAD_RESPONSE = [
     {