Repository: AlexsLemonade/refinebio

Issue:
Filters change based on filtered results isn't correct
### Context

https://github.com/AlexsLemonade/refinebio-frontend/issues/185

### Problem or idea

The backend seems to return filters using both the search term and the applied filters to determine what filters can be applied. The expected behavior on the frontend would be to ignore the applied filters and just return the filters calculated from the search term itself. This allows someone to filter for multiple mutually exclusive things, like for samples from either Gallus gallus or Homo sapiens.

### Solution or next step

Alter the code that determines appropriate filters to disregard already applied filters. We also have to nail down the exact behavior we want from the filters, i.e. any sample that matches any of the filters should be returned, any sample that matches a filter from every category should be returned, etc.

### New Issue Checklist

- [x] The title is short and descriptive.
- [x] You have explained the context that led you to write this issue.
- [x] You have reported a problem or idea.
- [x] You have proposed a solution or next step.



Discussion:
- Match samples with OR logic for filters within the same category
- Match samples with AND logic for filters between categories

For example: 
If I chose filters Homo sapiens, Gallus gallus(Organism), and RNA-seq(technology), I would expect samples which are RNA-seq and either Homo sapiens or Gallus gallus samples
@dvenprasad How should we prioritize this? Should we include that in this sprint?
Yes, include it in the current sprint with high proirity since it's blocking https://github.com/AlexsLemonade/refinebio-frontend/issues/185

Resolving patch:
diff --git a/api/data_refinery_api/views.py b/api/data_refinery_api/views.py
--- a/api/data_refinery_api/views.py
+++ b/api/data_refinery_api/views.py
@@ -6,6 +6,7 @@
 from django.shortcuts import get_object_or_404
 
 from django_filters.rest_framework import DjangoFilterBackend
+import django_filters
 
 from rest_framework.exceptions import APIException
 from rest_framework.pagination import LimitOffsetPagination
@@ -93,6 +94,38 @@ def get_paginated_response(self, data):
 # Search and Filter
 ##
 
+class ExperimentFilter(django_filters.FilterSet):
+    queryset = Experiment.processed_public_objects.all()
+    has_publication = django_filters.BooleanFilter(field_name="has_publication")
+    submitter_institution = \
+        django_filters.ModelMultipleChoiceFilter(field_name="submitter_institution",
+                                                 to_field_name="submitter_institution",
+                                                 queryset=queryset)
+    submitter_institution.always_filter = False
+    technology = django_filters.ModelMultipleChoiceFilter(field_name="technology",
+                                                          to_field_name="technology",
+                                                          queryset=queryset)
+    technology.always_filter = False
+    source_first_published = django_filters.DateTimeFilter(field_name="source_first_published")
+    organisms__name = django_filters.ModelMultipleChoiceFilter(field_name="organisms__name",
+                                                               to_field_name="name",
+                                                               queryset=Organism.objects.all())
+    organisms__name.always_filter = False
+    samples__platform_accession_code = \
+        django_filters.ModelMultipleChoiceFilter(field_name="smaples__platform_accession_code",
+                                                 to_field_name="platform_accession_code",
+                                                 queryset=Sample.objects.all())
+    samples__platform_accession_code.always_filter = False
+
+    class Meta:
+        model = Experiment
+        fields = ['has_publication', 
+                        'submitter_institution', 
+                        'technology',
+                        'source_first_published', 
+                        'organisms__name',
+                        'samples__platform_accession_code']
+
 # ListAPIView is read-only!
 class SearchAndFilter(generics.ListAPIView):
     """
@@ -108,6 +141,7 @@ class SearchAndFilter(generics.ListAPIView):
     pagination_class = LimitOffsetPagination
 
     filter_backends = (DjangoFilterBackend, filters.SearchFilter,)
+    filter_class = ExperimentFilter
 
     # via http://www.django-rest-framework.org/api-guide/filtering/#searchfilter
     # '^' Starts-with search.
@@ -125,13 +159,6 @@ class SearchAndFilter(generics.ListAPIView):
                         '@submitter_institution',
                         'experimentannotation__data'
                     )
-    filter_fields = (   'has_publication', 
-                        'submitter_institution', 
-                        'technology',
-                        'source_first_published', 
-                        'organisms__name',
-                        'samples__platform_accession_code'
-                    )
 
     def list(self, request, *args, **kwargs):
         """ Adds counts on certain filter fields to result JSON."""
@@ -142,7 +169,7 @@ def list(self, request, *args, **kwargs):
         response.data['filters']['publication'] = {}
         response.data['filters']['organism'] = {}
 
-        qs = self.filter_queryset(self.get_queryset())
+        qs = self.search_queryset(self.get_queryset())
         techs = qs.values('technology').annotate(Count('technology', unique=True))
         for tech in techs:
             if not tech['technology'] or not tech['technology'].strip():
@@ -166,6 +193,12 @@ def list(self, request, *args, **kwargs):
 
         return response
 
+    # We want to determine filters based off of the search term but not the filters to allow for
+    # multiple filters of the same type.
+    def search_queryset(self, queryset):
+        """ Filters the queryset based off of the search term (but not the filters) """
+        return filters.SearchFilter().filter_queryset(self.request, queryset, view=self)
+
 ##
 # Dataset
 ##


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
@@ -348,9 +348,9 @@ def test_search_and_filter(self):
         self.assertEqual(len(response.json()['results'][0]['platforms']), 2)
         self.assertEqual(sorted(response.json()['results'][0]['platforms']), sorted(ex.platforms))
         self.assertEqual(sorted(response.json()['results'][0]['platforms']), sorted(['AFFY', 'ILLUMINA']))
-        self.assertEqual(response.json()['filters']['technology'], {'MICROARRAY': 2})
+        self.assertEqual(response.json()['filters']['technology'], {'FAKE-TECH': 1, 'MICROARRAY': 2, 'RNA-SEQ': 1})
         self.assertEqual(response.json()['filters']['publication'], {})
-        self.assertEqual(response.json()['filters']['organism'], {'HOMO_SAPIENS': 2})
+        self.assertEqual(response.json()['filters']['organism'], {'Extra-Terrestrial-1982': 1, 'HOMO_SAPIENS': 3})
 
         response = self.client.get(reverse('search'),
                                    {'search': 'THISWILLBEINASEARCHRESULT',
@@ -361,6 +361,11 @@ def test_search_and_filter(self):
         self.assertEqual(response.json()['count'], 1)
         self.assertEqual(response.json()['results'][0]['accession_code'], "FINDME_TEMPURA")
 
+        # Test multiple filters
+        # This has to be done manually due to dicts requring distinct keys
+        response = self.client.get(reverse('search') + "?search=THISWILLBEINASEARCHRESULT&technology=MICROARRAY&technology=FAKE-TECH")
+        self.assertEqual(response.json()['count'], 2)
+
     @patch('data_refinery_common.message_queue.send_job')
     def test_create_update_dataset(self, mock_send_job):