Repository: AlexsLemonade/refinebio

Issue:
Make the response for non-downloadable samples more programmer-friendly
### Context

Right now the message is a string with an error message and accession codes inside of it.

### Problem or idea

We don't want users to have to parse the error message to get the accessions they need to remove from their dataset.

### Solution or next step

Can we make the serializer error return a dict with the error message and contents instead of a string?



Discussion:

Resolving patch:
diff --git a/api/data_refinery_api/views/dataset.py b/api/data_refinery_api/views/dataset.py
--- a/api/data_refinery_api/views/dataset.py
+++ b/api/data_refinery_api/views/dataset.py
@@ -32,6 +32,32 @@ def get_client_ip(request):
     return ip
 
 
+def experiment_has_downloadable_samples(experiment, quant_sf_only=False):
+    if quant_sf_only:
+        try:
+            experiment = Experiment.public_objects.get(accession_code=experiment)
+        except Exception as e:
+            return False
+
+        samples = experiment.sample_set.filter(
+            # We only want samples with a quant.sf file associated with them
+            results__computedfile__filename="quant.sf",
+            results__computedfile__s3_key__isnull=False,
+            results__computedfile__s3_bucket__isnull=False,
+        )
+
+        if samples.count() == 0:
+            return False
+
+    else:
+        try:
+            experiment = Experiment.processed_public_objects.get(accession_code=experiment)
+        except Exception as e:
+            return False
+
+    return True
+
+
 def validate_dataset(data):
     """
     Dataset validation. Each experiment should always have at least one
@@ -45,6 +71,7 @@ def validate_dataset(data):
         raise serializers.ValidationError("`data` must contain at least one experiment..")
 
     accessions = []
+    non_downloadable_experiments = []
     for key, value in data["data"].items():
         if type(value) != list:
             raise serializers.ValidationError(
@@ -66,36 +93,23 @@ def validate_dataset(data):
 
         # If they want "ALL", just make sure that the experiment has at least one downloadable sample
         if value == ["ALL"]:
-            if data.get("quant_sf_only", False):
-                try:
-                    experiment = Experiment.public_objects.get(accession_code=key)
-                except Exception as e:
-                    raise serializers.ValidationError("Experiment " + key + " does not exist")
-
-                samples = experiment.sample_set.filter(
-                    # We only want samples with a quant.sf file associated with them
-                    results__computedfile__filename="quant.sf",
-                    results__computedfile__s3_key__isnull=False,
-                    results__computedfile__s3_bucket__isnull=False,
-                )
-                if samples.count() == 0:
-                    raise serializers.ValidationError(
-                        "Experiment "
-                        + key
-                        + " does not have at least one sample with a quant.sf file"
-                    )
+            if not experiment_has_downloadable_samples(
+                key, quant_sf_only=data.get("quant_sf_only", False)
+            ):
+                non_downloadable_experiments.append(key)
 
-            else:
-                try:
-                    experiment = Experiment.processed_public_objects.get(accession_code=key)
-                except Exception as e:
-                    raise serializers.ValidationError(
-                        "Experiment " + key + " does not have at least one downloadable sample"
-                    )
         # Otherwise, we will check that all the samples they requested are downloadable
         else:
             accessions.extend(value)
 
+    if len(non_downloadable_experiments) != 0:
+        raise serializers.ValidationError(
+            {
+                "message": "Experiment(s) in dataset have zero downloadable samples. See `non_downloadable_experiments` for a full list",
+                "non_downloadable_experiments": non_downloadable_experiments,
+            }
+        )
+
     if len(accessions) == 0:
         return
 
@@ -110,9 +124,12 @@ def validate_dataset(data):
         )
         if samples_without_quant_sf.count() > 0:
             raise serializers.ValidationError(
-                "Sample(s) '"
-                + ", ".join([s.accession_code for s in samples_without_quant_sf])
-                + "' in dataset are missing quant.sf files"
+                {
+                    "message": "Sample(s) in dataset are missing quant.sf files. See `non_downloadable_samples` for a full list",
+                    "non_downloadable_samples": [
+                        s.accession_code for s in samples_without_quant_sf
+                    ],
+                },
             )
 
     else:
@@ -121,9 +138,10 @@ def validate_dataset(data):
         )
         if unprocessed_samples.count() > 0:
             raise serializers.ValidationError(
-                "Non-downloadable sample(s) '"
-                + ", ".join([s.accession_code for s in unprocessed_samples])
-                + "' in dataset"
+                {
+                    "message": "Non-downloadable sample(s) in dataset. See `non_downloadable_samples` for a full list",
+                    "non_downloadable_samples": [s.accession_code for s in unprocessed_samples],
+                }
             )
 
 


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
@@ -631,9 +631,10 @@ def test_dataset_adding_non_downloadable_samples_fails(self):
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json()["non_field_errors"][0], "Non-downloadable sample(s) '456' in dataset"
+        self.assertIn(
+            "Non-downloadable sample(s) in dataset", response.json()["message"][0],
         )
+        self.assertEqual(response.json()["non_downloadable_samples"], ["456"])
 
         # Good, 789 is processed
         jdata = json.dumps({"email_address": "baz@gmail.com", "data": {"GSE123": ["789"]}})
@@ -660,10 +661,10 @@ def test_dataset_adding_non_downloadable_samples_fails(self):
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json()["non_field_errors"][0],
-            "Sample(s) '456' in dataset are missing quant.sf files",
+        self.assertIn(
+            "Sample(s) in dataset are missing quant.sf files", response.json()["message"][0],
         )
+        self.assertEqual(response.json()["non_downloadable_samples"], ["456"])
 
         # Bad, none of the samples in GSE123 have a quant.sf file
         response = self.client.post(
@@ -678,10 +679,11 @@ def test_dataset_adding_non_downloadable_samples_fails(self):
             content_type="application/json",
         )
         self.assertEqual(response.status_code, 400)
-        self.assertEqual(
-            response.json()["non_field_errors"][0],
-            "Experiment GSE123 does not have at least one sample with a quant.sf file",
+        self.assertIn(
+            "Experiment(s) in dataset have zero downloadable samples",
+            response.json()["message"][0],
         )
+        self.assertEqual(response.json()["non_downloadable_experiments"], ["GSE123"])
 
         # Make 456 have a quant.sf file
         result = ComputationalResult()