From 7b9368e7259b685659c61be6fb5b4cfc8ead0a09 Mon Sep 17 00:00:00 2001
From: ishir21
Date: Mon, 18 Dec 2023 10:39:25 +0530
Subject: [PATCH 01/52] Removed the 'group' suffix in Data Explorer
---
mathesar/models/query.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/mathesar/models/query.py b/mathesar/models/query.py
index 5c92f21982..a0f191ef13 100644
--- a/mathesar/models/query.py
+++ b/mathesar/models/query.py
@@ -446,5 +446,4 @@ def _get_default_display_name_for_group_output_alias(
.map_of_output_alias_to_input_alias[output_alias]
input_alias_display_name = current_display_names.get(input_alias)
if input_alias_display_name:
- suffix_to_add = " group"
- return input_alias_display_name + suffix_to_add
+ return input_alias_display_name
From c4adf22f871ee2cc497c144e4b111288659e61e2 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Mar 2024 20:59:02 +0000
Subject: [PATCH 02/52] Bump django from 4.2.10 to 4.2.11
Bumps [django](https://github.com/django/django) from 4.2.10 to 4.2.11.
- [Commits](https://github.com/django/django/compare/4.2.10...4.2.11)
---
updated-dependencies:
- dependency-name: django
dependency-type: direct:production
...
Signed-off-by: dependabot[bot]
---
requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index 0fae4d3a46..e214eb3db0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,7 +3,7 @@ bidict==0.21.4
frozendict==2.1.3
charset-normalizer==2.0.7
clevercsv==0.6.8
-Django==4.2.10
+Django==4.2.11
dj-database-url==0.5.0
django-filter==23.5
django-property-filter==1.1.0
From c89c53a6afe236221cdbf804f98c56dbba6fe297 Mon Sep 17 00:00:00 2001
From: Sean Colsen
Date: Thu, 28 Mar 2024 11:07:18 -0400
Subject: [PATCH 03/52] Fix regeneration of exploration share URL
---
mathesar_ui/src/api/queryShares.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/mathesar_ui/src/api/queryShares.ts b/mathesar_ui/src/api/queryShares.ts
index daa0c7704f..f51fd39f75 100644
--- a/mathesar_ui/src/api/queryShares.ts
+++ b/mathesar_ui/src/api/queryShares.ts
@@ -32,7 +32,7 @@ function update(
function regenerate(queryId: number, shareId: QueryShare['id']) {
return postAPI(
- `/api/ui/v0/tables/${queryId}/shares/${shareId}/regenerate/`,
+ `/api/ui/v0/queries/${queryId}/shares/${shareId}/regenerate/`,
);
}
From d9bc2e2db8fe5a2b4bea486c75956f0cccbef6e2 Mon Sep 17 00:00:00 2001
From: Brent Moran
Date: Fri, 29 Mar 2024 11:14:01 -0500
Subject: [PATCH 04/52] add demo target to dockerfile for requirements
---
Dockerfile | 12 ++++++++++++
demo/settings.py | 3 +++
2 files changed, 15 insertions(+)
diff --git a/Dockerfile b/Dockerfile
index 84b2fb3a9f..175ccccd33 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -108,3 +108,15 @@ RUN rm -rf ./docs
EXPOSE 8000
ENTRYPOINT ["./run.sh"]
+
+
+#=========== STAGE: DEMO =====================================================#
+
+FROM production AS demo
+
+# Install prod requirements
+RUN pip install --no-cache-dir -r requirements-demo.txt
+
+EXPOSE 8000
+
+ENTRYPOINT ["./run.sh"]
diff --git a/demo/settings.py b/demo/settings.py
index 4ef3245d01..6487340f17 100644
--- a/demo/settings.py
+++ b/demo/settings.py
@@ -10,6 +10,9 @@
"demo.middleware.LiveDemoModeMiddleware",
]
+
+SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
+
MATHESAR_MODE = 'PRODUCTION'
MATHESAR_LIVE_DEMO = True
MATHESAR_LIVE_DEMO_USERNAME = decouple_config('MATHESAR_LIVE_DEMO_USERNAME', default=None)
From d9d167a379cb7da0eacea040cfdb872dadb492f6 Mon Sep 17 00:00:00 2001
From: Brent Moran
Date: Fri, 29 Mar 2024 11:32:56 -0500
Subject: [PATCH 05/52] reorder build so production is the default
---
Dockerfile | 19 ++++++++++++-------
1 file changed, 12 insertions(+), 7 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 175ccccd33..49f667b82a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -87,9 +87,9 @@ EXPOSE 8000 3000 6006
ENTRYPOINT ["./dev-run.sh"]
-#=========== STAGE: PRODUCTION ===============================================#
+#=========== STAGE: COMMON ===================================================#
-FROM base AS production
+from base as common
# Install prod requirements
RUN pip install --no-cache-dir -r requirements-prod.txt
@@ -105,14 +105,10 @@ RUN rm -rf ./mathesar_ui
RUN rm -rf ./mathesar/tests ./db/tests
RUN rm -rf ./docs
-EXPOSE 8000
-
-ENTRYPOINT ["./run.sh"]
-
#=========== STAGE: DEMO =====================================================#
-FROM production AS demo
+FROM common AS demo
# Install prod requirements
RUN pip install --no-cache-dir -r requirements-demo.txt
@@ -120,3 +116,12 @@ RUN pip install --no-cache-dir -r requirements-demo.txt
EXPOSE 8000
ENTRYPOINT ["./run.sh"]
+
+
+#=========== STAGE: PRODUCTION ===============================================#
+
+FROM common AS production
+
+EXPOSE 8000
+
+ENTRYPOINT ["./run.sh"]
From d8c0458f970136407ecd5f63c4e893f39f2cd0ad Mon Sep 17 00:00:00 2001
From: Brent Moran
Date: Tue, 2 Apr 2024 18:00:56 -0500
Subject: [PATCH 06/52] add functions to be exposed via RPC
---
mathesar/rpc/__init__.py | 0
mathesar/rpc/connections.py | 88 +++++++++++++++++++++++++++++++++++++
requirements.txt | 1 +
3 files changed, 89 insertions(+)
create mode 100644 mathesar/rpc/__init__.py
create mode 100644 mathesar/rpc/connections.py
diff --git a/mathesar/rpc/__init__.py b/mathesar/rpc/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/mathesar/rpc/connections.py b/mathesar/rpc/connections.py
new file mode 100644
index 0000000000..53e7f89b34
--- /dev/null
+++ b/mathesar/rpc/connections.py
@@ -0,0 +1,88 @@
+"""
+Functions exposed to the RPC endpoint for creating connections.
+"""
+from modernrpc.core import rpc_method
+from modernrpc.auth.basic import http_basic_auth_superuser_required
+
+from mathesar.utils import connections
+
+@rpc_method(name='connections.create_from_known_connection')
+@http_basic_auth_superuser_required
+def create_from_known_connection(
+ *,
+ nickname: str,
+ db_name: str,
+ create_db: bool=False,
+ connection_type: str='internal_database',
+ connection_id: int=None,
+ sample_data: list[str]=[],
+) -> int:
+ """
+ Create a new connection from an already existing one.
+
+ When using `connection_type`='user_database', the `connection_id`
+ parameter is required.
+
+ Args:
+ nickname: Used to identify the created connection
+ db_name: The name of the database on the server.
+ create_db: Whether we should create the database `db_name` if it
+ doesn't already exist.
+ connection_type: Type of the known connection - one of
+ 'internal_database' or 'user_database'
+ connection_id: Identifies the known connection when combined with
+ the user_database value for the connection_type parameter
+ sample_data: A list of strings requesting that some example data
+ sets be installed on the underlying database. Valid list
+ members are 'library_management' and 'movie_collection'.
+
+ Returns:
+ The Django id of the Database object associated with the connection.
+ """
+ connection = {
+ 'connection_type': connection_type, 'connection_id': connection_id
+ }
+ db_model = connections.copy_connection_from_preexisting(
+ connection, nickname, db_name, create_db, sample_data
+ )
+ return db_model.id
+
+
+@rpc_method(name='connections.create_from_scratch')
+@http_basic_auth_superuser_required
+def create_from_scratch(
+ *,
+ nickname: str,
+ db_name: str,
+ user: str,
+ password: str,
+ host: str,
+ port: str,
+ sample_data: list[str]=[],
+) -> int:
+ """
+ Create a new connection to a PostgreSQL server from scratch.
+
+ This requires inputting valid credentials for the connection. When
+ setting up the connection, therefore, the `db_name` must already
+ exist on the PostgreSQL server.
+
+ Args:
+ nickname: Used to identify the created connection.
+ db_name: The name of the database on the server.
+ user: A valid user (role) on the server, with `CONNECT` and
+ `CREATE` privileges on the database given by `db_name`.
+ password: The password for `user`.
+ host: The hostname or IP address of the PostgreSQL server.
+ port: The port of the PostgreSQL server.
+ sample_data: A list of strings requesting that some example data
+ sets be installed on the underlying database. Valid list
+ members are 'library_management' and 'movie_collection'.
+
+ Returns:
+ The Django id of the Database object associated with the connection.
+ """
+ db_model = connections.create_connection_from_scratch(
+ user, password, host, port, nickname, db_name, sample_data
+ )
+ return db_model.id
diff --git a/requirements.txt b/requirements.txt
index 0fae4d3a46..8e38db978b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,6 +6,7 @@ clevercsv==0.6.8
Django==4.2.10
dj-database-url==0.5.0
django-filter==23.5
+django-modern-rpc==1.0.3
django-property-filter==1.1.0
django-request-cache==1.3.0
djangorestframework==3.14.0
From 19751da2e5a9aa2a4e6f8dcb8b888e6ae05499be Mon Sep 17 00:00:00 2001
From: Brent Moran
Date: Tue, 2 Apr 2024 18:02:54 -0500
Subject: [PATCH 07/52] expose functions at /rpc/
---
config/settings/common_settings.py | 5 +++++
mathesar/urls.py | 1 +
mathesar/views.py | 5 +++++
3 files changed, 11 insertions(+)
diff --git a/config/settings/common_settings.py b/config/settings/common_settings.py
index 4d395ef80d..930482cbfa 100644
--- a/config/settings/common_settings.py
+++ b/config/settings/common_settings.py
@@ -43,6 +43,7 @@ def pipe_delim(pipe_string):
"django_filters",
"django_property_filter",
"drf_spectacular",
+ "modernrpc",
"mathesar",
]
@@ -64,6 +65,10 @@ def pipe_delim(pipe_string):
ROOT_URLCONF = "config.urls"
+MODERNRPC_METHODS_MODULES = [
+ 'mathesar.rpc.connections'
+]
+
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
diff --git a/mathesar/urls.py b/mathesar/urls.py
index 8c9491c5f6..6418d9edaf 100644
--- a/mathesar/urls.py
+++ b/mathesar/urls.py
@@ -38,6 +38,7 @@
ui_router.register(r'queries/(?P[^/.]+)/shares', ui_viewsets.SharedQueryViewSet, basename='shared-query')
urlpatterns = [
+ path('rpc/', views.MathesarRPCEntryPoint.as_view()),
path('api/db/v0/', include(db_router.urls)),
path('api/db/v0/', include(db_table_router.urls)),
path('api/ui/v0/', include(ui_router.urls)),
diff --git a/mathesar/views.py b/mathesar/views.py
index fe9c87110d..f013e02554 100644
--- a/mathesar/views.py
+++ b/mathesar/views.py
@@ -1,6 +1,8 @@
from django.conf import settings
from django.contrib.auth.decorators import login_required
+from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import render, redirect, get_object_or_404
+from modernrpc.views import RPCEntryPoint
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
@@ -274,6 +276,9 @@ def get_common_data_for_shared_query(request, query):
}
+class MathesarRPCEntryPoint(LoginRequiredMixin, RPCEntryPoint):
+ pass
+
@login_required
@api_view(['POST'])
def reflect_all(_):
From 02117f20bc26ab5997ee9605316399d0f01bdd54 Mon Sep 17 00:00:00 2001
From: Brent Moran
Date: Tue, 2 Apr 2024 18:03:34 -0500
Subject: [PATCH 08/52] add basic documentation of the RPC endpoint
---
docs/docs/user-guide/rpc-functions.md | 5 +++++
docs/mkdocs.yml | 10 ++++++++++
docs/requirements.txt | 2 ++
3 files changed, 17 insertions(+)
create mode 100644 docs/docs/user-guide/rpc-functions.md
diff --git a/docs/docs/user-guide/rpc-functions.md b/docs/docs/user-guide/rpc-functions.md
new file mode 100644
index 0000000000..bb3c63c204
--- /dev/null
+++ b/docs/docs/user-guide/rpc-functions.md
@@ -0,0 +1,5 @@
+# Using the RPC endpoint
+
+As of release 0.1.7, Mathesar comes with a new RPC endpoint, available at `/rpc/`. This endpoint lets you (or the front end) call some back end functions directly, using the JSON-RPC 2.0 protocol. The documentation of these functions is available here. To use an RPC function, call it with a dot path starting from its root path. So, to call function `my_func` from the `connections` section, you'd call `connections.my_func`.
+
+::: mathesar.rpc.connections
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index a486a03c4d..01815ce87a 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -49,6 +49,16 @@ plugins:
"install/build-from-source/index.md": "installation/build-from-source/index.md"
- macros
- placeholder
+ - mkdocstrings:
+ handlers:
+ python:
+ paths: [..]
+ options:
+ docstring_style: google
+ separate_signature: true
+ show_root_heading: true
+ show_root_full_path: false
+ show_source: false
theme:
name: material
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 8eef20f7fd..489148efae 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -3,3 +3,5 @@ mkdocs-material==8.5.11
mkdocs-redirects==1.2.0
mkdocs-macros-plugin==0.7.0
mkdocs-placeholder-plugin==0.3.1
+mkdocstrings==0.24.2
+mkdocstrings-python==1.9.2
From 18bd599bf6039b0b6810e06cd5ac0db07bcf0d05 Mon Sep 17 00:00:00 2001
From: Brent Moran
Date: Tue, 2 Apr 2024 20:53:59 -0500
Subject: [PATCH 09/52] make flake8 happy
---
mathesar/rpc/connections.py | 11 ++++++-----
mathesar/views.py | 1 +
requirements-dev.txt | 3 +--
3 files changed, 8 insertions(+), 7 deletions(-)
diff --git a/mathesar/rpc/connections.py b/mathesar/rpc/connections.py
index 53e7f89b34..3137af03eb 100644
--- a/mathesar/rpc/connections.py
+++ b/mathesar/rpc/connections.py
@@ -6,16 +6,17 @@
from mathesar.utils import connections
+
@rpc_method(name='connections.create_from_known_connection')
@http_basic_auth_superuser_required
def create_from_known_connection(
*,
nickname: str,
db_name: str,
- create_db: bool=False,
- connection_type: str='internal_database',
- connection_id: int=None,
- sample_data: list[str]=[],
+ create_db: bool = False,
+ connection_type: str = 'internal_database',
+ connection_id: int = None,
+ sample_data: list[str] = [],
) -> int:
"""
Create a new connection from an already existing one.
@@ -58,7 +59,7 @@ def create_from_scratch(
password: str,
host: str,
port: str,
- sample_data: list[str]=[],
+ sample_data: list[str] = [],
) -> int:
"""
Create a new connection to a PostgreSQL server from scratch.
diff --git a/mathesar/views.py b/mathesar/views.py
index f013e02554..6fc11b63ed 100644
--- a/mathesar/views.py
+++ b/mathesar/views.py
@@ -279,6 +279,7 @@ def get_common_data_for_shared_query(request, query):
class MathesarRPCEntryPoint(LoginRequiredMixin, RPCEntryPoint):
pass
+
@login_required
@api_view(['POST'])
def reflect_all(_):
diff --git a/requirements-dev.txt b/requirements-dev.txt
index ff159003d7..bb44ce3a1f 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -8,6 +8,5 @@ pytest-cov==3.0.0
pytest-xdist[psutil]==2.5.0
playwright==1.18.1
pytest-playwright==0.2.3
-flake8-no-types==1.4.0
mkdocs-material==8.5.11
-mkdocs-redirects==1.2.0
\ No newline at end of file
+mkdocs-redirects==1.2.0
From 2ecc28e8985ff61cb3311a8fc46526882c4018f8 Mon Sep 17 00:00:00 2001
From: Brent Moran
Date: Tue, 2 Apr 2024 21:07:18 -0500
Subject: [PATCH 10/52] move RPC endpoint to conform to others
---
mathesar/urls.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/mathesar/urls.py b/mathesar/urls.py
index 6418d9edaf..a0b438376d 100644
--- a/mathesar/urls.py
+++ b/mathesar/urls.py
@@ -38,7 +38,7 @@
ui_router.register(r'queries/(?P[^/.]+)/shares', ui_viewsets.SharedQueryViewSet, basename='shared-query')
urlpatterns = [
- path('rpc/', views.MathesarRPCEntryPoint.as_view()),
+ path('api/rpc/v0/', views.MathesarRPCEntryPoint.as_view()),
path('api/db/v0/', include(db_router.urls)),
path('api/db/v0/', include(db_table_router.urls)),
path('api/ui/v0/', include(ui_router.urls)),
From 55ed922accf832d4e7f72e5da4d5bfc68a73da17 Mon Sep 17 00:00:00 2001
From: Brent Moran
Date: Tue, 2 Apr 2024 21:08:08 -0500
Subject: [PATCH 11/52] Add usage help to RPC docs
---
docs/docs/user-guide/rpc-functions.md | 20 +++++++++++++++++++-
docs/mkdocs.yml | 1 +
2 files changed, 20 insertions(+), 1 deletion(-)
diff --git a/docs/docs/user-guide/rpc-functions.md b/docs/docs/user-guide/rpc-functions.md
index bb3c63c204..75773fb5c5 100644
--- a/docs/docs/user-guide/rpc-functions.md
+++ b/docs/docs/user-guide/rpc-functions.md
@@ -1,5 +1,23 @@
# Using the RPC endpoint
-As of release 0.1.7, Mathesar comes with a new RPC endpoint, available at `/rpc/`. This endpoint lets you (or the front end) call some back end functions directly, using the JSON-RPC 2.0 protocol. The documentation of these functions is available here. To use an RPC function, call it with a dot path starting from its root path. So, to call function `my_func` from the `connections` section, you'd call `connections.my_func`.
+As of release 0.1.7, Mathesar comes with an RPC endpoint, available at `/api/rpc/v0/`. This endpoint enables calling some backend functions directly, using the JSON-RPC 2.0 protocol. The documentation of these functions is available on this page. To use an RPC function, call it with a dot path starting from its root path. So, to call function `create_from_known_connection` from the `connections` section of this page, you'd send something like:
+
+```
+POST /api/rpc/v0/
+```
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "connections.create_from_known_connection",
+ "params": {"nickname": "anewconnection", "db_name": "mynewcooldb"},
+ "id": 234
+}
+```
+
+Note that
+
+- all parameters for all functions documented here are keyword parameters (even the required ones). In other words,
+- no positional arguments are allowed.
+- Requests must be made with valid session IDs, as well as CSRF cookies and tokens.
::: mathesar.rpc.connections
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 01815ce87a..47512d9d69 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -25,6 +25,7 @@ nav:
- Importing data: user-guide/importing-data.md
- Syncing database changes: user-guide/syncing-db.md
- Users & access levels: user-guide/users.md
+ - Using the RPC endpoint: user-guide/rpc-functions.md
- Glossary: user-guide/glossary.md
- Releases:
- '0.1.6': releases/0.1.6.md
From 3d81f485fa59fba2a706b255fa0ddc96d5a53d8a Mon Sep 17 00:00:00 2001
From: Brent Moran
Date: Tue, 2 Apr 2024 21:10:48 -0500
Subject: [PATCH 12/52] remove type-hint check from pipeline
---
.github/workflows/test-and-lint-code.yml | 1 -
1 file changed, 1 deletion(-)
diff --git a/.github/workflows/test-and-lint-code.yml b/.github/workflows/test-and-lint-code.yml
index 0aa3149f3a..030700ade3 100644
--- a/.github/workflows/test-and-lint-code.yml
+++ b/.github/workflows/test-and-lint-code.yml
@@ -169,7 +169,6 @@ jobs:
with:
checkName: "flake8"
path: "."
- plugins: flake8-no-types
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
From f2433eb1ab765c3db0f2d46fcbc00bfa0586ea58 Mon Sep 17 00:00:00 2001
From: Sean Colsen
Date: Wed, 3 Apr 2024 14:59:54 -0400
Subject: [PATCH 13/52] Clean up RPC API docs
---
docs/docs/api/rest.md | 8 +++++
docs/docs/api/rpc.md | 42 +++++++++++++++++++++++++++
docs/docs/user-guide/rpc-functions.md | 23 ---------------
docs/mkdocs.yml | 4 ++-
4 files changed, 53 insertions(+), 24 deletions(-)
create mode 100644 docs/docs/api/rest.md
create mode 100644 docs/docs/api/rpc.md
delete mode 100644 docs/docs/user-guide/rpc-functions.md
diff --git a/docs/docs/api/rest.md b/docs/docs/api/rest.md
new file mode 100644
index 0000000000..3adb0f6270
--- /dev/null
+++ b/docs/docs/api/rest.md
@@ -0,0 +1,8 @@
+# REST API
+
+Mathesar has a REST API that the front end uses to interact with the backend.
+
+For Mathesar's beta release, we are actively transitioning to a new [RPC-style API](https://wiki.mathesar.org/projects/2024/architecture-transition/rpc/) and will soon be phasing out the REST API entirely.
+
+The REST API is not documented and is not intended to be used by third-party developers.
+
diff --git a/docs/docs/api/rpc.md b/docs/docs/api/rpc.md
new file mode 100644
index 0000000000..f7499481ec
--- /dev/null
+++ b/docs/docs/api/rpc.md
@@ -0,0 +1,42 @@
+# RPC API
+
+Mathesar has an API available at `/api/rpc/v0/` which follows the [JSON-RPC](https://www.jsonrpc.org/specification) spec version 2.0.
+
+## About
+
+### Status
+
+We are currently in the process of [transitioning](https://wiki.mathesar.org/projects/2024/architecture-transition/rpc/) our API architecture from a [RESTful](../rest.md) API to this RPC-style API, and we hope to have all functionality available through the RPC API by Mathesar's beta release.
+
+!!! caution "Stability"
+ The RPC API is not yet stable and may change in the future, even after we've completed the transition to the RPC API architecture. If you build logic that depends on this API, be mindful that it may change in the future without warning or notice.
+
+### Usage
+
+To use an RPC function:
+
+- Call it with a dot path starting from its root path.
+- Always use named parameters.
+- Ensure that your request includes HTTP headers for valid session IDs, as well as CSRF cookies and tokens.
+
+!!! example
+
+ To call function `create_from_known_connection` from the `connections` section of this page, you'd send something like:
+
+ `POST /api/rpc/v0/`
+
+ ```json
+ {
+ "jsonrpc": "2.0",
+ "id": 234,
+ "method": "connections.create_from_known_connection",
+ "params": {
+ "nickname": "anewconnection",
+ "db_name": "mynewcooldb"
+ },
+ }
+ ```
+
+---
+
+::: mathesar.rpc.connections
diff --git a/docs/docs/user-guide/rpc-functions.md b/docs/docs/user-guide/rpc-functions.md
deleted file mode 100644
index 75773fb5c5..0000000000
--- a/docs/docs/user-guide/rpc-functions.md
+++ /dev/null
@@ -1,23 +0,0 @@
-# Using the RPC endpoint
-
-As of release 0.1.7, Mathesar comes with an RPC endpoint, available at `/api/rpc/v0/`. This endpoint enables calling some backend functions directly, using the JSON-RPC 2.0 protocol. The documentation of these functions is available on this page. To use an RPC function, call it with a dot path starting from its root path. So, to call function `create_from_known_connection` from the `connections` section of this page, you'd send something like:
-
-```
-POST /api/rpc/v0/
-```
-```json
-{
- "jsonrpc": "2.0",
- "method": "connections.create_from_known_connection",
- "params": {"nickname": "anewconnection", "db_name": "mynewcooldb"},
- "id": 234
-}
-```
-
-Note that
-
-- all parameters for all functions documented here are keyword parameters (even the required ones). In other words,
-- no positional arguments are allowed.
-- Requests must be made with valid session IDs, as well as CSRF cookies and tokens.
-
-::: mathesar.rpc.connections
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 47512d9d69..165e06bce7 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -25,8 +25,10 @@ nav:
- Importing data: user-guide/importing-data.md
- Syncing database changes: user-guide/syncing-db.md
- Users & access levels: user-guide/users.md
- - Using the RPC endpoint: user-guide/rpc-functions.md
- Glossary: user-guide/glossary.md
+ - API:
+ - REST: api/rest.md
+ - RPC: api/rpc.md
- Releases:
- '0.1.6': releases/0.1.6.md
- '0.1.5': releases/0.1.5.md
From 24809a700fd429314ae2123ff3803d928126b6c9 Mon Sep 17 00:00:00 2001
From: Brent Moran
Date: Wed, 3 Apr 2024 17:40:21 -0500
Subject: [PATCH 14/52] fix broken link in docs
---
docs/docs/api/rpc.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/docs/api/rpc.md b/docs/docs/api/rpc.md
index f7499481ec..a962fc4992 100644
--- a/docs/docs/api/rpc.md
+++ b/docs/docs/api/rpc.md
@@ -6,7 +6,7 @@ Mathesar has an API available at `/api/rpc/v0/` which follows the [JSON-RPC](htt
### Status
-We are currently in the process of [transitioning](https://wiki.mathesar.org/projects/2024/architecture-transition/rpc/) our API architecture from a [RESTful](../rest.md) API to this RPC-style API, and we hope to have all functionality available through the RPC API by Mathesar's beta release.
+We are currently in the process of [transitioning](https://wiki.mathesar.org/projects/2024/architecture-transition/rpc/) our API architecture from a [RESTful](rest.md) API to this RPC-style API, and we hope to have all functionality available through the RPC API by Mathesar's beta release.
!!! caution "Stability"
The RPC API is not yet stable and may change in the future, even after we've completed the transition to the RPC API architecture. If you build logic that depends on this API, be mindful that it may change in the future without warning or notice.
From 51951bd88a43c19c9e0002ea5cc0ae5f9240d0a3 Mon Sep 17 00:00:00 2001
From: Brent Moran
Date: Wed, 3 Apr 2024 18:16:24 -0500
Subject: [PATCH 15/52] add class to represent return type for connection funcs
---
mathesar/rpc/connections.py | 60 +++++++++++++++++++++++++++++--------
1 file changed, 47 insertions(+), 13 deletions(-)
diff --git a/mathesar/rpc/connections.py b/mathesar/rpc/connections.py
index 3137af03eb..de083a37ba 100644
--- a/mathesar/rpc/connections.py
+++ b/mathesar/rpc/connections.py
@@ -1,5 +1,5 @@
"""
-Functions exposed to the RPC endpoint for creating connections.
+Classes and functions exposed to the RPC endpoint for creating connections.
"""
from modernrpc.core import rpc_method
from modernrpc.auth.basic import http_basic_auth_superuser_required
@@ -7,6 +7,38 @@
from mathesar.utils import connections
+class DBModelReturn:
+
+ """
+ Information about a database model.
+
+ Attributes:
+ id (int): The Django id of the Database object created.
+ nickname (str): Used to identify the created connection.
+ database (str): The name of the database on the server.
+ username (str): The username of the role for the connection.
+ host (str): The hostname or IP address of the Postgres server.
+ port (int): The port of the Postgres server.
+ """
+
+ def __init__(
+ self,
+ id: int,
+ name: str,
+ db_name: str,
+ username: str,
+ host: str,
+ port: int,
+ **kwargs
+ ):
+ self.id = id
+ self.nickname = name
+ self.database = db_name
+ self.username = username
+ self.host = host
+ self.port = port
+
+
@rpc_method(name='connections.create_from_known_connection')
@http_basic_auth_superuser_required
def create_from_known_connection(
@@ -14,23 +46,20 @@ def create_from_known_connection(
nickname: str,
db_name: str,
create_db: bool = False,
- connection_type: str = 'internal_database',
connection_id: int = None,
sample_data: list[str] = [],
-) -> int:
+) -> DBModelReturn:
"""
Create a new connection from an already existing one.
- When using `connection_type`='user_database', the `connection_id`
- parameter is required.
+ If no `connection_id` is passed, the internal database connection
+ will be used.
Args:
nickname: Used to identify the created connection
db_name: The name of the database on the server.
create_db: Whether we should create the database `db_name` if it
doesn't already exist.
- connection_type: Type of the known connection - one of
- 'internal_database' or 'user_database'
connection_id: Identifies the known connection when combined with
the user_database value for the connection_type parameter
sample_data: A list of strings requesting that some example data
@@ -38,15 +67,20 @@ def create_from_known_connection(
members are 'library_management' and 'movie_collection'.
Returns:
- The Django id of the Database object associated with the connection.
+ Metadata about the Database associated with the connection.
"""
+ if connection_id is not None:
+ connection_type = 'user_database'
+ else:
+ connection_type = 'internal_database'
connection = {
- 'connection_type': connection_type, 'connection_id': connection_id
+ 'connection_type': connection_type,
+ 'connection_id': connection_id
}
db_model = connections.copy_connection_from_preexisting(
connection, nickname, db_name, create_db, sample_data
)
- return db_model.id
+ return DBModelReturn(**db_model.__dict__).__dict__
@rpc_method(name='connections.create_from_scratch')
@@ -60,7 +94,7 @@ def create_from_scratch(
host: str,
port: str,
sample_data: list[str] = [],
-) -> int:
+) -> DBModelReturn:
"""
Create a new connection to a PostgreSQL server from scratch.
@@ -81,9 +115,9 @@ def create_from_scratch(
members are 'library_management' and 'movie_collection'.
Returns:
- The Django id of the Database object associated with the connection.
+ Metadata about the Database associated with the connection.
"""
db_model = connections.create_connection_from_scratch(
user, password, host, port, nickname, db_name, sample_data
)
- return db_model.id
+ return DBModelReturn(**db_model.__dict__).__dict__
From 4697875e3d38534d4150c627d4ca2f8dac7f5ad2 Mon Sep 17 00:00:00 2001
From: Brent Moran
Date: Wed, 3 Apr 2024 18:16:54 -0500
Subject: [PATCH 16/52] add docs details to give info about the return type
---
docs/docs/api/rpc.md | 5 +++++
docs/mkdocs.yml | 2 ++
2 files changed, 7 insertions(+)
diff --git a/docs/docs/api/rpc.md b/docs/docs/api/rpc.md
index a962fc4992..00fed2b846 100644
--- a/docs/docs/api/rpc.md
+++ b/docs/docs/api/rpc.md
@@ -40,3 +40,8 @@ To use an RPC function:
---
::: mathesar.rpc.connections
+ options:
+ members:
+ - create_from_known_connection
+ - create_from_scratch
+ - DBModelReturn
diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml
index 165e06bce7..9d62188248 100644
--- a/docs/mkdocs.yml
+++ b/docs/mkdocs.yml
@@ -62,6 +62,8 @@ plugins:
show_root_heading: true
show_root_full_path: false
show_source: false
+ show_symbol_type_heading: true
+ group_by_category: false
theme:
name: material
From ffe19d3b6e6dd49d39935a03c095f7b7c4c07c69 Mon Sep 17 00:00:00 2001
From: hitenvidhani
Date: Fri, 5 Apr 2024 15:32:02 +0530
Subject: [PATCH 17/52] init
---
.../header-cell/ColumnHeaderContextMenu.svelte | 18 ++++++++++++++++++
.../src/systems/table-view/row/RowCell.svelte | 11 ++++++++---
2 files changed, 26 insertions(+), 3 deletions(-)
diff --git a/mathesar_ui/src/systems/table-view/header/header-cell/ColumnHeaderContextMenu.svelte b/mathesar_ui/src/systems/table-view/header/header-cell/ColumnHeaderContextMenu.svelte
index b4d15b8712..9c9518f7c6 100644
--- a/mathesar_ui/src/systems/table-view/header/header-cell/ColumnHeaderContextMenu.svelte
+++ b/mathesar_ui/src/systems/table-view/header/header-cell/ColumnHeaderContextMenu.svelte
@@ -6,6 +6,7 @@
type SortDirection,
} from '@mathesar/components/sort-entry/utils';
import {
+ iconTable,
iconAddFilter,
iconGrouping,
iconRemoveFilter,
@@ -20,6 +21,8 @@
} from '@mathesar/stores/table-data';
import { currentDatabase } from '@mathesar/stores/databases';
import { currentSchema } from '@mathesar/stores/schemas';
+ import { storeToGetTablePageUrl } from '@mathesar/stores/storeBasedUrls';
+ import Identifier from '@mathesar/components/Identifier.svelte';
const userProfile = getUserProfileStoreFromContext();
@@ -53,6 +56,14 @@
$: hasGrouping = $grouping.hasColumn(columnId);
+ $: linkKey = processedColumn.linkFk;
+ $: getTablePageUrl = $storeToGetTablePageUrl;
+ $: linkedTableName = linkKey ? linkKey.name : undefined;
+ $: linkedTableHref = linkKey
+ ? getTablePageUrl({ tableId: linkKey.referent_table })
+ : undefined;
+ $: ({ column } = processedColumn);
+
function addFilter() {
void imperativeFilterController?.beginAddingNewFilteringEntry(columnId);
}
@@ -83,6 +94,13 @@
}
+{#if linkedTableHref && linkedTableName}
+
+ {$_('open')}
+ {column.name}
+ {$_('table')}
+
+{/if}
{#if columnAllowsFiltering}
{#if filterCount > 0}
diff --git a/mathesar_ui/src/systems/table-view/row/RowCell.svelte b/mathesar_ui/src/systems/table-view/row/RowCell.svelte
index a34d1a930a..e0de3842eb 100644
--- a/mathesar_ui/src/systems/table-view/row/RowCell.svelte
+++ b/mathesar_ui/src/systems/table-view/row/RowCell.svelte
@@ -21,7 +21,7 @@
scrollBasedOnActiveCell,
SheetCell,
} from '@mathesar/components/sheet';
- import { iconLinkToRecordPage, iconSetToNull } from '@mathesar/icons';
+ import { iconSetToNull, iconRecord } from '@mathesar/icons';
import { currentDatabase } from '@mathesar/stores/databases';
import { currentSchema } from '@mathesar/stores/schemas';
import { storeToGetRecordPageUrl } from '@mathesar/stores/storeBasedUrls';
@@ -34,6 +34,7 @@
type TabularDataSelection,
} from '@mathesar/stores/table-data';
import { getUserProfileStoreFromContext } from '@mathesar/stores/userProfile';
+ import Identifier from '@mathesar/components/Identifier.svelte';
import CellErrors from './CellErrors.svelte';
import ColumnHeaderContextMenu from '../header/header-cell/ColumnHeaderContextMenu.svelte';
import RowContextOptions from './RowContextOptions.svelte';
@@ -207,8 +208,12 @@
{/if}
{#if showLinkedRecordHyperLink && linkedRecordHref}
-
- {$_('go_to_linked_record')}
+
+ {$_('open')}
+ {$recordSummaries.get(String(column.id))?.get(String(value)) ||
+ value}
{/if}
From 5df50d20813bfea58f5199a4890683aa57e8eb38 Mon Sep 17 00:00:00 2001
From: hitenvidhani
Date: Fri, 5 Apr 2024 20:26:30 +0530
Subject: [PATCH 18/52] Fix Feature: Added method for opening table
---
**Warning** | 0
Only | 0
mathesar-venv/bin/Activate.ps1 | 247 ++++++++++++++++++
mathesar-venv/bin/activate | 69 +++++
mathesar-venv/bin/activate.csh | 26 ++
mathesar-venv/bin/activate.fish | 69 +++++
mathesar-venv/bin/pip | 8 +
mathesar-venv/bin/pip3 | 8 +
mathesar-venv/bin/pip3.10 | 8 +
mathesar-venv/bin/python | 1 +
mathesar-venv/bin/python3 | 1 +
mathesar-venv/bin/python3.10 | 1 +
mathesar-venv/lib64 | 1 +
mathesar-venv/pyvenv.cfg | 3 +
.../ColumnHeaderContextMenu.svelte | 8 +-
15 files changed, 449 insertions(+), 1 deletion(-)
create mode 100644 **Warning**
create mode 100644 Only
create mode 100644 mathesar-venv/bin/Activate.ps1
create mode 100644 mathesar-venv/bin/activate
create mode 100644 mathesar-venv/bin/activate.csh
create mode 100644 mathesar-venv/bin/activate.fish
create mode 100755 mathesar-venv/bin/pip
create mode 100755 mathesar-venv/bin/pip3
create mode 100755 mathesar-venv/bin/pip3.10
create mode 120000 mathesar-venv/bin/python
create mode 120000 mathesar-venv/bin/python3
create mode 120000 mathesar-venv/bin/python3.10
create mode 120000 mathesar-venv/lib64
create mode 100644 mathesar-venv/pyvenv.cfg
diff --git a/**Warning** b/**Warning**
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/Only b/Only
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/mathesar-venv/bin/Activate.ps1 b/mathesar-venv/bin/Activate.ps1
new file mode 100644
index 0000000000..eeea3583fa
--- /dev/null
+++ b/mathesar-venv/bin/Activate.ps1
@@ -0,0 +1,247 @@
+<#
+.Synopsis
+Activate a Python virtual environment for the current PowerShell session.
+
+.Description
+Pushes the python executable for a virtual environment to the front of the
+$Env:PATH environment variable and sets the prompt to signify that you are
+in a Python virtual environment. Makes use of the command line switches as
+well as the `pyvenv.cfg` file values present in the virtual environment.
+
+.Parameter VenvDir
+Path to the directory that contains the virtual environment to activate. The
+default value for this is the parent of the directory that the Activate.ps1
+script is located within.
+
+.Parameter Prompt
+The prompt prefix to display when this virtual environment is activated. By
+default, this prompt is the name of the virtual environment folder (VenvDir)
+surrounded by parentheses and followed by a single space (ie. '(.venv) ').
+
+.Example
+Activate.ps1
+Activates the Python virtual environment that contains the Activate.ps1 script.
+
+.Example
+Activate.ps1 -Verbose
+Activates the Python virtual environment that contains the Activate.ps1 script,
+and shows extra information about the activation as it executes.
+
+.Example
+Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
+Activates the Python virtual environment located in the specified location.
+
+.Example
+Activate.ps1 -Prompt "MyPython"
+Activates the Python virtual environment that contains the Activate.ps1 script,
+and prefixes the current prompt with the specified string (surrounded in
+parentheses) while the virtual environment is active.
+
+.Notes
+On Windows, it may be required to enable this Activate.ps1 script by setting the
+execution policy for the user. You can do this by issuing the following PowerShell
+command:
+
+PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
+
+For more information on Execution Policies:
+https://go.microsoft.com/fwlink/?LinkID=135170
+
+#>
+Param(
+ [Parameter(Mandatory = $false)]
+ [String]
+ $VenvDir,
+ [Parameter(Mandatory = $false)]
+ [String]
+ $Prompt
+)
+
+<# Function declarations --------------------------------------------------- #>
+
+<#
+.Synopsis
+Remove all shell session elements added by the Activate script, including the
+addition of the virtual environment's Python executable from the beginning of
+the PATH variable.
+
+.Parameter NonDestructive
+If present, do not remove this function from the global namespace for the
+session.
+
+#>
+function global:deactivate ([switch]$NonDestructive) {
+ # Revert to original values
+
+ # The prior prompt:
+ if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
+ Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
+ Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
+ }
+
+ # The prior PYTHONHOME:
+ if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
+ Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
+ Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
+ }
+
+ # The prior PATH:
+ if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
+ Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
+ Remove-Item -Path Env:_OLD_VIRTUAL_PATH
+ }
+
+ # Just remove the VIRTUAL_ENV altogether:
+ if (Test-Path -Path Env:VIRTUAL_ENV) {
+ Remove-Item -Path env:VIRTUAL_ENV
+ }
+
+ # Just remove VIRTUAL_ENV_PROMPT altogether.
+ if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
+ Remove-Item -Path env:VIRTUAL_ENV_PROMPT
+ }
+
+ # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
+ if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
+ Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
+ }
+
+ # Leave deactivate function in the global namespace if requested:
+ if (-not $NonDestructive) {
+ Remove-Item -Path function:deactivate
+ }
+}
+
+<#
+.Description
+Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
+given folder, and returns them in a map.
+
+For each line in the pyvenv.cfg file, if that line can be parsed into exactly
+two strings separated by `=` (with any amount of whitespace surrounding the =)
+then it is considered a `key = value` line. The left hand string is the key,
+the right hand is the value.
+
+If the value starts with a `'` or a `"` then the first and last character is
+stripped from the value before being captured.
+
+.Parameter ConfigDir
+Path to the directory that contains the `pyvenv.cfg` file.
+#>
+function Get-PyVenvConfig(
+ [String]
+ $ConfigDir
+) {
+ Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
+
+ # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
+ $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
+
+ # An empty map will be returned if no config file is found.
+ $pyvenvConfig = @{ }
+
+ if ($pyvenvConfigPath) {
+
+ Write-Verbose "File exists, parse `key = value` lines"
+ $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
+
+ $pyvenvConfigContent | ForEach-Object {
+ $keyval = $PSItem -split "\s*=\s*", 2
+ if ($keyval[0] -and $keyval[1]) {
+ $val = $keyval[1]
+
+ # Remove extraneous quotations around a string value.
+ if ("'""".Contains($val.Substring(0, 1))) {
+ $val = $val.Substring(1, $val.Length - 2)
+ }
+
+ $pyvenvConfig[$keyval[0]] = $val
+ Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
+ }
+ }
+ }
+ return $pyvenvConfig
+}
+
+
+<# Begin Activate script --------------------------------------------------- #>
+
+# Determine the containing directory of this script
+$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
+$VenvExecDir = Get-Item -Path $VenvExecPath
+
+Write-Verbose "Activation script is located in path: '$VenvExecPath'"
+Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
+Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
+
+# Set values required in priority: CmdLine, ConfigFile, Default
+# First, get the location of the virtual environment, it might not be
+# VenvExecDir if specified on the command line.
+if ($VenvDir) {
+ Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
+}
+else {
+ Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
+ $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
+ Write-Verbose "VenvDir=$VenvDir"
+}
+
+# Next, read the `pyvenv.cfg` file to determine any required value such
+# as `prompt`.
+$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
+
+# Next, set the prompt from the command line, or the config file, or
+# just use the name of the virtual environment folder.
+if ($Prompt) {
+ Write-Verbose "Prompt specified as argument, using '$Prompt'"
+}
+else {
+ Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
+ if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
+ Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
+ $Prompt = $pyvenvCfg['prompt'];
+ }
+ else {
+ Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
+ Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
+ $Prompt = Split-Path -Path $venvDir -Leaf
+ }
+}
+
+Write-Verbose "Prompt = '$Prompt'"
+Write-Verbose "VenvDir='$VenvDir'"
+
+# Deactivate any currently active virtual environment, but leave the
+# deactivate function in place.
+deactivate -nondestructive
+
+# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
+# that there is an activated venv.
+$env:VIRTUAL_ENV = $VenvDir
+
+if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
+
+ Write-Verbose "Setting prompt to '$Prompt'"
+
+ # Set the prompt to include the env name
+ # Make sure _OLD_VIRTUAL_PROMPT is global
+ function global:_OLD_VIRTUAL_PROMPT { "" }
+ Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
+ New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
+
+ function global:prompt {
+ Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
+ _OLD_VIRTUAL_PROMPT
+ }
+ $env:VIRTUAL_ENV_PROMPT = $Prompt
+}
+
+# Clear PYTHONHOME
+if (Test-Path -Path Env:PYTHONHOME) {
+ Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
+ Remove-Item -Path Env:PYTHONHOME
+}
+
+# Add the venv to the PATH
+Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
+$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
diff --git a/mathesar-venv/bin/activate b/mathesar-venv/bin/activate
new file mode 100644
index 0000000000..78c660df67
--- /dev/null
+++ b/mathesar-venv/bin/activate
@@ -0,0 +1,69 @@
+# This file must be used with "source bin/activate" *from bash*
+# you cannot run it directly
+
+deactivate () {
+ # reset old environment variables
+ if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
+ PATH="${_OLD_VIRTUAL_PATH:-}"
+ export PATH
+ unset _OLD_VIRTUAL_PATH
+ fi
+ if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
+ PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
+ export PYTHONHOME
+ unset _OLD_VIRTUAL_PYTHONHOME
+ fi
+
+ # This should detect bash and zsh, which have a hash command that must
+ # be called to get it to forget past commands. Without forgetting
+ # past commands the $PATH changes we made may not be respected
+ if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
+ hash -r 2> /dev/null
+ fi
+
+ if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
+ PS1="${_OLD_VIRTUAL_PS1:-}"
+ export PS1
+ unset _OLD_VIRTUAL_PS1
+ fi
+
+ unset VIRTUAL_ENV
+ unset VIRTUAL_ENV_PROMPT
+ if [ ! "${1:-}" = "nondestructive" ] ; then
+ # Self destruct!
+ unset -f deactivate
+ fi
+}
+
+# unset irrelevant variables
+deactivate nondestructive
+
+VIRTUAL_ENV="/home/hiten/Desktop/mathesar/mathesar-venv"
+export VIRTUAL_ENV
+
+_OLD_VIRTUAL_PATH="$PATH"
+PATH="$VIRTUAL_ENV/bin:$PATH"
+export PATH
+
+# unset PYTHONHOME if set
+# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
+# could use `if (set -u; : $PYTHONHOME) ;` in bash
+if [ -n "${PYTHONHOME:-}" ] ; then
+ _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
+ unset PYTHONHOME
+fi
+
+if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
+ _OLD_VIRTUAL_PS1="${PS1:-}"
+ PS1="(mathesar-venv) ${PS1:-}"
+ export PS1
+ VIRTUAL_ENV_PROMPT="(mathesar-venv) "
+ export VIRTUAL_ENV_PROMPT
+fi
+
+# This should detect bash and zsh, which have a hash command that must
+# be called to get it to forget past commands. Without forgetting
+# past commands the $PATH changes we made may not be respected
+if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
+ hash -r 2> /dev/null
+fi
diff --git a/mathesar-venv/bin/activate.csh b/mathesar-venv/bin/activate.csh
new file mode 100644
index 0000000000..42aaa6bbcc
--- /dev/null
+++ b/mathesar-venv/bin/activate.csh
@@ -0,0 +1,26 @@
+# This file must be used with "source bin/activate.csh" *from csh*.
+# You cannot run it directly.
+# Created by Davide Di Blasi .
+# Ported to Python 3.3 venv by Andrew Svetlov
+
+alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
+
+# Unset irrelevant variables.
+deactivate nondestructive
+
+setenv VIRTUAL_ENV "/home/hiten/Desktop/mathesar/mathesar-venv"
+
+set _OLD_VIRTUAL_PATH="$PATH"
+setenv PATH "$VIRTUAL_ENV/bin:$PATH"
+
+
+set _OLD_VIRTUAL_PROMPT="$prompt"
+
+if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
+ set prompt = "(mathesar-venv) $prompt"
+ setenv VIRTUAL_ENV_PROMPT "(mathesar-venv) "
+endif
+
+alias pydoc python -m pydoc
+
+rehash
diff --git a/mathesar-venv/bin/activate.fish b/mathesar-venv/bin/activate.fish
new file mode 100644
index 0000000000..e11919044d
--- /dev/null
+++ b/mathesar-venv/bin/activate.fish
@@ -0,0 +1,69 @@
+# This file must be used with "source /bin/activate.fish" *from fish*
+# (https://fishshell.com/); you cannot run it directly.
+
+function deactivate -d "Exit virtual environment and return to normal shell environment"
+ # reset old environment variables
+ if test -n "$_OLD_VIRTUAL_PATH"
+ set -gx PATH $_OLD_VIRTUAL_PATH
+ set -e _OLD_VIRTUAL_PATH
+ end
+ if test -n "$_OLD_VIRTUAL_PYTHONHOME"
+ set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
+ set -e _OLD_VIRTUAL_PYTHONHOME
+ end
+
+ if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
+ set -e _OLD_FISH_PROMPT_OVERRIDE
+ # prevents error when using nested fish instances (Issue #93858)
+ if functions -q _old_fish_prompt
+ functions -e fish_prompt
+ functions -c _old_fish_prompt fish_prompt
+ functions -e _old_fish_prompt
+ end
+ end
+
+ set -e VIRTUAL_ENV
+ set -e VIRTUAL_ENV_PROMPT
+ if test "$argv[1]" != "nondestructive"
+ # Self-destruct!
+ functions -e deactivate
+ end
+end
+
+# Unset irrelevant variables.
+deactivate nondestructive
+
+set -gx VIRTUAL_ENV "/home/hiten/Desktop/mathesar/mathesar-venv"
+
+set -gx _OLD_VIRTUAL_PATH $PATH
+set -gx PATH "$VIRTUAL_ENV/bin" $PATH
+
+# Unset PYTHONHOME if set.
+if set -q PYTHONHOME
+ set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
+ set -e PYTHONHOME
+end
+
+if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
+ # fish uses a function instead of an env var to generate the prompt.
+
+ # Save the current fish_prompt function as the function _old_fish_prompt.
+ functions -c fish_prompt _old_fish_prompt
+
+ # With the original prompt function renamed, we can override with our own.
+ function fish_prompt
+ # Save the return status of the last command.
+ set -l old_status $status
+
+ # Output the venv prompt; color taken from the blue of the Python logo.
+ printf "%s%s%s" (set_color 4B8BBE) "(mathesar-venv) " (set_color normal)
+
+ # Restore the return status of the previous command.
+ echo "exit $old_status" | .
+ # Output the original/"old" prompt.
+ _old_fish_prompt
+ end
+
+ set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
+ set -gx VIRTUAL_ENV_PROMPT "(mathesar-venv) "
+end
diff --git a/mathesar-venv/bin/pip b/mathesar-venv/bin/pip
new file mode 100755
index 0000000000..97301ef602
--- /dev/null
+++ b/mathesar-venv/bin/pip
@@ -0,0 +1,8 @@
+#!/home/hiten/Desktop/mathesar/mathesar-venv/bin/python3
+# -*- coding: utf-8 -*-
+import re
+import sys
+from pip._internal.cli.main import main
+if __name__ == '__main__':
+ sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+ sys.exit(main())
diff --git a/mathesar-venv/bin/pip3 b/mathesar-venv/bin/pip3
new file mode 100755
index 0000000000..97301ef602
--- /dev/null
+++ b/mathesar-venv/bin/pip3
@@ -0,0 +1,8 @@
+#!/home/hiten/Desktop/mathesar/mathesar-venv/bin/python3
+# -*- coding: utf-8 -*-
+import re
+import sys
+from pip._internal.cli.main import main
+if __name__ == '__main__':
+ sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+ sys.exit(main())
diff --git a/mathesar-venv/bin/pip3.10 b/mathesar-venv/bin/pip3.10
new file mode 100755
index 0000000000..97301ef602
--- /dev/null
+++ b/mathesar-venv/bin/pip3.10
@@ -0,0 +1,8 @@
+#!/home/hiten/Desktop/mathesar/mathesar-venv/bin/python3
+# -*- coding: utf-8 -*-
+import re
+import sys
+from pip._internal.cli.main import main
+if __name__ == '__main__':
+ sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
+ sys.exit(main())
diff --git a/mathesar-venv/bin/python b/mathesar-venv/bin/python
new file mode 120000
index 0000000000..b8a0adbbb9
--- /dev/null
+++ b/mathesar-venv/bin/python
@@ -0,0 +1 @@
+python3
\ No newline at end of file
diff --git a/mathesar-venv/bin/python3 b/mathesar-venv/bin/python3
new file mode 120000
index 0000000000..ae65fdaa12
--- /dev/null
+++ b/mathesar-venv/bin/python3
@@ -0,0 +1 @@
+/usr/bin/python3
\ No newline at end of file
diff --git a/mathesar-venv/bin/python3.10 b/mathesar-venv/bin/python3.10
new file mode 120000
index 0000000000..b8a0adbbb9
--- /dev/null
+++ b/mathesar-venv/bin/python3.10
@@ -0,0 +1 @@
+python3
\ No newline at end of file
diff --git a/mathesar-venv/lib64 b/mathesar-venv/lib64
new file mode 120000
index 0000000000..7951405f85
--- /dev/null
+++ b/mathesar-venv/lib64
@@ -0,0 +1 @@
+lib
\ No newline at end of file
diff --git a/mathesar-venv/pyvenv.cfg b/mathesar-venv/pyvenv.cfg
new file mode 100644
index 0000000000..0537ffc00b
--- /dev/null
+++ b/mathesar-venv/pyvenv.cfg
@@ -0,0 +1,3 @@
+home = /usr/bin
+include-system-site-packages = false
+version = 3.10.12
diff --git a/mathesar_ui/src/systems/table-view/header/header-cell/ColumnHeaderContextMenu.svelte b/mathesar_ui/src/systems/table-view/header/header-cell/ColumnHeaderContextMenu.svelte
index 9c9518f7c6..b72e1cf9e2 100644
--- a/mathesar_ui/src/systems/table-view/header/header-cell/ColumnHeaderContextMenu.svelte
+++ b/mathesar_ui/src/systems/table-view/header/header-cell/ColumnHeaderContextMenu.svelte
@@ -1,4 +1,5 @@
{#if linkedTableHref && linkedTableName}
-
+
{$_('open')}
{column.name}
{$_('table')}
From a285b604cab5c9d1c253e1d381ce9e847b65051b Mon Sep 17 00:00:00 2001
From: hitenvidhani
Date: Fri, 5 Apr 2024 20:30:07 +0530
Subject: [PATCH 19/52] Clean Previous Commit
---
**Warning** | 0
Only | 0
mathesar-venv/bin/Activate.ps1 | 247 --------------------------------
mathesar-venv/bin/activate | 69 ---------
mathesar-venv/bin/activate.csh | 26 ----
mathesar-venv/bin/activate.fish | 69 ---------
mathesar-venv/bin/pip | 8 --
mathesar-venv/bin/pip3 | 8 --
mathesar-venv/bin/pip3.10 | 8 --
mathesar-venv/bin/python | 1 -
mathesar-venv/bin/python3 | 1 -
mathesar-venv/bin/python3.10 | 1 -
mathesar-venv/lib64 | 1 -
mathesar-venv/pyvenv.cfg | 3 -
14 files changed, 442 deletions(-)
delete mode 100644 **Warning**
delete mode 100644 Only
delete mode 100644 mathesar-venv/bin/Activate.ps1
delete mode 100644 mathesar-venv/bin/activate
delete mode 100644 mathesar-venv/bin/activate.csh
delete mode 100644 mathesar-venv/bin/activate.fish
delete mode 100755 mathesar-venv/bin/pip
delete mode 100755 mathesar-venv/bin/pip3
delete mode 100755 mathesar-venv/bin/pip3.10
delete mode 120000 mathesar-venv/bin/python
delete mode 120000 mathesar-venv/bin/python3
delete mode 120000 mathesar-venv/bin/python3.10
delete mode 120000 mathesar-venv/lib64
delete mode 100644 mathesar-venv/pyvenv.cfg
diff --git a/**Warning** b/**Warning**
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/Only b/Only
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/mathesar-venv/bin/Activate.ps1 b/mathesar-venv/bin/Activate.ps1
deleted file mode 100644
index eeea3583fa..0000000000
--- a/mathesar-venv/bin/Activate.ps1
+++ /dev/null
@@ -1,247 +0,0 @@
-<#
-.Synopsis
-Activate a Python virtual environment for the current PowerShell session.
-
-.Description
-Pushes the python executable for a virtual environment to the front of the
-$Env:PATH environment variable and sets the prompt to signify that you are
-in a Python virtual environment. Makes use of the command line switches as
-well as the `pyvenv.cfg` file values present in the virtual environment.
-
-.Parameter VenvDir
-Path to the directory that contains the virtual environment to activate. The
-default value for this is the parent of the directory that the Activate.ps1
-script is located within.
-
-.Parameter Prompt
-The prompt prefix to display when this virtual environment is activated. By
-default, this prompt is the name of the virtual environment folder (VenvDir)
-surrounded by parentheses and followed by a single space (ie. '(.venv) ').
-
-.Example
-Activate.ps1
-Activates the Python virtual environment that contains the Activate.ps1 script.
-
-.Example
-Activate.ps1 -Verbose
-Activates the Python virtual environment that contains the Activate.ps1 script,
-and shows extra information about the activation as it executes.
-
-.Example
-Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
-Activates the Python virtual environment located in the specified location.
-
-.Example
-Activate.ps1 -Prompt "MyPython"
-Activates the Python virtual environment that contains the Activate.ps1 script,
-and prefixes the current prompt with the specified string (surrounded in
-parentheses) while the virtual environment is active.
-
-.Notes
-On Windows, it may be required to enable this Activate.ps1 script by setting the
-execution policy for the user. You can do this by issuing the following PowerShell
-command:
-
-PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
-
-For more information on Execution Policies:
-https://go.microsoft.com/fwlink/?LinkID=135170
-
-#>
-Param(
- [Parameter(Mandatory = $false)]
- [String]
- $VenvDir,
- [Parameter(Mandatory = $false)]
- [String]
- $Prompt
-)
-
-<# Function declarations --------------------------------------------------- #>
-
-<#
-.Synopsis
-Remove all shell session elements added by the Activate script, including the
-addition of the virtual environment's Python executable from the beginning of
-the PATH variable.
-
-.Parameter NonDestructive
-If present, do not remove this function from the global namespace for the
-session.
-
-#>
-function global:deactivate ([switch]$NonDestructive) {
- # Revert to original values
-
- # The prior prompt:
- if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
- Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
- Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
- }
-
- # The prior PYTHONHOME:
- if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
- Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
- Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
- }
-
- # The prior PATH:
- if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
- Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
- Remove-Item -Path Env:_OLD_VIRTUAL_PATH
- }
-
- # Just remove the VIRTUAL_ENV altogether:
- if (Test-Path -Path Env:VIRTUAL_ENV) {
- Remove-Item -Path env:VIRTUAL_ENV
- }
-
- # Just remove VIRTUAL_ENV_PROMPT altogether.
- if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
- Remove-Item -Path env:VIRTUAL_ENV_PROMPT
- }
-
- # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
- if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
- Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
- }
-
- # Leave deactivate function in the global namespace if requested:
- if (-not $NonDestructive) {
- Remove-Item -Path function:deactivate
- }
-}
-
-<#
-.Description
-Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
-given folder, and returns them in a map.
-
-For each line in the pyvenv.cfg file, if that line can be parsed into exactly
-two strings separated by `=` (with any amount of whitespace surrounding the =)
-then it is considered a `key = value` line. The left hand string is the key,
-the right hand is the value.
-
-If the value starts with a `'` or a `"` then the first and last character is
-stripped from the value before being captured.
-
-.Parameter ConfigDir
-Path to the directory that contains the `pyvenv.cfg` file.
-#>
-function Get-PyVenvConfig(
- [String]
- $ConfigDir
-) {
- Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
-
- # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
- $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
-
- # An empty map will be returned if no config file is found.
- $pyvenvConfig = @{ }
-
- if ($pyvenvConfigPath) {
-
- Write-Verbose "File exists, parse `key = value` lines"
- $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
-
- $pyvenvConfigContent | ForEach-Object {
- $keyval = $PSItem -split "\s*=\s*", 2
- if ($keyval[0] -and $keyval[1]) {
- $val = $keyval[1]
-
- # Remove extraneous quotations around a string value.
- if ("'""".Contains($val.Substring(0, 1))) {
- $val = $val.Substring(1, $val.Length - 2)
- }
-
- $pyvenvConfig[$keyval[0]] = $val
- Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
- }
- }
- }
- return $pyvenvConfig
-}
-
-
-<# Begin Activate script --------------------------------------------------- #>
-
-# Determine the containing directory of this script
-$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
-$VenvExecDir = Get-Item -Path $VenvExecPath
-
-Write-Verbose "Activation script is located in path: '$VenvExecPath'"
-Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
-Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
-
-# Set values required in priority: CmdLine, ConfigFile, Default
-# First, get the location of the virtual environment, it might not be
-# VenvExecDir if specified on the command line.
-if ($VenvDir) {
- Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
-}
-else {
- Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
- $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
- Write-Verbose "VenvDir=$VenvDir"
-}
-
-# Next, read the `pyvenv.cfg` file to determine any required value such
-# as `prompt`.
-$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
-
-# Next, set the prompt from the command line, or the config file, or
-# just use the name of the virtual environment folder.
-if ($Prompt) {
- Write-Verbose "Prompt specified as argument, using '$Prompt'"
-}
-else {
- Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
- if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
- Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
- $Prompt = $pyvenvCfg['prompt'];
- }
- else {
- Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
- Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
- $Prompt = Split-Path -Path $venvDir -Leaf
- }
-}
-
-Write-Verbose "Prompt = '$Prompt'"
-Write-Verbose "VenvDir='$VenvDir'"
-
-# Deactivate any currently active virtual environment, but leave the
-# deactivate function in place.
-deactivate -nondestructive
-
-# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
-# that there is an activated venv.
-$env:VIRTUAL_ENV = $VenvDir
-
-if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
-
- Write-Verbose "Setting prompt to '$Prompt'"
-
- # Set the prompt to include the env name
- # Make sure _OLD_VIRTUAL_PROMPT is global
- function global:_OLD_VIRTUAL_PROMPT { "" }
- Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
- New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
-
- function global:prompt {
- Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
- _OLD_VIRTUAL_PROMPT
- }
- $env:VIRTUAL_ENV_PROMPT = $Prompt
-}
-
-# Clear PYTHONHOME
-if (Test-Path -Path Env:PYTHONHOME) {
- Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
- Remove-Item -Path Env:PYTHONHOME
-}
-
-# Add the venv to the PATH
-Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
-$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
diff --git a/mathesar-venv/bin/activate b/mathesar-venv/bin/activate
deleted file mode 100644
index 78c660df67..0000000000
--- a/mathesar-venv/bin/activate
+++ /dev/null
@@ -1,69 +0,0 @@
-# This file must be used with "source bin/activate" *from bash*
-# you cannot run it directly
-
-deactivate () {
- # reset old environment variables
- if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
- PATH="${_OLD_VIRTUAL_PATH:-}"
- export PATH
- unset _OLD_VIRTUAL_PATH
- fi
- if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
- PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
- export PYTHONHOME
- unset _OLD_VIRTUAL_PYTHONHOME
- fi
-
- # This should detect bash and zsh, which have a hash command that must
- # be called to get it to forget past commands. Without forgetting
- # past commands the $PATH changes we made may not be respected
- if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
- hash -r 2> /dev/null
- fi
-
- if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
- PS1="${_OLD_VIRTUAL_PS1:-}"
- export PS1
- unset _OLD_VIRTUAL_PS1
- fi
-
- unset VIRTUAL_ENV
- unset VIRTUAL_ENV_PROMPT
- if [ ! "${1:-}" = "nondestructive" ] ; then
- # Self destruct!
- unset -f deactivate
- fi
-}
-
-# unset irrelevant variables
-deactivate nondestructive
-
-VIRTUAL_ENV="/home/hiten/Desktop/mathesar/mathesar-venv"
-export VIRTUAL_ENV
-
-_OLD_VIRTUAL_PATH="$PATH"
-PATH="$VIRTUAL_ENV/bin:$PATH"
-export PATH
-
-# unset PYTHONHOME if set
-# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
-# could use `if (set -u; : $PYTHONHOME) ;` in bash
-if [ -n "${PYTHONHOME:-}" ] ; then
- _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
- unset PYTHONHOME
-fi
-
-if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
- _OLD_VIRTUAL_PS1="${PS1:-}"
- PS1="(mathesar-venv) ${PS1:-}"
- export PS1
- VIRTUAL_ENV_PROMPT="(mathesar-venv) "
- export VIRTUAL_ENV_PROMPT
-fi
-
-# This should detect bash and zsh, which have a hash command that must
-# be called to get it to forget past commands. Without forgetting
-# past commands the $PATH changes we made may not be respected
-if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
- hash -r 2> /dev/null
-fi
diff --git a/mathesar-venv/bin/activate.csh b/mathesar-venv/bin/activate.csh
deleted file mode 100644
index 42aaa6bbcc..0000000000
--- a/mathesar-venv/bin/activate.csh
+++ /dev/null
@@ -1,26 +0,0 @@
-# This file must be used with "source bin/activate.csh" *from csh*.
-# You cannot run it directly.
-# Created by Davide Di Blasi .
-# Ported to Python 3.3 venv by Andrew Svetlov
-
-alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
-
-# Unset irrelevant variables.
-deactivate nondestructive
-
-setenv VIRTUAL_ENV "/home/hiten/Desktop/mathesar/mathesar-venv"
-
-set _OLD_VIRTUAL_PATH="$PATH"
-setenv PATH "$VIRTUAL_ENV/bin:$PATH"
-
-
-set _OLD_VIRTUAL_PROMPT="$prompt"
-
-if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
- set prompt = "(mathesar-venv) $prompt"
- setenv VIRTUAL_ENV_PROMPT "(mathesar-venv) "
-endif
-
-alias pydoc python -m pydoc
-
-rehash
diff --git a/mathesar-venv/bin/activate.fish b/mathesar-venv/bin/activate.fish
deleted file mode 100644
index e11919044d..0000000000
--- a/mathesar-venv/bin/activate.fish
+++ /dev/null
@@ -1,69 +0,0 @@
-# This file must be used with "source /bin/activate.fish" *from fish*
-# (https://fishshell.com/); you cannot run it directly.
-
-function deactivate -d "Exit virtual environment and return to normal shell environment"
- # reset old environment variables
- if test -n "$_OLD_VIRTUAL_PATH"
- set -gx PATH $_OLD_VIRTUAL_PATH
- set -e _OLD_VIRTUAL_PATH
- end
- if test -n "$_OLD_VIRTUAL_PYTHONHOME"
- set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
- set -e _OLD_VIRTUAL_PYTHONHOME
- end
-
- if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
- set -e _OLD_FISH_PROMPT_OVERRIDE
- # prevents error when using nested fish instances (Issue #93858)
- if functions -q _old_fish_prompt
- functions -e fish_prompt
- functions -c _old_fish_prompt fish_prompt
- functions -e _old_fish_prompt
- end
- end
-
- set -e VIRTUAL_ENV
- set -e VIRTUAL_ENV_PROMPT
- if test "$argv[1]" != "nondestructive"
- # Self-destruct!
- functions -e deactivate
- end
-end
-
-# Unset irrelevant variables.
-deactivate nondestructive
-
-set -gx VIRTUAL_ENV "/home/hiten/Desktop/mathesar/mathesar-venv"
-
-set -gx _OLD_VIRTUAL_PATH $PATH
-set -gx PATH "$VIRTUAL_ENV/bin" $PATH
-
-# Unset PYTHONHOME if set.
-if set -q PYTHONHOME
- set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
- set -e PYTHONHOME
-end
-
-if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
- # fish uses a function instead of an env var to generate the prompt.
-
- # Save the current fish_prompt function as the function _old_fish_prompt.
- functions -c fish_prompt _old_fish_prompt
-
- # With the original prompt function renamed, we can override with our own.
- function fish_prompt
- # Save the return status of the last command.
- set -l old_status $status
-
- # Output the venv prompt; color taken from the blue of the Python logo.
- printf "%s%s%s" (set_color 4B8BBE) "(mathesar-venv) " (set_color normal)
-
- # Restore the return status of the previous command.
- echo "exit $old_status" | .
- # Output the original/"old" prompt.
- _old_fish_prompt
- end
-
- set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
- set -gx VIRTUAL_ENV_PROMPT "(mathesar-venv) "
-end
diff --git a/mathesar-venv/bin/pip b/mathesar-venv/bin/pip
deleted file mode 100755
index 97301ef602..0000000000
--- a/mathesar-venv/bin/pip
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/home/hiten/Desktop/mathesar/mathesar-venv/bin/python3
-# -*- coding: utf-8 -*-
-import re
-import sys
-from pip._internal.cli.main import main
-if __name__ == '__main__':
- sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
- sys.exit(main())
diff --git a/mathesar-venv/bin/pip3 b/mathesar-venv/bin/pip3
deleted file mode 100755
index 97301ef602..0000000000
--- a/mathesar-venv/bin/pip3
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/home/hiten/Desktop/mathesar/mathesar-venv/bin/python3
-# -*- coding: utf-8 -*-
-import re
-import sys
-from pip._internal.cli.main import main
-if __name__ == '__main__':
- sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
- sys.exit(main())
diff --git a/mathesar-venv/bin/pip3.10 b/mathesar-venv/bin/pip3.10
deleted file mode 100755
index 97301ef602..0000000000
--- a/mathesar-venv/bin/pip3.10
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/home/hiten/Desktop/mathesar/mathesar-venv/bin/python3
-# -*- coding: utf-8 -*-
-import re
-import sys
-from pip._internal.cli.main import main
-if __name__ == '__main__':
- sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
- sys.exit(main())
diff --git a/mathesar-venv/bin/python b/mathesar-venv/bin/python
deleted file mode 120000
index b8a0adbbb9..0000000000
--- a/mathesar-venv/bin/python
+++ /dev/null
@@ -1 +0,0 @@
-python3
\ No newline at end of file
diff --git a/mathesar-venv/bin/python3 b/mathesar-venv/bin/python3
deleted file mode 120000
index ae65fdaa12..0000000000
--- a/mathesar-venv/bin/python3
+++ /dev/null
@@ -1 +0,0 @@
-/usr/bin/python3
\ No newline at end of file
diff --git a/mathesar-venv/bin/python3.10 b/mathesar-venv/bin/python3.10
deleted file mode 120000
index b8a0adbbb9..0000000000
--- a/mathesar-venv/bin/python3.10
+++ /dev/null
@@ -1 +0,0 @@
-python3
\ No newline at end of file
diff --git a/mathesar-venv/lib64 b/mathesar-venv/lib64
deleted file mode 120000
index 7951405f85..0000000000
--- a/mathesar-venv/lib64
+++ /dev/null
@@ -1 +0,0 @@
-lib
\ No newline at end of file
diff --git a/mathesar-venv/pyvenv.cfg b/mathesar-venv/pyvenv.cfg
deleted file mode 100644
index 0537ffc00b..0000000000
--- a/mathesar-venv/pyvenv.cfg
+++ /dev/null
@@ -1,3 +0,0 @@
-home = /usr/bin
-include-system-site-packages = false
-version = 3.10.12
From ddaeaedd7d480b7562edd45dd9355817e0ea2c6b Mon Sep 17 00:00:00 2001
From: ishir21
Date: Sat, 6 Apr 2024 19:40:34 +0530
Subject: [PATCH 20/52] removed the if statement
---
.env.example | 2 -
.github/workflows/create-draft-release.yml | 42 +
.github/workflows/test-and-lint-code.yml | 56 +-
Caddyfile | 23 +-
Dockerfile | 93 +-
Dockerfile.integ-tests | 2 +-
README.md | 1 -
config/context_processors.py | 58 +-
config/settings/common_settings.py | 22 +-
config/settings/production.py | 8 +
db/install.py | 66 +-
db/records/exceptions.py | 6 +-
db/records/operations/select.py | 2 -
db/sql/0_msar.sql | 39 +
db/sql/test_0_msar.sql | 22 +
demo/install/base.py | 4 +-
demo/install/datasets.py | 39 +-
demo/install/dumpcsvs.py | 40 +
demo/install/library_dataset.py | 29 +
demo/install/movies_dataset.py | 29 +
.../resources/movie_collection.sql.bz2 | Bin 6375862 -> 0 bytes
.../resources/movie_collection_fks.sql | 105 +
.../resources/movie_collection_tables.sql | 339 +
.../resources/movies_csv/Departments.csv | 13 +
demo/install/resources/movies_csv/Genres.csv | 21 +
demo/install/resources/movies_csv/Jobs.csv | 420 +
.../resources/movies_csv/Movie Cast Map.csv | 159013 +++++++++++++++
.../resources/movies_csv/Movie Crew Map.csv | 130712 ++++++++++++
.../resources/movies_csv/Movie Genre Map.csv | 25887 +++
.../Movie Production Company Map.csv | 19960 ++
.../Movie Production Country Map.csv | 14088 ++
.../movies_csv/Movie Spoken Language Map.csv | 14952 ++
demo/install/resources/movies_csv/Movies.csv | 13110 ++
demo/install/resources/movies_csv/People.csv | 152698 ++++++++++++++
.../movies_csv/Production Companies.csv | 9878 +
.../movies_csv/Production Countries.csv | 123 +
.../resources/movies_csv/Spoken Languages.csv | 108 +
.../resources/movies_csv/Sub-Collections.csv | 813 +
.../commands/setup_demo_template_db.py | 24 +-
demo/settings.py | 10 +-
demo/urls.py | 1 +
dev-run.sh | 9 -
docker-compose.dev.yml | 50 +-
docker-compose.yml | 302 +-
docs/docs/administration/debug.md | 44 +
docs/docs/administration/uninstall.md | 99 +-
docs/docs/administration/upgrade.md | 19 -
docs/docs/administration/upgrade/0.1.4.md | 207 +
docs/docs/administration/upgrade/0.1.5.md | 17 +
docs/docs/administration/upgrade/0.1.6.md | 86 +
docs/docs/administration/upgrade/older.md | 16 +
.../configuration/connect-to-existing-db.md | 92 -
.../configuration/customize-docker-compose.md | 99 -
docs/docs/configuration/env-variables.md | 78 +-
docs/docs/index.md | 59 +-
.../installation/build-from-source/index.md | 294 +-
.../docs/installation/docker-compose/index.md | 147 +-
docs/docs/installation/docker/index.md | 166 -
.../docs/installation/guided-install/index.md | 44 -
.../guided-install/troubleshooting.md | 64 -
.../guided-install/under-the-hood.md | 75 -
docs/docs/releases/.gitignore | 2 +
docs/docs/releases/0.1.0.md | 16 +
docs/docs/releases/0.1.1.md | 24 +
docs/docs/releases/0.1.2.md | 42 +
docs/docs/releases/0.1.3.md | 147 +
docs/docs/releases/0.1.4.md | 145 +
docs/docs/releases/0.1.5.md | 37 +
docs/docs/releases/0.1.6.md | 76 +
docs/docs/releases/README.md | 20 +
docs/docs/releases/TEMPLATE.md | 35 +
docs/docs/releases/find_missing_prs.sh | 126 +
.../snippets/docker-compose-administration.md | 86 -
.../snippets/docker-compose-prerequisites.md | 24 -
docs/mkdocs.yml | 40 +-
docs/placeholder-plugin.yaml | 1 +
install.sh | 453 -
mathesar/__init__.py | 2 +-
mathesar/api/db/viewsets/databases.py | 2 +-
mathesar/api/db/viewsets/records.py | 2 -
mathesar/api/db/viewsets/tables.py | 6 +-
.../database_exceptions/base_exceptions.py | 16 +
mathesar/api/exceptions/error_codes.py | 3 +
.../validation_exceptions/exceptions.py | 24 +
mathesar/api/serializers/dependents.py | 2 +-
mathesar/api/serializers/table_settings.py | 11 +-
mathesar/api/ui/permissions/ui_database.py | 15 +-
mathesar/api/ui/viewsets/databases.py | 95 +-
mathesar/exception_handlers.py | 8 +-
mathesar/install.py | 66 +-
.../migrations/0005_datafile_sheet_name.py | 18 -
mathesar/migrations/0005_release_0_1_4.py | 94 +
.../migrations/0005_user_display_language.py | 18 -
.../migrations/0006_auto_20230906_0413.py | 22 -
.../0006_mathesar_databases_to_model.py | 34 +
.../migrations/0007_auto_20230913_1912.py | 20 -
.../migrations/0008_auto_20230921_1834.py | 49 -
.../migrations/0009_merge_20231025_1733.py | 14 -
mathesar/migrations/0010_remove_editable.py | 17 -
mathesar/models/base.py | 42 +-
mathesar/models/query.py | 138 +-
mathesar/state/django.py | 2 +-
mathesar/templates/mathesar/index.html | 32 +-
mathesar/templates/mathesar/login_base.html | 3 +-
mathesar/tests/api/test_database_api.py | 29 +
mathesar/tests/api/test_record_api.py | 18 +-
mathesar/tests/api/test_table_settings_api.py | 16 +
mathesar/tests/conftest.py | 52 +-
mathesar/utils/connections.py | 113 +
mathesar/utils/frontend.py | 4 +-
mathesar/views.py | 20 +-
mathesar_ui/.eslintrc.cjs | 17 +-
mathesar_ui/src/App.svelte | 31 +-
mathesar_ui/src/AppTypes.ts | 2 +-
mathesar_ui/src/api/connections.ts | 77 +-
mathesar_ui/src/api/queryShares.ts | 2 +-
mathesar_ui/src/api/upgrade.ts | 8 -
.../src/component-library/button/Button.scss | 12 +
.../checkbox-group/CheckboxGroup.svelte | 16 +-
.../common/utils/typeUtils.ts | 10 +
.../component-library/dropdown/Dropdown.scss | 5 +
.../fieldset-group/FieldsetGroup.scss | 8 +-
.../fieldset-group/FieldsetGroup.svelte | 20 +-
mathesar_ui/src/component-library/index.ts | 1 +
.../labeled-input/LabeledInput.scss | 30 +-
.../labeled-input/LabeledInput.svelte | 15 +-
.../list-box/ListBoxTypes.ts | 2 +-
.../margin-trim/MarginTrim.svelte | 16 +
.../radio-group/RadioGroup.svelte | 2 +-
.../component-library/render/Render.svelte | 32 +
.../render/RenderComponentWithProps.svelte | 21 +
.../component-library/select/Select.svelte | 4 +-
.../component-library/select/SelectTypes.ts | 2 +-
.../StringOrComponent.svelte | 6 +
mathesar_ui/src/components/AppHeader.svelte | 2 +-
.../src/components/EntityPageHeader.svelte | 9 +-
.../src/components/GrowableTextArea.svelte | 53 +
.../src/components/LiveDemoBanner.svelte | 2 +-
.../DbTypeIndicator.svelte | 7 +-
.../breadcrumb/DatabaseSelector.svelte | 10 +-
.../cell-fabric/data-types/string.ts | 17 +-
mathesar_ui/src/components/form/Field.svelte | 8 +-
.../src/components/form/FieldHelp.svelte | 10 +
.../src/components/form/GridFormInput.svelte | 6 +
.../components/form/GridFormInputRow.svelte | 6 +
.../src/components/grid-form/GridForm.svelte | 40 +
.../grid-form/GridFormDivider.svelte | 9 +
.../grid-form/GridFormLabelRow.svelte | 28 +
mathesar_ui/src/components/grid-form/index.ts | 3 +
mathesar_ui/src/components/sheet/Sheet.svelte | 14 +-
mathesar_ui/src/i18n/index.ts | 11 +-
mathesar_ui/src/i18n/languages/en/dict.json | 170 +-
mathesar_ui/src/i18n/languages/ja/dict.json | 599 +-
mathesar_ui/src/pages/ErrorPage.svelte | 9 +-
.../src/pages/admin-update/ReleaseBox.svelte | 55 +-
.../admin-update/SoftwareUpdateContent.svelte | 20 +-
.../admin-update/SoftwareUpdatePage.svelte | 7 +-
.../upgrade-modal/UpgradeConfirm.svelte | 54 -
.../upgrade-modal/UpgradeError.svelte | 22 -
.../upgrade-modal/UpgradeModal.svelte | 92 -
.../upgrade-modal/UpgradeProcessing.svelte | 28 -
.../pages/admin-users/AdminNavigation.svelte | 7 +-
.../src/pages/admin-users/EditUserPage.svelte | 12 +-
.../src/pages/admin-users/NewUserPage.svelte | 5 +-
.../pages/admin-users/UserListingPage.svelte | 33 +-
.../pages/connections/ConnectionsPage.svelte | 48 +-
.../data-explorer/DataExplorerPage.svelte | 3 +-
.../pages/database/AddEditSchemaModal.svelte | 21 +-
.../src/pages/database/DatabaseDetails.svelte | 80 +-
.../database/DbAccessControlModal.svelte | 10 +-
.../database/SchemaConstituentCounts.svelte | 6 +-
.../pages/database/SchemaListSkeleton.svelte | 29 +
.../src/pages/database/SchemaRow.svelte | 12 +-
.../pages/database/__help__/databaseHelp.ts | 4 -
.../pages/exploration/ExplorationPage.svelte | 4 +-
.../src/pages/exploration/Header.svelte | 5 +-
.../ShareExplorationDropdown.svelte | 12 +-
.../ColumnNamingStrategyInput.svelte | 4 +-
.../column-names/LabelNoHeaderRow.svelte | 3 +-
.../column-names/LabelYesHeaderRow.svelte | 3 +-
.../inference/ColumnTypeInferenceInput.svelte | 4 +-
.../inference/LabelInferenceFalse.svelte | 5 +-
.../inference/LabelInferenceTrue.svelte | 5 +-
.../src/pages/import/preview/ErrorInfo.svelte | 11 +-
.../preview/ImportPreviewContent.svelte | 22 +-
.../import/preview/ImportPreviewLayout.svelte | 3 +-
.../import/preview/ImportPreviewPage.svelte | 6 +-
.../pages/import/upload/DataFileInput.svelte | 6 +-
.../import/upload/ImportUploadPage.svelte | 26 +-
mathesar_ui/src/pages/pageTitleUtils.ts | 3 +-
.../src/pages/record/DirectField.svelte | 10 +-
.../src/pages/record/RecordPageContent.svelte | 13 +-
mathesar_ui/src/pages/record/Widgets.svelte | 18 +-
.../CreateNewExplorationTutorial.svelte | 17 +-
.../pages/schema/CreateNewTableButton.svelte | 12 +-
.../schema/CreateNewTableTutorial.svelte | 16 +-
mathesar_ui/src/pages/schema/EditTable.svelte | 10 +-
.../src/pages/schema/ExplorationItem.svelte | 5 +-
.../src/pages/schema/ExplorationsList.svelte | 3 +-
.../schema/SchemaAccessControlModal.svelte | 10 +-
.../pages/schema/SchemaExplorations.svelte | 22 +-
.../src/pages/schema/SchemaOverview.svelte | 19 +-
.../src/pages/schema/SchemaPage.svelte | 11 +-
.../src/pages/schema/SchemaTables.svelte | 18 +-
mathesar_ui/src/pages/schema/TableCard.svelte | 57 +-
.../src/pages/schema/TablesList.svelte | 3 +-
.../src/pages/user-profile/ProfilePage.svelte | 20 +-
mathesar_ui/src/routes/AdminRoute.svelte | 8 +-
.../src/routes/AuthenticatedRoutes.svelte | 29 +-
.../src/routes/DataExplorerRoute.svelte | 9 +-
mathesar_ui/src/routes/DatabaseRoute.svelte | 17 +-
.../src/routes/ExplorationRoute.svelte | 3 +-
mathesar_ui/src/routes/ImportRoute.svelte | 4 +-
mathesar_ui/src/routes/RootRoute.svelte | 5 +-
mathesar_ui/src/routes/SchemaRoute.svelte | 4 +-
.../src/routes/SharedExplorationRoute.svelte | 3 +-
.../src/routes/SharedTableRoute.svelte | 3 +-
mathesar_ui/src/routes/TableRoute.svelte | 4 +-
.../src/routes/UserProfileRoute.svelte | 3 +-
mathesar_ui/src/routes/UsersRoute.svelte | 3 +-
.../src/stores/abstract-types/store.ts | 2 +-
mathesar_ui/src/stores/databases.ts | 174 +-
mathesar_ui/src/stores/queries.ts | 4 +-
mathesar_ui/src/stores/releases.ts | 2 -
mathesar_ui/src/stores/schemas.ts | 34 +-
mathesar_ui/src/stores/tables.ts | 4 +-
.../systems/connections/AddConnection.svelte | 289 +
.../connections/AddConnectionModal.svelte | 25 +-
.../connections/DeleteConnectionModal.svelte | 21 +-
.../connections/EditConnectionModal.svelte | 176 +-
.../connections/GeneralConnection.svelte | 43 +
.../systems/connections/generalConnections.ts | 76 +
.../data-explorer/__tests__/utils.test.ts | 6 +
.../exploration-inspector/CellTab.svelte | 2 +-
.../RecordSelectorColumnHeaderCell.svelte | 1 +
.../systems/table-view/row/GroupHeader.svelte | 2 +-
.../table-view/row/NewRecordMessage.svelte | 2 +-
.../table-inspector/cell/CellMode.svelte | 2 +-
.../column/ColumnTypeSpecifierTag.svelte | 2 +-
.../ExtractColumnsModal.svelte | 24 +-
.../SelectDisplayLanguage.svelte | 2 +-
.../UserDetailsForm.svelte | 11 +-
mathesar_ui/src/utils/iterUtils.ts | 19 +
mathesar_ui/src/utils/preloadData.ts | 8 +-
pyproject.toml | 2 +-
release-scripts/debian/changelog | 6 +-
requirements-dev.txt | 2 +-
requirements.txt | 13 +-
run.sh | 15 +-
translations/ja/LC_MESSAGES/django.po | 170 +
250 files changed, 547627 insertions(+), 2999 deletions(-)
create mode 100644 .github/workflows/create-draft-release.yml
create mode 100644 demo/install/dumpcsvs.py
create mode 100644 demo/install/library_dataset.py
create mode 100644 demo/install/movies_dataset.py
delete mode 100644 demo/install/resources/movie_collection.sql.bz2
create mode 100644 demo/install/resources/movie_collection_fks.sql
create mode 100644 demo/install/resources/movie_collection_tables.sql
create mode 100644 demo/install/resources/movies_csv/Departments.csv
create mode 100644 demo/install/resources/movies_csv/Genres.csv
create mode 100644 demo/install/resources/movies_csv/Jobs.csv
create mode 100644 demo/install/resources/movies_csv/Movie Cast Map.csv
create mode 100644 demo/install/resources/movies_csv/Movie Crew Map.csv
create mode 100644 demo/install/resources/movies_csv/Movie Genre Map.csv
create mode 100644 demo/install/resources/movies_csv/Movie Production Company Map.csv
create mode 100644 demo/install/resources/movies_csv/Movie Production Country Map.csv
create mode 100644 demo/install/resources/movies_csv/Movie Spoken Language Map.csv
create mode 100644 demo/install/resources/movies_csv/Movies.csv
create mode 100644 demo/install/resources/movies_csv/People.csv
create mode 100644 demo/install/resources/movies_csv/Production Companies.csv
create mode 100644 demo/install/resources/movies_csv/Production Countries.csv
create mode 100644 demo/install/resources/movies_csv/Spoken Languages.csv
create mode 100644 demo/install/resources/movies_csv/Sub-Collections.csv
create mode 100644 docs/docs/administration/debug.md
delete mode 100644 docs/docs/administration/upgrade.md
create mode 100644 docs/docs/administration/upgrade/0.1.4.md
create mode 100644 docs/docs/administration/upgrade/0.1.5.md
create mode 100644 docs/docs/administration/upgrade/0.1.6.md
create mode 100644 docs/docs/administration/upgrade/older.md
delete mode 100644 docs/docs/configuration/connect-to-existing-db.md
delete mode 100644 docs/docs/configuration/customize-docker-compose.md
delete mode 100644 docs/docs/installation/docker/index.md
delete mode 100644 docs/docs/installation/guided-install/index.md
delete mode 100644 docs/docs/installation/guided-install/troubleshooting.md
delete mode 100644 docs/docs/installation/guided-install/under-the-hood.md
create mode 100644 docs/docs/releases/.gitignore
create mode 100644 docs/docs/releases/0.1.0.md
create mode 100644 docs/docs/releases/0.1.1.md
create mode 100644 docs/docs/releases/0.1.2.md
create mode 100644 docs/docs/releases/0.1.3.md
create mode 100644 docs/docs/releases/0.1.4.md
create mode 100644 docs/docs/releases/0.1.5.md
create mode 100644 docs/docs/releases/0.1.6.md
create mode 100644 docs/docs/releases/README.md
create mode 100644 docs/docs/releases/TEMPLATE.md
create mode 100755 docs/docs/releases/find_missing_prs.sh
delete mode 100644 docs/docs/snippets/docker-compose-administration.md
delete mode 100755 install.sh
delete mode 100644 mathesar/migrations/0005_datafile_sheet_name.py
create mode 100644 mathesar/migrations/0005_release_0_1_4.py
delete mode 100644 mathesar/migrations/0005_user_display_language.py
delete mode 100644 mathesar/migrations/0006_auto_20230906_0413.py
create mode 100644 mathesar/migrations/0006_mathesar_databases_to_model.py
delete mode 100644 mathesar/migrations/0007_auto_20230913_1912.py
delete mode 100644 mathesar/migrations/0008_auto_20230921_1834.py
delete mode 100644 mathesar/migrations/0009_merge_20231025_1733.py
delete mode 100644 mathesar/migrations/0010_remove_editable.py
create mode 100644 mathesar/utils/connections.py
delete mode 100644 mathesar_ui/src/api/upgrade.ts
create mode 100644 mathesar_ui/src/component-library/margin-trim/MarginTrim.svelte
create mode 100644 mathesar_ui/src/component-library/render/Render.svelte
create mode 100644 mathesar_ui/src/component-library/render/RenderComponentWithProps.svelte
create mode 100644 mathesar_ui/src/components/GrowableTextArea.svelte
create mode 100644 mathesar_ui/src/components/form/FieldHelp.svelte
create mode 100644 mathesar_ui/src/components/grid-form/GridForm.svelte
create mode 100644 mathesar_ui/src/components/grid-form/GridFormDivider.svelte
create mode 100644 mathesar_ui/src/components/grid-form/GridFormLabelRow.svelte
create mode 100644 mathesar_ui/src/components/grid-form/index.ts
delete mode 100644 mathesar_ui/src/pages/admin-update/upgrade-modal/UpgradeConfirm.svelte
delete mode 100644 mathesar_ui/src/pages/admin-update/upgrade-modal/UpgradeError.svelte
delete mode 100644 mathesar_ui/src/pages/admin-update/upgrade-modal/UpgradeModal.svelte
delete mode 100644 mathesar_ui/src/pages/admin-update/upgrade-modal/UpgradeProcessing.svelte
create mode 100644 mathesar_ui/src/pages/database/SchemaListSkeleton.svelte
delete mode 100644 mathesar_ui/src/pages/database/__help__/databaseHelp.ts
create mode 100644 mathesar_ui/src/systems/connections/AddConnection.svelte
create mode 100644 mathesar_ui/src/systems/connections/GeneralConnection.svelte
create mode 100644 mathesar_ui/src/systems/connections/generalConnections.ts
create mode 100644 mathesar_ui/src/utils/iterUtils.ts
diff --git a/.env.example b/.env.example
index 7e8e2933aa..f002e016f1 100644
--- a/.env.example
+++ b/.env.example
@@ -1,6 +1,4 @@
ALLOWED_HOSTS='.localhost, 127.0.0.1, [::1]'
SECRET_KEY=2gr6ud88x=(p855_5nbj_+7^bw-iz&n7ldqv%94mjaecl+b9=4
-DJANGO_DATABASE_URL=postgres://mathesar:mathesar@mathesar_db:5432/mathesar_django
-MATHESAR_DATABASES=(mathesar_tables|postgresql://mathesar:mathesar@mathesar_db:5432/mathesar)
## Uncomment the setting below to put Mathesar in 'demo mode'
# DJANGO_SETTINGS_MODULE=demo.settings
diff --git a/.github/workflows/create-draft-release.yml b/.github/workflows/create-draft-release.yml
new file mode 100644
index 0000000000..f289321afa
--- /dev/null
+++ b/.github/workflows/create-draft-release.yml
@@ -0,0 +1,42 @@
+name: Build static files and create draft release
+
+on:
+ push:
+ tags:
+ - "*"
+ workflow_dispatch:
+
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 18
+
+ - name: Build frontend static files
+ working-directory: ./mathesar_ui
+ run: npm ci && npm run build
+
+ - name: Move static files
+ run: mv ./mathesar/static/mathesar ./static_files
+
+ - name: Zip static files
+ uses: montudor/action-zip@v1
+ with:
+ args: zip -qq -r static_files.zip static_files
+
+ - name: Create a draft release
+ id: create_release
+ uses: shogo82148/actions-create-release@v1
+ with:
+ draft: true
+
+ - name: Upload assets
+ uses: shogo82148/actions-upload-release-asset@v1
+ with:
+ upload_url: ${{ steps.create_release.outputs.upload_url }}
+ asset_path: static_files.zip
\ No newline at end of file
diff --git a/.github/workflows/test-and-lint-code.yml b/.github/workflows/test-and-lint-code.yml
index ca04c29e0b..0aa3149f3a 100644
--- a/.github/workflows/test-and-lint-code.yml
+++ b/.github/workflows/test-and-lint-code.yml
@@ -28,7 +28,7 @@ jobs:
- uses: actions/checkout@v4
- name: Get changed files
id: changed_files
- uses: tj-actions/changed-files@v39
+ uses: tj-actions/changed-files@v41
with:
files: |
*.py
@@ -44,7 +44,7 @@ jobs:
- uses: actions/checkout@v4
- name: Get changed files
id: changed_files
- uses: tj-actions/changed-files@v39
+ uses: tj-actions/changed-files@v41
with:
files: '**.py'
@@ -55,9 +55,11 @@ jobs:
tests_should_run: ${{ steps.changed_files.outputs.any_changed }}
steps:
- uses: actions/checkout@v4
+ - name: echo
+ run: echo "${{needs.all_tests_required.outputs.tests_should_run}}"
- name: Get changed files
id: changed_files
- uses: tj-actions/changed-files@v39
+ uses: tj-actions/changed-files@v41
with:
files: '**.sql'
@@ -70,10 +72,26 @@ jobs:
- uses: actions/checkout@v4
- name: Get changed files
id: changed_files
- uses: tj-actions/changed-files@v39
+ uses: tj-actions/changed-files@v41
with:
files: 'mathesar_ui/**'
+ all_be_tests_required:
+ name: Check for file changes requiring all backend tests
+ runs-on: ubuntu-latest
+ outputs:
+ tests_should_run: ${{ steps.changed_files.outputs.any_changed }}
+ steps:
+ - uses: actions/checkout@v4
+ - name: Get changed files
+ id: changed_files
+ uses: tj-actions/changed-files@v41
+ with:
+ files: |
+ **.yml
+ **.sh
+ Dockerfile*
+
################################################################################
## BACK END TEST/LINT RUNNERS ##
## ##
@@ -90,11 +108,13 @@ jobs:
python_tests:
name: Run Python tests
runs-on: ubuntu-latest
- needs: python_tests_required
- if: needs.python_tests_required.outputs.tests_should_run == 'true'
+ needs: [python_tests_required, all_be_tests_required]
+ if: needs.python_tests_required.outputs.tests_should_run == 'true' ||
+ needs.all_be_tests_required.outputs.tests_should_run
strategy:
matrix:
- pg-version: [13, 14, 15]
+ py-version: [3.9-bookworm, 3.10-bookworm, 3.11-bookworm, 3.12-bookworm]
+ pg-version: [13, 14, 15, 16]
steps:
- uses: actions/checkout@v4
- name: Copy env file
@@ -104,8 +124,9 @@ jobs:
- name: Fix permissions
run: sudo chown -R 1000:1000 .
- name: Build the stack
- run: docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build -d test-service
+ run: docker compose -f docker-compose.dev.yml up --build -d test-service
env:
+ PYTHON_VERSION: ${{ matrix.py-version }}
PG_VERSION: ${{ matrix.pg-version }}
- name: Run tests with pytest
run: docker exec mathesar_service_test ./run_pytest.sh
@@ -113,11 +134,12 @@ jobs:
sql_tests:
name: Run SQL tests
runs-on: ubuntu-latest
- needs: sql_tests_required
- if: needs.sql_tests_required.outputs.tests_should_run == 'true'
+ needs: [sql_tests_required, all_be_tests_required]
+ if: needs.sql_tests_required.outputs.tests_should_run == 'true' ||
+ needs.all_be_tests_required.outputs.tests_should_run
strategy:
matrix:
- pg-version: [13, 14, 15]
+ pg-version: [13, 14, 15, 16]
steps:
- uses: actions/checkout@v4
- name: Copy env file
@@ -127,7 +149,7 @@ jobs:
- name: Fix permissions
run: sudo chown -R 1000:1000 .
- name: Build the test DB
- run: docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build -d dev-db
+ run: docker compose -f docker-compose.dev.yml up --build -d dev-db
env:
PG_VERSION: ${{ matrix.pg-version }}
- name: Run tests with pg_prove
@@ -136,8 +158,9 @@ jobs:
python_lint:
name: Run Python linter
runs-on: ubuntu-latest
- needs: python_lint_required
- if: needs.python_lint_required.outputs.lint_should_run == 'true'
+ needs: [python_lint_required, all_be_tests_required]
+ if: needs.python_lint_required.outputs.lint_should_run == 'true' ||
+ needs.all_be_tests_required.outputs.tests_should_run
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
@@ -153,8 +176,9 @@ jobs:
vulture:
name: Find unused code
runs-on: ubuntu-latest
- needs: python_lint_required
- if: needs.python_lint_required.outputs.lint_should_run == 'true'
+ needs: [python_lint_required, all_be_tests_required]
+ if: needs.python_lint_required.outputs.lint_should_run == 'true' ||
+ needs.all_be_tests_required.outputs.tests_should_run
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
diff --git a/Caddyfile b/Caddyfile
index c37041dd72..ece8bcbed1 100644
--- a/Caddyfile
+++ b/Caddyfile
@@ -12,33 +12,14 @@
file_server {
precompressed br zstd gzip
- root {$MEDIA_ROOT:/mathesar/media/}
+ root {$MEDIA_ROOT:/code/media/}
}
}
handle_path /static/* {
file_server {
precompressed br zstd gzip
- root {$STATIC_ROOT:/mathesar/static/}
+ root {$STATIC_ROOT:/code/static/}
}
}
- # Rewrite and reverse proxy upgrade endpoint calls to Watchtower;
- # Accepts only POST requests, rewrites them to GET.
- @upgrade_request {
- path /api/ui/v0/upgrade/
- method POST
- }
- handle @upgrade_request {
- rewrite * /v1/update
- method * GET
- reverse_proxy watchtower:8080 {
- header_up Authorization "Bearer mytoken"
- transport http {
- # We want keepalive connections to stay open as long as a dockerhub pull
- # might take, because Watchtower responds to the upgrade request only when
- # it's finished upgrading.
- keepalive 0.5h
- }
- }
- }
reverse_proxy mathesar_service:8000
}
diff --git a/Dockerfile b/Dockerfile
index 7a3fdb20c5..49f667b82a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,24 +1,19 @@
-FROM python:3.9-buster
-ARG PYTHON_REQUIREMENTS=requirements.txt
+#=========== STAGE: BASE =====================================================#
+ARG PYTHON_VERSION=3.9-bookworm
+FROM python:$PYTHON_VERSION AS base
+
ENV PYTHONUNBUFFERED=1
ENV DOCKERIZE_VERSION v0.6.1
-ENV NODE_MAJOR 18
ARG BUILD_PG_MAJOR=15
ENV PG_MAJOR=$BUILD_PG_MAJOR
RUN set -eux;
-#---------- 1. INSTALL SYSTEM DEPENDENCIES -----------------------------------#
-
RUN mkdir -p /etc/apt/keyrings;
# Add Postgres source
RUN curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - ; \
- echo "deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main" > /etc/apt/sources.list.d/pgdg.list;
-
-# Add Node.js source
-RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \
- echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list;
+ echo "deb http://apt.postgresql.org/pub/repos/apt/ bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list;
# Install common dependencies
RUN apt-get update && \
@@ -28,7 +23,6 @@ RUN apt-get update && \
curl \
gnupg \
gettext \
- nodejs \
locales \
&& rm -rf /var/lib/apt/lists/*
@@ -42,18 +36,12 @@ RUN apt-get update && \
postgresql-$PG_MAJOR postgresql-client-$PG_MAJOR postgresql-contrib-$PG_MAJOR \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
-
-#---------- 2. CONFIGURE SYSTEM DEPENDENCIES ---------------------------------#
-
-# Postgres
-
ENV PATH $PATH:/usr/lib/postgresql/$PG_MAJOR/bin
ENV PGDATA /var/lib/postgresql/mathesar
VOLUME /etc/postgresql/
VOLUME /var/lib/postgresql/
-
# We set the default STOPSIGNAL to SIGINT, which corresponds to what PostgreSQL
# calls "Fast Shutdown mode" wherein new connections are disallowed and any
# in-progress transactions are aborted, allowing PostgreSQL to stop cleanly and
@@ -64,17 +52,76 @@ STOPSIGNAL SIGINT
EXPOSE 5432
+# Mathesar source
+WORKDIR /code/
+COPY . .
-#---------- 3. SETUP MATHESAR ------------------------------------------------#
-WORKDIR /code/
+#=========== STAGE: DEVELOPMENT ==============================================#
-COPY requirements* ./
-RUN pip install --no-cache-dir -r ${PYTHON_REQUIREMENTS} --force-reinstall sqlalchemy-filters
-COPY . .
+FROM base AS development
+
+ENV NODE_MAJOR 18
+
+# Install dev requirements
+RUN pip install --no-cache-dir -r requirements-dev.txt
+# Compile translation files
+RUN python manage.py compilemessages
+
+# Add NodeJS source
+RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg; \
+ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list;
+
+# Install node
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends \
+ nodejs \
+ && rm -rf /var/lib/apt/lists/*
+
+# Build frontend source
RUN cd mathesar_ui && npm ci && npm run build
EXPOSE 8000 3000 6006
-ENTRYPOINT ["./run.sh"]
\ No newline at end of file
+ENTRYPOINT ["./dev-run.sh"]
+
+
+#=========== STAGE: COMMON ===================================================#
+
+from base as common
+
+# Install prod requirements
+RUN pip install --no-cache-dir -r requirements-prod.txt
+
+# Compile translation files
+RUN python manage.py compilemessages
+
+# Copy built frontend static files
+COPY --from=development /code/mathesar/static/mathesar ./mathesar/static/mathesar/
+
+# Remove FE source, tests, docs
+RUN rm -rf ./mathesar_ui
+RUN rm -rf ./mathesar/tests ./db/tests
+RUN rm -rf ./docs
+
+
+#=========== STAGE: DEMO =====================================================#
+
+FROM common AS demo
+
+# Install prod requirements
+RUN pip install --no-cache-dir -r requirements-demo.txt
+
+EXPOSE 8000
+
+ENTRYPOINT ["./run.sh"]
+
+
+#=========== STAGE: PRODUCTION ===============================================#
+
+FROM common AS production
+
+EXPOSE 8000
+
+ENTRYPOINT ["./run.sh"]
diff --git a/Dockerfile.integ-tests b/Dockerfile.integ-tests
index 0e1989f3e9..ad04a97416 100644
--- a/Dockerfile.integ-tests
+++ b/Dockerfile.integ-tests
@@ -131,7 +131,7 @@ COPY requirements.txt .
COPY requirements-dev.txt .
COPY requirements-demo.txt .
-RUN pip install -r requirements.txt --force-reinstall sqlalchemy-filters
+RUN pip install -r requirements.txt
RUN pip install -r requirements-dev.txt
RUN pip install -r requirements-demo.txt
COPY . .
diff --git a/README.md b/README.md
index 7344f65aea..df5b5dd66b 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,6 @@
-
diff --git a/config/context_processors.py b/config/context_processors.py
index e4d87670df..85d6fa80a7 100644
--- a/config/context_processors.py
+++ b/config/context_processors.py
@@ -8,19 +8,26 @@
def frontend_settings(request):
manifest_data = get_manifest_data()
development_mode = settings.MATHESAR_MODE == 'DEVELOPMENT'
+ display_language = get_display_language_from_request(request)
+ fallback_language = settings.FALLBACK_LANGUAGE
- i18n_settings = get_i18n_settings(manifest_data, development_mode)
frontend_settings = {
'development_mode': development_mode,
'manifest_data': manifest_data,
'live_demo_mode': getattr(settings, 'MATHESAR_LIVE_DEMO', False),
'live_demo_username': getattr(settings, 'MATHESAR_LIVE_DEMO_USERNAME', None),
'live_demo_password': getattr(settings, 'MATHESAR_LIVE_DEMO_PASSWORD', None),
- **i18n_settings
+ 'display_language': display_language,
+ 'include_i18n_fallback': display_language != fallback_language,
}
# Only include development URL if we're in development mode.
if frontend_settings['development_mode'] is True:
frontend_settings['client_dev_url'] = settings.MATHESAR_CLIENT_DEV_URL
+ i18n_settings = get_i18n_settings_dev(display_language)
+ else:
+ i18n_settings = get_i18n_settings_prod(display_language, manifest_data)
+
+ frontend_settings = {**frontend_settings, **i18n_settings}
return frontend_settings
@@ -36,30 +43,33 @@ def get_display_language_from_request(request):
return lang_from_locale_middleware
-def get_i18n_settings(manifest_data, development_mode):
- """
- Hard coding this for now
- but will be taken from users model
- and cookies later on
- """
- display_language = 'en'
- fallback_language = 'en'
-
+def get_i18n_settings_dev(display_language):
client_dev_url = settings.MATHESAR_CLIENT_DEV_URL
+ fallback_language = settings.FALLBACK_LANGUAGE
- if development_mode is True:
- module_translations_file_path = f'{client_dev_url}/src/i18n/languages/{display_language}/index.ts'
- legacy_translations_file_path = f'{client_dev_url}/src/i18n/languages/{display_language}/index.ts'
- else:
- try:
- module_translations_file_path = static(manifest_data[display_language]["file"])
- legacy_translations_file_path = static(manifest_data[f"{display_language}-legacy"]["file"])
- except KeyError:
- module_translations_file_path = static(manifest_data[fallback_language]["file"])
- legacy_translations_file_path = static(manifest_data[f"{fallback_language}-legacy"]["file"])
+ return {
+ 'dev_display_language_url': f'{client_dev_url}/src/i18n/languages/{display_language}/index.ts',
+ 'dev_fallback_language_url': f'{client_dev_url}/src/i18n/languages/{fallback_language}/index.ts',
+ }
+
+
+def get_prod_translation_file_urls(language, manifest_data):
+ prod_module_url = static(manifest_data[f"language_{language}"]["file"])
+ prod_legacy_url = static(manifest_data[f"language_{language}_legacy"]["file"])
+
+ return {
+ 'module': prod_module_url,
+ 'legacy': prod_legacy_url,
+ }
+
+
+def get_i18n_settings_prod(display_language, manifest_data):
+ fallback_language = settings.FALLBACK_LANGUAGE
+
+ display_language_urls = get_prod_translation_file_urls(display_language, manifest_data)
+ fallback_language_urls = get_prod_translation_file_urls(fallback_language, manifest_data)
return {
- 'module_translations_file_path': module_translations_file_path,
- 'legacy_translations_file_path': legacy_translations_file_path,
- 'display_language': display_language
+ 'prod_display_language_urls': display_language_urls,
+ 'prod_fallback_language_urls': fallback_language_urls,
}
diff --git a/config/settings/common_settings.py b/config/settings/common_settings.py
index b0e1f60400..4d395ef80d 100644
--- a/config/settings/common_settings.py
+++ b/config/settings/common_settings.py
@@ -15,7 +15,6 @@
from decouple import Csv, config as decouple_config
from dj_database_url import parse as db_url
-from django.utils.translation import gettext_lazy
# We use a 'tuple' with pipes as delimiters as decople naively splits the global
@@ -94,10 +93,21 @@ def pipe_delim(pipe_string):
# See pipe_delim above for why we use pipes as delimiters
DATABASES = {
db_key: db_url(url_string)
- for db_key, url_string in decouple_config('MATHESAR_DATABASES', cast=Csv(pipe_delim))
+ for db_key, url_string in decouple_config('MATHESAR_DATABASES', default='', cast=Csv(pipe_delim))
}
-DATABASES[decouple_config('DJANGO_DATABASE_KEY', default="default")] = decouple_config('DJANGO_DATABASE_URL', cast=db_url, default='sqlite:///db.sqlite3')
+# POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_HOST & POSTGRES_PORT are required env variables for forming a pg connection string for the django database
+# lack of any one of these will result in the internal django database to be sqlite.
+POSTGRES_DB = decouple_config('POSTGRES_DB', default=None)
+POSTGRES_USER = decouple_config('POSTGRES_USER', default=None)
+POSTGRES_PASSWORD = decouple_config('POSTGRES_PASSWORD', default=None)
+POSTGRES_HOST = decouple_config('POSTGRES_HOST', default=None)
+POSTGRES_PORT = decouple_config('POSTGRES_PORT', default=None)
+
+if POSTGRES_DB and POSTGRES_USER and POSTGRES_PASSWORD and POSTGRES_HOST and POSTGRES_PORT:
+ DATABASES['default'] = db_url(f'postgres://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}')
+else:
+ DATABASES['default'] = db_url('sqlite:///db.sqlite3')
for db_key, db_dict in DATABASES.items():
# Engine should be '.postgresql' or '.postgresql_psycopg2' for all db(s),
@@ -261,11 +271,13 @@ def pipe_delim(pipe_string):
# i18n
LANGUAGES = [
- ('en', gettext_lazy('English')),
- ('ja', gettext_lazy('Japanese')),
+ ('en', 'English'),
+ ('ja', 'Japanese'),
]
LOCALE_PATHS = [
'translations'
]
LANGUAGE_COOKIE_NAME = 'display_language'
+FALLBACK_LANGUAGE = 'en'
+
SALT_KEY = SECRET_KEY
diff --git a/config/settings/production.py b/config/settings/production.py
index 18a2b7734c..f2b6b2af72 100644
--- a/config/settings/production.py
+++ b/config/settings/production.py
@@ -1,7 +1,15 @@
from config.settings.common_settings import * # noqa
# Override default settings
+DEBUG = False
+MATHESAR_MODE = 'PRODUCTION'
+'''
+This tells Django to trust the X-Forwarded-Proto header that comes from our proxy,
+and any time its value is 'https', then the request is guaranteed to be secure
+(i.e., it originally came in via HTTPS).
+'''
+SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Use a local.py module for settings that shouldn't be version tracked
try:
diff --git a/db/install.py b/db/install.py
index b525da7bd9..fbeede2466 100644
--- a/db/install.py
+++ b/db/install.py
@@ -8,7 +8,14 @@
def install_mathesar(
- database_name, username, password, hostname, port, skip_confirm
+ database_name,
+ username,
+ password,
+ hostname,
+ port,
+ skip_confirm,
+ create_db=True,
+ root_db='postgres'
):
"""Create database and install Mathesar on it."""
user_db_engine = engine.create_future_engine(
@@ -17,40 +24,53 @@ def install_mathesar(
)
try:
user_db_engine.connect()
- print(f"Installing Mathesar on preexisting PostgreSQL database {database_name} at host {hostname}...")
+ print(
+ "Installing Mathesar on preexisting PostgreSQL database"
+ f" {database_name} at host {hostname}..."
+ )
sql_install.install(user_db_engine)
types_install.install_mathesar_on_database(user_db_engine)
user_db_engine.dispose()
- except OperationalError:
- database_created = _create_database(
- database_name=database_name,
- hostname=hostname,
- username=username,
- password=password,
- port=port,
- skip_confirm=skip_confirm
- )
+ except OperationalError as e:
+ if create_db:
+ database_created = _create_database(
+ db_name=database_name,
+ hostname=hostname,
+ username=username,
+ password=password,
+ port=port,
+ skip_confirm=skip_confirm,
+ root_database=root_db
+ )
+ else:
+ database_created = False
if database_created:
- print(f"Installing Mathesar on PostgreSQL database {database_name} at host {hostname}...")
+ print(
+ "Installing Mathesar on PostgreSQL database"
+ f" {database_name} at host {hostname}..."
+ )
sql_install.install(user_db_engine)
types_install.install_mathesar_on_database(user_db_engine)
user_db_engine.dispose()
else:
print(f"Skipping installing on DB with key {database_name}.")
+ raise e
-def _create_database(database_name, hostname, username, password, port, skip_confirm=True):
+def _create_database(
+ db_name, hostname, username, password, port, skip_confirm, root_database
+):
if skip_confirm is True:
create_database = "y"
else:
create_database = input(
- f"Create a new Database called {database_name}? (y/n) > "
+ f"Create a new Database called {db_name}? (y/n) > "
)
if create_database.lower() in ["y", "yes"]:
- # We need to connect to an existing database inorder to create a new Database.
- # So we use the default database `postgres` that comes with postgres.
- # TODO Throw correct error when the default postgres database does not exists(which is very rare but still possible)
- root_database = "postgres"
+ # We need to connect to an existing database inorder to create a new
+ # Database. So we use the default database `postgres` that comes with
+ # postgres.
+ # TODO Throw correct error when the root database does not exist.
root_db_engine = engine.create_future_engine(
username, password, hostname, root_database, port,
connect_args={"connect_timeout": 10}
@@ -58,17 +78,17 @@ def _create_database(database_name, hostname, username, password, port, skip_con
try:
with root_db_engine.connect() as conn:
conn.execution_options(isolation_level="AUTOCOMMIT")
- conn.execute(text(f'CREATE DATABASE "{database_name}"'))
+ conn.execute(text(f'CREATE DATABASE "{db_name}"'))
root_db_engine.dispose()
- print(f"Created DB is {database_name}.")
+ print(f"Created DB is {db_name}.")
return True
except ProgrammingError as e:
if isinstance(e.orig, InsufficientPrivilege):
- print(f"Database {database_name} could not be created due to Insufficient Privilege")
+ print(f"Database {db_name} could not be created due to Insufficient Privilege")
return False
except Exception:
- print(f"Database {database_name} could not be created!")
+ print(f"Database {db_name} could not be created!")
return False
else:
- print(f"Database {database_name} not created!")
+ print(f"Database {db_name} not created!")
return False
diff --git a/db/records/exceptions.py b/db/records/exceptions.py
index d97331e4d1..346467a983 100644
--- a/db/records/exceptions.py
+++ b/db/records/exceptions.py
@@ -1,12 +1,8 @@
-from sqlalchemy_filters.exceptions import FieldNotFound
-
-
-# Grouping exceptions follow the sqlalchemy_filters exceptions patterns
class BadGroupFormat(Exception):
pass
-class GroupFieldNotFound(FieldNotFound):
+class GroupFieldNotFound(Exception):
pass
diff --git a/db/records/operations/select.py b/db/records/operations/select.py
index ef2f91a759..4da11f90f5 100644
--- a/db/records/operations/select.py
+++ b/db/records/operations/select.py
@@ -42,10 +42,8 @@ def get_records(
'direction' field.
search: list of dictionaries, where each dictionary has a 'column' and
'literal' field.
- See: https://github.com/centerofci/sqlalchemy-filters#sort-format
filter: a dictionary with one key-value pair, where the key is the filter id and
the value is a list of parameters; supports composition/nesting.
- See: https://github.com/centerofci/sqlalchemy-filters#filters-format
group_by: group.GroupBy object
duplicate_only: list of column names; only rows that have duplicates across those rows
will be returned
diff --git a/db/sql/0_msar.sql b/db/sql/0_msar.sql
index 7b7b87f8f6..e16bf53246 100644
--- a/db/sql/0_msar.sql
+++ b/db/sql/0_msar.sql
@@ -482,6 +482,45 @@ SELECT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname=schema_name);
$$ LANGUAGE SQL RETURNS NULL ON NULL INPUT;
+----------------------------------------------------------------------------------------------------
+----------------------------------------------------------------------------------------------------
+-- ROLE MANIPULATION FUNCTIONS
+--
+-- Functions in this section should always involve creating, granting, or revoking privileges or
+-- roles
+----------------------------------------------------------------------------------------------------
+----------------------------------------------------------------------------------------------------
+
+
+-- Create mathesar user ----------------------------------------------------------------------------
+
+
+CREATE OR REPLACE FUNCTION
+msar.create_basic_mathesar_user(username text, password_ text) RETURNS TEXT AS $$/*
+*/
+DECLARE
+ sch_name text;
+ mathesar_schemas text[] := ARRAY['mathesar_types', '__msar', 'msar'];
+BEGIN
+ PERFORM __msar.exec_ddl('CREATE USER %I WITH PASSWORD %L', username, password_);
+ PERFORM __msar.exec_ddl(
+ 'GRANT CREATE, CONNECT, TEMP ON DATABASE %I TO %I',
+ current_database()::text,
+ username
+ );
+ FOREACH sch_name IN ARRAY mathesar_schemas LOOP
+ BEGIN
+ PERFORM __msar.exec_ddl('GRANT USAGE ON SCHEMA %I TO %I', sch_name, username);
+ EXCEPTION
+ WHEN invalid_schema_name THEN
+ RAISE NOTICE 'Schema % does not exist', sch_name;
+ END;
+ END LOOP;
+ RETURN username;
+END;
+$$ LANGUAGE plpgsql RETURNS NULL ON NULL INPUT;
+
+
----------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
-- ALTER SCHEMA FUNCTIONS
diff --git a/db/sql/test_0_msar.sql b/db/sql/test_0_msar.sql
index 659070202b..e21f4712f6 100644
--- a/db/sql/test_0_msar.sql
+++ b/db/sql/test_0_msar.sql
@@ -2057,3 +2057,25 @@ BEGIN
RETURN NEXT is(msar.is_default_possibly_dynamic(tab_id, 6), true);
END;
$$ LANGUAGE plpgsql;
+
+
+CREATE OR REPLACE FUNCTION test_create_basic_mathesar_user() RETURNS SETOF TEXT AS $$
+BEGIN
+ PERFORM msar.create_basic_mathesar_user('testuser', 'mypass1234');
+ RETURN NEXT database_privs_are (
+ 'mathesar_testing', 'testuser', ARRAY['CREATE', 'CONNECT', 'TEMPORARY']
+ );
+ RETURN NEXT schema_privs_are ('msar', 'testuser', ARRAY['USAGE']);
+ RETURN NEXT schema_privs_are ('__msar', 'testuser', ARRAY['USAGE']);
+ PERFORM msar.create_basic_mathesar_user(
+ 'Ro"\bert''); DROP SCHEMA public;', 'my''pass1234"; DROP SCHEMA public;'
+ );
+ RETURN NEXT has_schema('public');
+ RETURN NEXT has_user('Ro"\bert''); DROP SCHEMA public;');
+ RETURN NEXT database_privs_are (
+ 'mathesar_testing', 'Ro"\bert''); DROP SCHEMA public;', ARRAY['CREATE', 'CONNECT', 'TEMPORARY']
+ );
+ RETURN NEXT schema_privs_are ('msar', 'Ro"\bert''); DROP SCHEMA public;', ARRAY['USAGE']);
+ RETURN NEXT schema_privs_are ('__msar', 'Ro"\bert''); DROP SCHEMA public;', ARRAY['USAGE']);
+END;
+$$ LANGUAGE plpgsql;
diff --git a/demo/install/base.py b/demo/install/base.py
index 4e5b9da999..24869ac382 100644
--- a/demo/install/base.py
+++ b/demo/install/base.py
@@ -13,7 +13,9 @@
LIBRARY_ONE = os.path.join(RESOURCES, "library_without_checkouts.sql")
LIBRARY_TWO = os.path.join(RESOURCES, "library_add_checkouts.sql")
DEVCON_DATASET = os.path.join(RESOURCES, "devcon_dataset.sql")
-MOVIES_SQL_BZ2 = os.path.join(RESOURCES, "movie_collection.sql.bz2")
+MOVIES_SQL_TABLES = os.path.join(RESOURCES, "movie_collection_tables.sql")
+MOVIES_SQL_FKS = os.path.join(RESOURCES, "movie_collection_fks.sql")
+MOVIES_CSV = os.path.join(RESOURCES, 'movies_csv')
ARXIV_SETUP_SQL = os.path.join(RESOURCES, 'arxiv_dataset_setup.sql')
ARXIV_PAPERS_PICKLE = os.path.join(RESOURCES, 'arxiv_papers.pickle')
LIBRARY_MANAGEMENT = 'Library Management'
diff --git a/demo/install/datasets.py b/demo/install/datasets.py
index df46d543eb..af8bcf84e2 100644
--- a/demo/install/datasets.py
+++ b/demo/install/datasets.py
@@ -1,15 +1,14 @@
"""This module contains functions to load datasets for the demo."""
-import bz2
import logging
import pickle
from sqlalchemy import text
from demo.install.arxiv_skeleton import setup_and_register_schema_for_receiving_arxiv_data
+from demo.install.library_dataset import load_library_dataset
+from demo.install.movies_dataset import load_movies_dataset
from demo.management.commands.load_arxiv_data import update_arxiv_schema
from demo.install.base import (
- LIBRARY_MANAGEMENT, LIBRARY_ONE, LIBRARY_TWO,
- MOVIE_COLLECTION, MOVIES_SQL_BZ2,
MATHESAR_CON, DEVCON_DATASET,
ARXIV, ARXIV_PAPERS_PICKLE,
)
@@ -17,42 +16,12 @@
def load_datasets(engine):
"""Load some SQL files with demo data to DB targeted by `engine`."""
- _load_library_dataset(engine)
- _load_movies_dataset(engine)
+ load_library_dataset(engine)
+ load_movies_dataset(engine)
_load_devcon_dataset(engine)
_load_arxiv_data_skeleton(engine)
-def _load_library_dataset(engine):
- """
- Load the library dataset into a "Library Management" schema.
-
- Uses given engine to define database to load into.
- Destructive, and will knock out any previous "Library Management"
- schema in the given database.
- """
- drop_schema_query = text(f"""DROP SCHEMA IF EXISTS "{LIBRARY_MANAGEMENT}" CASCADE;""")
- create_schema_query = text(f"""CREATE SCHEMA "{LIBRARY_MANAGEMENT}";""")
- set_search_path = text(f"""SET search_path="{LIBRARY_MANAGEMENT}";""")
- with engine.begin() as conn, open(LIBRARY_ONE) as f1, open(LIBRARY_TWO) as f2:
- conn.execute(drop_schema_query)
- conn.execute(create_schema_query)
- conn.execute(set_search_path)
- conn.execute(text(f1.read()))
- conn.execute(text(f2.read()))
-
-
-def _load_movies_dataset(engine):
- drop_schema_query = text(f"""DROP SCHEMA IF EXISTS "{MOVIE_COLLECTION}" CASCADE;""")
- create_schema_query = text(f"""CREATE SCHEMA "{MOVIE_COLLECTION}";""")
- set_search_path = text(f"""SET search_path="{MOVIE_COLLECTION}";""")
- with engine.begin() as conn, bz2.open(MOVIES_SQL_BZ2, 'rt') as f:
- conn.execute(drop_schema_query)
- conn.execute(create_schema_query)
- conn.execute(set_search_path)
- conn.execute(text(f.read()))
-
-
def _load_devcon_dataset(engine):
drop_schema_query = text(f"""DROP SCHEMA IF EXISTS "{MATHESAR_CON}" CASCADE;""")
create_schema_query = text(f"""CREATE SCHEMA "{MATHESAR_CON}";""")
diff --git a/demo/install/dumpcsvs.py b/demo/install/dumpcsvs.py
new file mode 100644
index 0000000000..23feb3daee
--- /dev/null
+++ b/demo/install/dumpcsvs.py
@@ -0,0 +1,40 @@
+"""
+Dump data for all the tables of a provided schema to separate {table_name}.csv files
+with header as column names.
+
+Usage: python dumpcsvs.py
+"""
+import psycopg
+import csv
+
+DB_NAME = "mathesar"
+DB_USER = "mathesar"
+DB_PASSWORD = "mathesar"
+DB_HOST = "mathesar_dev_db"
+SCHEMA_NAME = "Movie Collection"
+
+conn = psycopg.connect(
+ dbname=DB_NAME,
+ user=DB_USER,
+ password=DB_PASSWORD,
+ host=DB_HOST,
+ port=5432
+)
+
+# get names of tables.
+tables = conn.execute(
+ f"SELECT table_name FROM information_schema.tables WHERE table_schema = '{SCHEMA_NAME}'"
+).fetchall()
+
+for table in tables:
+ table_name = table[0]
+ with open(f'{table_name}.csv', 'w', newline="") as csv_file:
+ csv_writer = csv.writer(csv_file)
+ columns = conn.execute(
+ f"""SELECT column_name FROM information_schema.columns WHERE
+ table_schema = '{SCHEMA_NAME}' AND table_name = '{table_name}';"""
+ ).fetchall()
+ columns = [column[0] for column in columns]
+ csv_writer.writerow(columns)
+ with conn.cursor().copy(f"""COPY "{SCHEMA_NAME}"."{table_name}" TO STDOUT""") as copy:
+ csv_writer.writerows(copy.rows())
diff --git a/demo/install/library_dataset.py b/demo/install/library_dataset.py
new file mode 100644
index 0000000000..14c1f39b32
--- /dev/null
+++ b/demo/install/library_dataset.py
@@ -0,0 +1,29 @@
+"""This module contains functions to load the Library Management dataset."""
+
+from sqlalchemy import text
+from demo.install.base import LIBRARY_MANAGEMENT, LIBRARY_ONE, LIBRARY_TWO
+
+
+def load_library_dataset(engine, safe_mode=False):
+ """
+ Load the library dataset into a "Library Management" schema.
+
+ Args:
+ engine: an SQLAlchemy engine defining the connection to load data into.
+ safe_mode: When True, we will throw an error if the "Library Management"
+ schema already exists instead of dropping it.
+
+ Uses given engine to define database to load into.
+ Destructive, and will knock out any previous "Library Management"
+ schema in the given database, unless safe_mode=True.
+ """
+ drop_schema_query = text(f"""DROP SCHEMA IF EXISTS "{LIBRARY_MANAGEMENT}" CASCADE;""")
+ create_schema_query = text(f"""CREATE SCHEMA "{LIBRARY_MANAGEMENT}";""")
+ set_search_path = text(f"""SET search_path="{LIBRARY_MANAGEMENT}";""")
+ with engine.begin() as conn, open(LIBRARY_ONE) as f1, open(LIBRARY_TWO) as f2:
+ if safe_mode is False:
+ conn.execute(drop_schema_query)
+ conn.execute(create_schema_query)
+ conn.execute(set_search_path)
+ conn.execute(text(f1.read()))
+ conn.execute(text(f2.read()))
diff --git a/demo/install/movies_dataset.py b/demo/install/movies_dataset.py
new file mode 100644
index 0000000000..b45c1dbeaa
--- /dev/null
+++ b/demo/install/movies_dataset.py
@@ -0,0 +1,29 @@
+"""This module contains functions to load the Movie Collection dataset."""
+import os
+from sqlalchemy import text
+
+from demo.install.base import MOVIE_COLLECTION, MOVIES_SQL_TABLES, MOVIES_CSV, MOVIES_SQL_FKS
+
+
+def load_movies_dataset(engine, safe_mode=False):
+ """
+ Load the movie demo data set.
+
+ Args:
+ engine: an SQLAlchemy engine defining the connection to load data into.
+ safe_mode: When True, we will throw an error if the "Movie Collection"
+ schema already exists instead of dropping it.
+ """
+ drop_schema_query = text(f"""DROP SCHEMA IF EXISTS "{MOVIE_COLLECTION}" CASCADE;""")
+ with engine.begin() as conn, open(MOVIES_SQL_TABLES) as f, open(MOVIES_SQL_FKS) as f2:
+ if safe_mode is False:
+ conn.execute(drop_schema_query)
+ conn.execute(text(f.read()))
+ for file in os.scandir(MOVIES_CSV):
+ table_name = file.name.split('.csv')[0]
+ with open(file, 'r') as csv_file:
+ conn.connection.cursor().copy_expert(
+ f"""COPY "{MOVIE_COLLECTION}"."{table_name}" FROM STDIN DELIMITER ',' CSV HEADER""",
+ csv_file
+ )
+ conn.execute(text(f2.read()))
diff --git a/demo/install/resources/movie_collection.sql.bz2 b/demo/install/resources/movie_collection.sql.bz2
deleted file mode 100644
index 1d93bc315534fe0230306b2715258d7b032373a7..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 6375862
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