From 55a018f85b3c70859abbadd177f6088fc00802cd Mon Sep 17 00:00:00 2001 From: Anish Umale Date: Tue, 16 Jul 2024 00:47:02 +0530 Subject: [PATCH 1/3] add rpc endpoints for explorations list and delete --- config/settings/common_settings.py | 3 +- mathesar/migrations/0011_explorations.py | 33 +++++++++++++++ mathesar/models/base.py | 11 +++++ mathesar/rpc/explorations.py | 51 ++++++++++++++++++++++++ mathesar/utils/explorations.py | 9 +++++ 5 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 mathesar/migrations/0011_explorations.py create mode 100644 mathesar/rpc/explorations.py create mode 100644 mathesar/utils/explorations.py diff --git a/config/settings/common_settings.py b/config/settings/common_settings.py index 4155531dce..aeda54a9e5 100644 --- a/config/settings/common_settings.py +++ b/config/settings/common_settings.py @@ -75,7 +75,8 @@ def pipe_delim(pipe_string): 'mathesar.rpc.schemas', 'mathesar.rpc.tables', 'mathesar.rpc.tables.metadata', - 'mathesar.rpc.types' + 'mathesar.rpc.types', + 'mathesar.rpc.explorations' ] TEMPLATES = [ diff --git a/mathesar/migrations/0011_explorations.py b/mathesar/migrations/0011_explorations.py new file mode 100644 index 0000000000..371d83d800 --- /dev/null +++ b/mathesar/migrations/0011_explorations.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.11 on 2024-07-15 19:14 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('mathesar', '0010_alter_tablemetadata_column_order_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Explorations', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('base_table_oid', models.PositiveBigIntegerField()), + ('name', models.CharField(max_length=128, unique=True)), + ('description', models.CharField(null=True)), + ('initial_columns', models.JSONField()), + ('transformations', models.JSONField(null=True)), + ('display_options', models.JSONField(null=True)), + ('display_names', models.JSONField()), + ('database', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mathesar.database')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/mathesar/models/base.py b/mathesar/models/base.py index 3781f5d98c..422e3696a5 100644 --- a/mathesar/models/base.py +++ b/mathesar/models/base.py @@ -138,3 +138,14 @@ class Meta: name="unique_table_metadata" ) ] + + +class Explorations(BaseModel): + database = models.ForeignKey('Database', on_delete=models.CASCADE) + base_table_oid = models.PositiveBigIntegerField() + name = models.CharField(max_length=128, unique=True) + description = models.CharField(null=True) + initial_columns = models.JSONField() + transformations = models.JSONField(null=True) + display_options = models.JSONField(null=True) + display_names = models.JSONField(null=False) diff --git a/mathesar/rpc/explorations.py b/mathesar/rpc/explorations.py new file mode 100644 index 0000000000..01ad74af1d --- /dev/null +++ b/mathesar/rpc/explorations.py @@ -0,0 +1,51 @@ +""" +Classes and functions exposed to the RPC endpoint for managing explorations. +""" +from typing import TypedDict + +from modernrpc.core import rpc_method +from modernrpc.auth.basic import http_basic_auth_login_required + +from mathesar.rpc.exceptions.handlers import handle_rpc_exceptions +from mathesar.utils.explorations import get_explorations, delete_exploration + + +class ExplorationInfo(TypedDict): + id: int + database: int + base_table_oid: int + name: str + description: str + initial_columns: list + transformations: dict + display_options: list + display_names: dict + + @classmethod + def from_model(cls, model): + return cls( + id=model.id, + database=model.database, + base_table_oid=model.base_table_oid, + name=model.name, + description=model.description, + initial_columns=model.initial_columns, + transformations=model.transformations, + display_options=model.display_options, + display_names=model.display_names, + ) + + +@rpc_method(name="explorations.list") +@http_basic_auth_login_required +@handle_rpc_exceptions +def list_(*, database_id: int, **kwargs) -> list[ExplorationInfo]: + explorations = get_explorations(database_id) + return [ExplorationInfo.from_model(exploration) for exploration in explorations] + + +@rpc_method(name="explorations.delete") +@http_basic_auth_login_required +@handle_rpc_exceptions +def delete(*, exploration_id: int, **kwargs) -> None: + delete_exploration(exploration_id) diff --git a/mathesar/utils/explorations.py b/mathesar/utils/explorations.py new file mode 100644 index 0000000000..db29594623 --- /dev/null +++ b/mathesar/utils/explorations.py @@ -0,0 +1,9 @@ +from mathesar.models.base import Explorations + + +def get_explorations(database_id): + return Explorations.objects.filter(database__id=database_id) + + +def delete_exploration(exploration_id): + Explorations.objects.get(id=exploration_id).delete() From cfd59fe1c749e8a6da2f20d63ede1f1a9ddc6ffd Mon Sep 17 00:00:00 2001 From: Anish Umale Date: Tue, 16 Jul 2024 18:41:27 +0530 Subject: [PATCH 2/3] add docs and minor args change --- docs/docs/api/rpc.md | 9 +++++ mathesar/migrations/0011_explorations.py | 6 +-- mathesar/models/base.py | 4 +- mathesar/rpc/explorations.py | 49 +++++++++++++++++++----- 4 files changed, 53 insertions(+), 15 deletions(-) diff --git a/docs/docs/api/rpc.md b/docs/docs/api/rpc.md index ed08c3c843..53817655de 100644 --- a/docs/docs/api/rpc.md +++ b/docs/docs/api/rpc.md @@ -152,6 +152,15 @@ To use an RPC function: - UniqueConstraint - CreatableConstraintInfo +## Explorations + +::: explorations + options: + members: + - list_ + - delete + - ExplorationInfo + ## Roles ::: roles diff --git a/mathesar/migrations/0011_explorations.py b/mathesar/migrations/0011_explorations.py index 371d83d800..5d7a951cd6 100644 --- a/mathesar/migrations/0011_explorations.py +++ b/mathesar/migrations/0011_explorations.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.11 on 2024-07-15 19:14 +# Generated by Django 4.2.11 on 2024-07-16 13:07 from django.db import migrations, models import django.db.models.deletion @@ -17,13 +17,13 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created_at', models.DateTimeField(auto_now_add=True)), ('updated_at', models.DateTimeField(auto_now=True)), - ('base_table_oid', models.PositiveBigIntegerField()), ('name', models.CharField(max_length=128, unique=True)), - ('description', models.CharField(null=True)), + ('base_table_oid', models.PositiveBigIntegerField()), ('initial_columns', models.JSONField()), ('transformations', models.JSONField(null=True)), ('display_options', models.JSONField(null=True)), ('display_names', models.JSONField()), + ('description', models.CharField(null=True)), ('database', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mathesar.database')), ], options={ diff --git a/mathesar/models/base.py b/mathesar/models/base.py index 422e3696a5..ac4dafceab 100644 --- a/mathesar/models/base.py +++ b/mathesar/models/base.py @@ -142,10 +142,10 @@ class Meta: class Explorations(BaseModel): database = models.ForeignKey('Database', on_delete=models.CASCADE) - base_table_oid = models.PositiveBigIntegerField() name = models.CharField(max_length=128, unique=True) - description = models.CharField(null=True) + base_table_oid = models.PositiveBigIntegerField() initial_columns = models.JSONField() transformations = models.JSONField(null=True) display_options = models.JSONField(null=True) display_names = models.JSONField(null=False) + description = models.CharField(null=True) diff --git a/mathesar/rpc/explorations.py b/mathesar/rpc/explorations.py index 01ad74af1d..5ac9bfc168 100644 --- a/mathesar/rpc/explorations.py +++ b/mathesar/rpc/explorations.py @@ -1,7 +1,7 @@ """ Classes and functions exposed to the RPC endpoint for managing explorations. """ -from typing import TypedDict +from typing import Optional, TypedDict from modernrpc.core import rpc_method from modernrpc.auth.basic import http_basic_auth_login_required @@ -11,28 +11,42 @@ class ExplorationInfo(TypedDict): + """ + Information about a Exploration. + + Attributes: + id: The Django id of an exploration. + database_id: The Django id of the database containing the exploration. + name: The name of the exploration. + base_table_oid: The OID of the base table of the exploration on the database. + initial_columns: A list describing the columns to be included in the exploration. + transformations: A list describing the transformations to be made on the included columns. + display_options: A list desrcibing metadata for the columns in the explorations. + display_names: A map between the actual column names on the database and the alias to be displayed(if any). + description: The description of the exploration. + """ id: int - database: int - base_table_oid: int + database_id: int name: str - description: str + base_table_oid: int initial_columns: list - transformations: dict - display_options: list - display_names: dict + transformations: Optional[list] + display_options: Optional[list] + display_names: Optional[dict] + description: Optional[str] @classmethod def from_model(cls, model): return cls( id=model.id, - database=model.database, - base_table_oid=model.base_table_oid, + database_id=model.database.id, name=model.name, - description=model.description, + base_table_oid=model.base_table_oid, initial_columns=model.initial_columns, transformations=model.transformations, display_options=model.display_options, display_names=model.display_names, + description=model.description, ) @@ -40,6 +54,15 @@ def from_model(cls, model): @http_basic_auth_login_required @handle_rpc_exceptions def list_(*, database_id: int, **kwargs) -> list[ExplorationInfo]: + """ + List information about explorations for a database. Exposed as `list`. + + Args: + database_id: The Django id of the database containing the explorations. + + Returns: + A list of exploration details. + """ explorations = get_explorations(database_id) return [ExplorationInfo.from_model(exploration) for exploration in explorations] @@ -48,4 +71,10 @@ def list_(*, database_id: int, **kwargs) -> list[ExplorationInfo]: @http_basic_auth_login_required @handle_rpc_exceptions def delete(*, exploration_id: int, **kwargs) -> None: + """ + Delete an exploration. + + Args: + exploration_id: The Django id of the exploration to delete. + """ delete_exploration(exploration_id) From 7e36a9ff251b1dc0641c60471154b7568cb8c5cc Mon Sep 17 00:00:00 2001 From: Anish Umale Date: Wed, 17 Jul 2024 17:49:50 +0530 Subject: [PATCH 3/3] add tests --- mathesar/tests/rpc/test_endpoints.py | 11 ++ mathesar/tests/rpc/test_explorations.py | 134 ++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 mathesar/tests/rpc/test_explorations.py diff --git a/mathesar/tests/rpc/test_endpoints.py b/mathesar/tests/rpc/test_endpoints.py index 4e09892736..12f4117c23 100644 --- a/mathesar/tests/rpc/test_endpoints.py +++ b/mathesar/tests/rpc/test_endpoints.py @@ -13,6 +13,7 @@ from mathesar.rpc import constraints from mathesar.rpc import database_setup from mathesar.rpc import databases +from mathesar.rpc import explorations from mathesar.rpc import roles from mathesar.rpc import schemas from mathesar.rpc import tables @@ -94,6 +95,16 @@ "databases.list", [user_is_authenticated] ), + ( + explorations.list_, + "explorations.list", + [user_is_authenticated] + ), + ( + explorations.delete, + "explorations.delete", + [user_is_authenticated] + ), ( roles.list_, "roles.list", diff --git a/mathesar/tests/rpc/test_explorations.py b/mathesar/tests/rpc/test_explorations.py new file mode 100644 index 0000000000..f43d15462a --- /dev/null +++ b/mathesar/tests/rpc/test_explorations.py @@ -0,0 +1,134 @@ +""" +This file tests the explorations RPC functions. + +Fixtures: + rf(pytest-django): Provides mocked `Request` objects. + monkeypatch(pytest): Lets you monkeypatch an object for testing. +""" +from mathesar.rpc import explorations +from mathesar.models.users import User +from mathesar.models.base import Database, Explorations + + +def test_explorations_list(rf, monkeypatch): + request = rf.post('/api/rpc/v0', data={}) + request.user = User(username='alice', password='pass1234') + database_id = 11 + + def mock_exploration_info(_database_id): + if _database_id != database_id: + raise AssertionError('incorrect parameters passed') + return [ + Explorations( + id=2, + database=Database(id=1), + name='page count', + base_table_oid=12375, + initial_columns=[ + {'id': 51586, 'alias': 'Items_Acquisition Date'}, + {'id': 51598, 'alias': 'Items_id'}, + {'id': 51572, 'alias': 'Books_Media', 'jp_path': [[51588, 51596]]}, + {'id': 51573, 'alias': 'Books_Page Count', 'jp_path': [[51588, 51596]]} + ], + transformations=[ + {'spec': {'greater': [{'column_name': ['Books_Page Count']}, {'literal': ['50']}]}, 'type': 'filter'} + ], + display_options=None, + display_names={ + 'Items_id': 'Items_id', + 'Items_Book': 'Items_Book', + 'Books_Media': 'Books_Media', + 'Items_Book_1': 'Items_Book_1', + 'Books_Page Count': 'Books_Page Count', + 'Items_Acquisition Date': 'Items_Acquisition Date' + }, + description=None + ), + Explorations( + id=3, + database=Database(id=1), + name='ISBN', + base_table_oid=12356, + initial_columns=[ + {'id': 51594, 'alias': 'Publishers_Name'}, + {'id': 51575, 'alias': 'Books_ISBN', 'jp_path': [[51593, 51579]]} + ], + transformations=[ + { + 'spec': { + 'base_grouping_column': 'Publishers_Name', + 'grouping_expressions': [ + {'input_alias': 'Publishers_Name', 'output_alias': 'Publishers_Name_grouped'} + ], + 'aggregation_expressions': [{ + 'function': 'distinct_aggregate_to_array', + 'input_alias': 'Books_ISBN', + 'output_alias': 'Books_ISBN_agged'}] + }, + 'type': 'summarize'} + ], + display_options=None, + display_names={ + 'Books_ISBN': 'Books_ISBN', 'Publishers_Name': 'Publishers_Name' + }, + description=None + ) + ] + monkeypatch.setattr(explorations, 'get_explorations', mock_exploration_info) + expect_explorations_list = [ + { + 'id': 2, + 'database_id': 1, + 'name': 'page count', + 'base_table_oid': 12375, + 'initial_columns': [ + {'id': 51586, 'alias': 'Items_Acquisition Date'}, + {'id': 51598, 'alias': 'Items_id'}, + {'id': 51572, 'alias': 'Books_Media', 'jp_path': [[51588, 51596]]}, + {'id': 51573, 'alias': 'Books_Page Count', 'jp_path': [[51588, 51596]]} + ], + 'transformations': [ + {'spec': {'greater': [{'column_name': ['Books_Page Count']}, {'literal': ['50']}]}, 'type': 'filter'}], + 'display_options': None, + 'display_names': { + 'Items_id': 'Items_id', + 'Items_Book': 'Items_Book', + 'Books_Media': 'Books_Media', + 'Items_Book_1': 'Items_Book_1', + 'Books_Page Count': 'Books_Page Count', + 'Items_Acquisition Date': 'Items_Acquisition Date' + }, + 'description': None + }, + { + 'id': 3, + 'database_id': 1, + 'name': 'ISBN', + 'base_table_oid': 12356, + 'initial_columns': [ + {'id': 51594, 'alias': 'Publishers_Name'}, + {'id': 51575, 'alias': 'Books_ISBN', 'jp_path': [[51593, 51579]]} + ], + 'transformations': [ + { + 'spec': { + 'base_grouping_column': 'Publishers_Name', + 'grouping_expressions': [ + {'input_alias': 'Publishers_Name', 'output_alias': 'Publishers_Name_grouped'} + ], + 'aggregation_expressions': [{ + 'function': 'distinct_aggregate_to_array', + 'input_alias': 'Books_ISBN', + 'output_alias': 'Books_ISBN_agged'}] + }, + 'type': 'summarize'} + ], + 'display_options': None, + 'display_names': { + 'Books_ISBN': 'Books_ISBN', 'Publishers_Name': 'Publishers_Name' + }, + 'description': None + } + ] + actual_explorations_list = explorations.list_(database_id=11) + assert actual_explorations_list == expect_explorations_list