Repository: mathesar-foundation/mathesar

Issue:
More filters for table list API
**Problem**
<!-- Please provide a clear and concise description of the problem that this feature request is designed to solve.-->
The table list API should allow filtering. For example, we might want to get the list of all tables in a schema to see if the table the user is trying to create already exists in that schema.

**Proposed solution**
<!-- A clear and concise description of your proposed solution or feature. -->
The table list endpoint should support filtering by:
- schema
- before/after: created, last updated
- whether the import was verified

**Additional context**
<!-- Add any other context or screenshots about the feature request here.-->
We should use `django-filter` since it integrates with DRF and makes setting up filters easy.


Discussion:

Resolving patch:
diff --git a/mathesar/filters.py b/mathesar/filters.py
--- a/mathesar/filters.py
+++ b/mathesar/filters.py
@@ -1,5 +1,6 @@
 from django_property_filter import (
     PropertyFilterSet, PropertyBaseInFilter, PropertyCharFilter,
+    PropertyDateTimeFromToRangeFilter, PropertyBooleanFilter
 )
 
 from mathesar.models import Schema, Table
@@ -20,7 +21,13 @@ class Meta:
 
 class TableFilter(PropertyFilterSet):
     name = CharInFilter(field_name='name', lookup_expr='in')
+    schema = CharInFilter(field_name='schema__name', lookup_expr='in')
+    created = PropertyDateTimeFromToRangeFilter(field_name='created_at')
+    updated = PropertyDateTimeFromToRangeFilter(field_name='updated_at')
+    import_verified = PropertyBooleanFilter(field_name='import_verified')
+    not_imported = PropertyBooleanFilter(lookup_expr="isnull",
+                                         field_name='import_verified')
 
     class Meta:
         model = Table
-        fields = ['name']
+        fields = ['name', 'schema', 'created_at', 'updated_at', 'import_verified']
diff --git a/mathesar/models.py b/mathesar/models.py
--- a/mathesar/models.py
+++ b/mathesar/models.py
@@ -28,13 +28,21 @@ def __str__(self):
         return f"{self.__class__.__name__}: {self.oid}"
 
 
+# TODO: Replace with a proper form of caching
+# See: https://github.com/centerofci/mathesar/issues/280
+_engine = None
+
+
 class Schema(DatabaseObject):
     database = models.CharField(max_length=128)
 
-    @cached_property
+    @property
     def _sa_engine(self):
+        global _engine
         # We're caching this since the engine is used frequently.
-        return create_mathesar_engine(self.database)
+        if _engine is None:
+            _engine = create_mathesar_engine(self.database)
+        return _engine
 
     @cached_property
     def name(self):


Test patch:
diff --git a/mathesar/tests/views/api/conftest.py b/mathesar/tests/views/api/conftest.py
--- a/mathesar/tests/views/api/conftest.py
+++ b/mathesar/tests/views/api/conftest.py
@@ -21,11 +21,11 @@ def client():
 
 @pytest.fixture
 def create_table(engine, csv_filename, test_db_name):
-    def _create_table(table_name):
+    def _create_table(table_name, schema='Patents'):
         with open(csv_filename, 'rb') as csv_file:
             table = legacy_create_table_from_csv(
                 name=table_name,
-                schema='Patents',
+                schema=schema,
                 database_key=test_db_name,
                 csv_file=csv_file
             )
diff --git a/mathesar/tests/views/api/test_table_api.py b/mathesar/tests/views/api/test_table_api.py
--- a/mathesar/tests/views/api/test_table_api.py
+++ b/mathesar/tests/views/api/test_table_api.py
@@ -6,7 +6,7 @@
 from sqlalchemy import text
 
 from mathesar.models import Table
-from mathesar.models import DataFile
+from mathesar.models import DataFile, Schema
 from mathesar.utils.schemas import create_schema_and_object
 from mathesar.views import api
 
@@ -38,6 +38,15 @@ def check_table_response(response_table, table, table_name):
     assert response_table['records'].endswith('/records/')
 
 
+def check_table_filter_response(response, status_code=None, count=None):
+    response_data = response.json()
+    if status_code is not None:
+        assert response.status_code == status_code
+    if count is not None:
+        assert response_data['count'] == count
+        assert len(response_data['results']) == count
+
+
 def test_table_list(create_table, client):
     """
     Desired format:
@@ -82,21 +91,18 @@ def test_table_list(create_table, client):
     check_table_response(response_table, table, table_name)
 
 
-def test_table_list_filter(create_table, client):
+def test_table_list_filter_name(create_table, client):
     tables = {
-        'Nasa Table List Filter': create_table('Nasa Table List Filter'),
-        'Filler Table 1': create_table('Filler Table 1'),
-        'Filler Table 2': create_table('Filler Table 2')
+        'Filter Name 1': create_table('Filter Name 1'),
+        'Filter Name 2': create_table('Filter Name 2'),
+        'Filter Name 3': create_table('Filter Name 3')
     }
 
-    filter_tables = ['Nasa Table List Filter', 'Filler Table 1']
+    filter_tables = ['Filter Name 1', 'Filter Name 2']
     query_str = ','.join(filter_tables)
-
     response = client.get(f'/api/v0/tables/?name={query_str}')
     response_data = response.json()
-    assert response.status_code == 200
-    assert response_data['count'] == 2
-    assert len(response_data['results']) == 2
+    check_table_filter_response(response, status_code=200, count=2)
 
     response_tables = {res['name']: res for res in response_data['results']}
     for table_name in filter_tables:
@@ -106,6 +112,87 @@ def test_table_list_filter(create_table, client):
         check_table_response(response_table, table, table_name)
 
 
+def test_table_list_filter_schema(create_table, client):
+    tables = {
+        'Schema 1': create_table('Filter Schema 1', schema='Schema 1'),
+        'Schema 2': create_table('Filter Schema 2', schema='Schema 2'),
+        'Schema 3': create_table('Filter Schema 3', schema='Schema 3')
+    }
+
+    filter_tables = ['Schema 2', 'Schema 3']
+    query_str = ','.join(filter_tables)
+    response = client.get(f'/api/v0/tables/?schema={query_str}')
+    response_data = response.json()
+    check_table_filter_response(response, status_code=200, count=2)
+
+    response_tables = {Schema.objects.get(id=res['schema']).name: res
+                       for res in response_data['results']}
+    for schema_name in filter_tables:
+        assert schema_name in response_tables
+        table = tables[schema_name]
+        response_table = response_tables[schema_name]
+        check_table_response(response_table, table, table.name)
+
+
+@pytest.mark.parametrize('timestamp_type', ['created', 'updated'])
+def test_table_list_filter_timestamps(create_table, client, timestamp_type):
+    table_name = f'Fitler {timestamp_type}'
+    table = create_table(table_name)
+    query_str = '2020-01-01 8:00'
+
+    response = client.get(f'/api/v0/tables/?{timestamp_type}_before={query_str}')
+    response_data = response.json()
+    check_table_filter_response(response, status_code=200, count=0)
+
+    response = client.get(f'/api/v0/tables/?{timestamp_type}_after={query_str}')
+    response_data = response.json()
+    check_table_filter_response(response, status_code=200, count=1)
+    check_table_response(response_data['results'][0], table, table_name)
+
+    timestamp = table.created_at if timestamp_type == 'created' else table.updated_at
+    response = client.get(f'/api/v0/tables/?{timestamp_type}={timestamp}')
+    response_data = response.json()
+    check_table_filter_response(response, status_code=200, count=1)
+    check_table_response(response_data['results'][0], table, table_name)
+
+
+def test_table_list_filter_import_verified(create_table, client):
+    tables = {
+        True: create_table('Filter Verified 1'),
+        False: create_table('Filter Verified 2'),
+    }
+    for verified, table in tables.items():
+        table.import_verified = verified
+        table.save()
+
+    for verified, table in tables.items():
+        query_str = str(verified).lower()
+        response = client.get(f'/api/v0/tables/?import_verified={query_str}')
+        response_data = response.json()
+        check_table_filter_response(response, status_code=200, count=1)
+        check_table_response(response_data['results'][0], table, table.name)
+
+
+def test_table_list_filter_imported(create_table, client):
+    tables = {
+        None: create_table('Filter Imported 1'),
+        False: create_table('Filter Imported 2'),
+        True: create_table('Filter Imported 3'),
+    }
+    for verified, table in tables.items():
+        table.import_verified = verified
+        table.save()
+
+    response = client.get('/api/v0/tables/?not_imported=false')
+    check_table_filter_response(response, status_code=200, count=2)
+
+    table = tables[None]
+    response = client.get('/api/v0/tables/?not_imported=true')
+    response_data = response.json()
+    check_table_filter_response(response, status_code=200, count=1)
+    check_table_response(response_data['results'][0], table, table.name)
+
+
 def test_table_detail(create_table, client):
     """
     Desired format: